UI plane
React 19 · TypeScript · Vite
- AppShell — composition and layout only
- Tool registry — 5 modules registered, routing still in AppShell
- Session hooks — image · selection · canvas · mask
- Zustand stores — UI · tool · gallery · annotation · guides
Everything inside the solid boundary runs in your tab. Cut the dashed plane off entirely and what's left is still a complete, working image editor — that isn't a fallback mode, it's the demo everyone gets.
Everything, including the parts that aren’t built yet.
React 19 · TypeScript · Vite
where the pixels are touched
IndexedDB · Dexie
Plus localStorage for lightweight prefs. A service worker that precaches the shell and the WASM binary is written and tested, and switched off — it has never been on in a build that shipped.
identity
prefs sync · entitlements · gallery
the server holds the provider key
ShareViewer
Vite + React 19 · Netlify static SPA
Engine worker · zero-copy blit to an OffscreenCanvas
7 stores · atomic selectors
ImageBuffer · layer stack · composite / mask
Brush / eraser / mask · blur / pixelate / redact
Live text & shape overlays · magic-wand
Clone brush · flip / rotate / resize / crop
Brightness · contrast · gaussian blur
Arrows / shapes / bézier · embedded fonts
PNG encode (Rust) · undo snapshots
v128/f32x4 kernels · scalar fallback
json · point math · shared helpers
identity provider
anonymous · 12 photos
signed in · 24 photos
100 photos · coming soon
save / getEdit
dispatch to Replicate
job status (useQuery)
public share links
recent texts
checkout / portal
Originals · SHA-256 content-addressed · your machine
Edit archives · shares · AI frames · a server
background removal
text extract
object removal (masked)
4× upscale
soonsubscription changes → subscriptions
AI complete → ai_jobs.status
Clerk sign-in isn't a webhook here — the client calls users.upsert once Convex's own auth bridge comes up, and that is what actually creates the users row.
Every table, its fields and its indexes. Flatter than a typical projects → images tree: each row hangs straight off users, keyed by the client's own photoKey string rather than a server-side image id.
by_clerkIdby_email
by_userIdby_stripeCustomerIdby_stripeSubId
by_userId_photoKey
Real per-photo edit persistence path (useEditPersistence.ts)
by_userIdby_userId_usedAt
Text-tool history, per signed-in user
by_tokenby_userId
Public, no-auth read — anyone with the link can view or download
by_userIdby_userId_photoKeyby_replicateIdby_status
Keyed by photoKey, not an images row — the Replicate webhook updates status
useQuery hooks auto-update when data changes. No polling.
ctx.auth in mutations, plus query filters for user-scoped data.
Integrated blob storage for images via storage.getUrl().
Replicate + Stripe post back to convex/http.ts, HMAC-verified.
Because the boundary is the product. An editor that needs a server is an editor that can be switched off, rate-limited, or quietly trained on. The dashed plane buys you sync, sharing, and the AI passes — and it is the only place a photo of yours can travel. Demo mode never crosses it.
The map above is drawn by hand from system-architecture.mermaid, the flowchart the repo ships. Take the original and render it wherever you like — Mermaid Live, a VS Code preview, your own docs.
system-architecture.mermaid · Mermaid flowchart TB · 79 lines · the dashed subgraph is the plane you can cut.