mirror-habit now ships as an Obsidian community plugin, not just a web app. Same React core, opened in a pane inside the vault.

How it’s built

  • The React app (React 19 + Vite + Supabase) is synced into src/app/ from the main repo by scripts/sync-source.sh, then apply-patches.mjs adapts it to Obsidian — scoped styles, a lazy Supabase client, and Obsidian-safe auth links. One source, two targets (web + plugin).
  • Offline-first: with no config, everything runs on local vault data via the plugin’s data.json — habit tracking, streaks, challenges, titles/levels. Supabase adds multi-device sync when you supply a URL + anon key.

The two real gotchas

  1. Plugin view crash — the view registered as new MirrorHabitView() without passing the leaf, so opening the pane threw Cannot read properties of undefined (reading 'app'). Fix: (leaf) => new MirrorHabitView(leaf).

  2. OAuth inside Obsidian’s sandbox — the plug-in origin can’t complete a Google redirect flow, so Google sign-in opens the web app in a browser tab (mirrorhabit.pages.dev/login) and the plugin watches for the session to land. This is also why the app’s default URL had to be corrected to pages.dev (it was still netlify.app in several spots after the migration).

Shipping notes

  • Built via esbuild (JS) + a small CSS build step, outputs main.js + styles.css copied into the vault’s .obsidian/plugins/.
  • Manifest declares minAppVersion 1.4.0; license MIT; README lays out the required commerce/backend disclosures for community publishing (offline default, external Supabase only when configured, no telemetry).

The “same React core, two surfaces” pattern is holding up — the plugin is mostly plumbing, with the hard thinking already done in the web app.