页面 · Mark It Down
Platform architecture — web · desktop · VSCode · Chrome
Issue: #328
Mark It Down runs on four surfaces backed by one cloud service:
apps/web — the ToGo service
Built on the togo framework (Go microkernel
- plugins) with a Vite + React 19 + TanStack Router SPA. The SPA deliberately uses
no UI kit and no Tailwind: it imports the desktop app's design system straight
from
packages/ui-tokens(tokens, primitives, icons) andpackages/core(the 25 themes) via the@mid/*aliases invite.config.ts, and ports the desktop shell (titlebar · activity bar · sidebar · statusbar), settings groups, onboarding frame, welcome hero, and notes list under the samemid-*class names — so web and desktop look identical and share one theme engine (src/lib/theme.tsx). Installed plugins:auth,auth-mfa,auth-dev(dev only),mail,cache,storage,realtime,i18n.
| Surface | Where |
|---|---|
| Landing page (install links per platform) | / — reads /api/updates |
| Auth: register / login / logout / me | /api/auth/* (auth plugin) |
| Password reset | email OTP: POST /api/auth/otp (purpose reset) + otp/verify — delivery bridged to the mail plugin in internal/server/otpmail.go |
| TOTP 2FA | app-gated login POST /api/auth/mfa-login → challenge → POST /api/auth/mfa/totp/verify (see internal/server/mfa.go; self-service enroll/disable is pinned to the session identity) |
| Personal access tokens | POST/GET/DELETE /api/auth/tokens — bearer togo_pat_… works everywhere a session does |
| Notes warehouse | GET/POST /api/notes, PUT/DELETE /api/notes/{id} — always scoped to the caller (internal/rest/note_handler.go); admins may pass ?all=1 |
| Public sharing | POST /api/notes/{id}/share → slug; anonymous GET /api/shared/{slug} returns sanitized HTML (goldmark + bluemonday); SPA page /s/{slug} |
| MCP connector | POST /mcp — Streamable HTTP behind the auth middleware; tools list_notes, get_note, create_note, update_note, delete_note, search_notes (parity with the stdio server in src/mcp/server.ts) |
| Update feed | GET /api/updates and /api/updates/{platform} (windows mac linux vscode chrome), 10-min cached proxy over GitHub Releases |
| Admin | /api/admin/users (+ impersonate / reset / magic-link), /api/admin/mail, resource CRUD driven by /api/_meta/resources |
Run it locally:
Or with the togo CLI: togo dev (both), togo generate && togo migrate after
editing togo.resources.yaml.
Connecting an MCP client
Mint a token in Profile → Access tokens, then:
apps/mobile — the Flutter companion (Android/iOS)
Same brand, same design system, same backend:
- Design tokens:
lib/theme/tokens.dartmirrorspackages/ui-tokens/tokens.css(zinc light/dark/sepia palettes, spacing/radius/type scale) as Flutter ThemeData. - The 25 named themes are code-generated from
packages/core/src/themes/themes.tsintolib/theme/themes.g.dart— regenerate withnode scripts/gen-mobile-themes.mjs. - Brand mark is drawn natively (
lib/widgets/brand.dart, same art asmedia/brand/icon.svg); launcher icons come frombuild/icons/1024.pngviadart run flutter_launcher_icons. - Warehouse client (
lib/api/) talks to the same ToGo backend as every other surface:POST /api/auth/mfa-login(with the TOTP challenge + recovery-code flow), register, emailed-code password reset, and the user-scoped notes CRUD + public sharing. The bearer token lives in secure storage; the server URL is changeable in Settings (defaulthttps://markitdown.fadymondy.com). - Build:
cd apps/mobile && flutter build apk --release(Android) /flutter build ipa(macOS host required for iOS).
Auto-update matrix
GitHub Releases is the single source of truth. Every surface updates from it — directly or through a store that mirrors it:
| Surface | Mechanism | Where implemented |
|---|---|---|
| Electron desktop | electron-updater — auto-download, install on quit (channels: stable/beta) | apps/electron/main.ts setupAutoUpdate(); publishes via release.yml |
| VSCode extension | Marketplace auto-update + in-app release checker (6-hourly, "what's new" toast) | src/updates/updateChecker.ts |
| Chrome extension | Chrome Web Store auto-update + daily release check (badge when newer) | apps/chrome-extension/src/background.ts |
| Web app | Self-updating on deploy; serves the feed for everyone else | apps/web/internal/updates/updates.go |
| Mac App Store build | MAS handles updates (electron-updater opted out) | docs/desktop-mac-app-store.md |
The landing page's download buttons and the extensions' checkers all consume
/api/updates/{platform}?redirect=1 (or the raw GitHub API as fallback), so a
new tag on main propagates everywhere with zero extra steps:
Repository layout
packages/* stay source-only on purpose — each app's bundler (tsc/esbuild/vite)
compiles them in place, so there is still no workspace tooling to maintain.
Deploying the web app (markitdown.fadymondy.com)
The web service is published exactly like the other ToGo services on this host (cabrain, circlexo, health…): a container on the WSL stack, exposed through a reverse-SSH tunnel to the Proxmox edge, where Nginx Proxy Manager routes the hostname and Cloudflare fronts it.
The local WSL NPM (18080/18443/8181) and Windows Firewall are not in this path: WSL containers cannot reach Windows-side listeners, so the app runs in WSL and the tunnel picks it up from the published loopback port.
- Build the artifacts on the host (no network needed inside the image):
bash
- Build + run the container from the WSL dir
~/services/markitdown/(docker-compose.ymlbuilds from/mnt/e/Sites/mark-it-down/apps/web, joinsstack_stacknet, publishes127.0.0.1:9321, mountsmarkitdown_datafor the SQLite file andmarkitdown_storage, and readsAUTH_SECRETfrom its private.env):bash - Tunnel — the
markitdown-tunnelScheduled Task (logon trigger) keeps the-R 10.10.10.1:4572:127.0.0.1:9321forward alive; log in.setup/markitdown-tunnel.log. - NPM + DNS (one-time, done): NPM proxy host id 24 →
http://10.10.10.1:4572, Cloudflare A recordmarkitdown→45.129.183.99(proxied). Credentials live in the cabraintogobrain's secrets vault. - Redeploy = rebuild the two artifacts, then
docker compose up -d --build.
Production env set in the image: APP_ENV=production, ADDR=:8080,
WEB_DIST=/app/web/dist, SQLite at /app/data/togo.db; the compose adds
COOKIE_SECURE=1, MAIL_DRIVER=log (switch to SMTP via MAIL_* for real
password-reset emails) and UPDATES_REPO.
Migrations and the developer login on the public host
- The container entrypoint (
apps/web/start.sh) runscmd/migrate(applies the idempotentinternal/db/schema/*.sql) against the mounted SQLite volume before starting the API — a fresh volume is usable on first boot, and redeploys are safe. - The public container runs with
APP_ENV=staging(notproduction) so theauth-devplugin mounts its one-click Login as developer button. Because that route hands out an admin session forDEV_LOGIN_EMAILwith no credentials,internal/server/devlogin.goonly lets it through when the Cloudflare-reported client IP is insideDEV_LOGIN_ALLOW_CIDRS(home WAN + tailnet); any other caller — including requests that reach the container without a Cloudflare/proxy header — gets a 404. Update the CIDR list in~/services/markitdown/docker-compose.ymlwhen the home WAN IP changes.