One-liner: The build order from empty repo to web, desktop, and mobile — web-first, because every shell wraps the same React SPA and talks to the same server-side Hono backend, so the SPA + backend must exist before Tauri can wrap anything. Each phase is a vertical slice that ends in something runnable.
This is the sequencing doc. What we're building and why live in the companion docs —
features.md (tiers), stack.md, architecture.md,
data-model.md, platforms.md, repo-structure.md.
- Vertical slices, not layers. Each phase ends with something you can run and see, not a half-finished abstraction. The first real milestone is a webhook lands and you can read it.
- Web before shells. Tauri desktop/mobile are thin wrappers around the SPA pointed at a backend URL. There is nothing to wrap until the web app works — so Phases 0–5 are web/backend, and the shells (7–8) come after. The PWA (6) is free once the SPA exists.
- Node target first, Workers second. Local Node + SQLite is the fastest dev loop; the Cloudflare Workers + D1 target is the same code behind the storage adapter, hardened in Phase 5.
- Keep V1 small. Phases 1–4 deliver the
features.mdV1 tier and nothing more.
Status — ✅ done (built & tested) · 🚧 in progress · ⬜ not started (as of 2026-06-23).
| # | Phase | Status | Track | Delivers | Depends on |
|---|---|---|---|---|---|
| 0 | Scaffold & foundations | ✅ | web | Workspaces monorepo that builds & runs empty | — |
| 1 | Ingest slice | ✅ | backend | A real webhook lands in the own store | 0 |
| 2 | Read UI | 🚧 | web | Browse/read real mail in the browser | 1 |
| 3 | Compose & reply | ✅ (backend) | web | Reply that threads correctly | 2 |
| 4 | Organize & search | 🚧 | web | The full V1 inbox (labels, search, shortcuts) | 3 |
| 4.5 | Admin & Setup dashboard | ✅ (backend) | web+backend | Roles (admin/user), admin-only Settings/Setup, config surfacing | 3 |
| 5 | Workers target + deploy | ✅ | backend | Hosted on Workers + D1 + R2 (live on workers.dev) | 1–4 |
| 6 | PWA | ⬜ | web | Installable app, push notifications | 2 (4 ideal) |
| 7 | Desktop (Tauri 2) | ⬜ | desktop | Signed macOS/Windows/Linux installers | 4 |
| 8 | Mobile (Tauri 2) | ⬜ | mobile | iOS + Android test-track builds | 4, 7 |
| 9 | Hardening & OSS launch | ⬜ | all | Self-host story, CI releases, public launch | 5–8 |
Backend ahead of UI: the Phase 3/4.5 server routes (
/api/send, auth, admin config) are built and covered by tests inapps/web/testandpackages/core/test; the matching SPA screens are the remaining 🚧 work. The whole API surface is documented inauth.mdandadmin-dashboard.md.
Critical path: 0 → 1 → 2 → 3 → 4 → 4.5, then 5/6/7 can run in parallel, 8 after 7. (User roles and the admin-only Settings/Setup dashboard land in 4.5 — see admin-dashboard.md.)
Goal: an empty but real workspaces monorepo that builds, lints, and runs.
- pnpm workspaces + Turborepo (
pnpm-workspace.yaml,turbo.json, root scripts) perrepo-structure.md. - Skeletons:
packages/core,packages/ui,apps/web,apps/desktop,apps/mobile. - Shared TS config, ESLint/Prettier, Vitest, CI (typecheck + build + test).
packages/ui: Tailwind CSS 4 inline@themewith brand tokens lifted from the website, shadcn/ui (new-york) initialized, the design-token block.apps/web: a Hono app serving a near-empty Vite/React SPA (dev via Node).- Exit:
pnpm devserves a blank styled SPA atlocalhost;turbo buildis green in CI.
Goal: a real MailKite webhook is verified, stored, and visible in the DB.
apps/webHonoPOST /webhook: verifyx-mailkite-signature(HMAC-SHA256, timestamp tolerance, constant-time compare), then normalize theemail.receivedpayload.packages/core(@mailkite/core/server): theSqlDriver/BlobStore/MailRepoadapter interfaces + the SQLite driver (Node); schema fromdata-model.md(messages,threads,attachments,ingest_log, …) + migrations.- Idempotent ingest (dedupe via
ingest_log); fetch-and-rehost attachments at ingest (fsBlobStoreon Node). - Exit: point a MailKite route's webhook at the local
/webhook(cloudflared tunnel) → send a real email → the message + attachments persist; replay is a no-op.
Goal: browse, thread, and read real mail in the browser.
packages/ui: app shell (sidebar + list + reading pane), inbox list, conversation/thread view, message reader.- Safe HTML rendering: sanitize, proxy remote images, link handling, CSP — the security work from
architecture.md. apps/weblocal/api/*: list messages, get thread/message, serve rehosted attachments.- Auth: HMAC-signed session cookie (
SESSION_SECRET), login screen — the as-built model is inauth.md. TanStack Router + Query wired. - Exit: log in and read real, correctly-threaded mail with working attachments.
Goal: send a reply that threads correctly at the recipient.
- Compose UI (rich + plaintext), reply / reply-all / forward, quoting, identities, drafts.
apps/webPOST /api/send→ proxies to MailKite/v1/sendwithinReplyTo(RFC-5322In-Reply-To/References).- Exit: reply to a stored message → it arrives and threads in the original client.
Goal: the full features.md V1 inbox.
- Read/unread, archive, trash, star/pin, labels (local constructs), full-text search over the own store, keyboard shortcuts, derived contacts.
- Exit: every V1-tier feature works; V2/Later explicitly deferred.
Goal: any self-hoster can configure the app, and only an admin can.
- Roles:
users.role∈ {admin,user}. Theadminsees Settings/Setup; auseronly gets mail. Single-operator installs have one admin. - Admin gate:
requireAdminmiddleware on/api/admin/*and the dashboard route; non-admins get 403. Bootstrapped byADMIN_PASSWORDenv, or a first-run setup wizard that creates the admin (hashed in the store). - Config surfacing: admin Settings shows each item's status — ✅ from env / ✅ saved /
⚠️ missing (secrets masked) — with inputs to save the non-platform ones. - Env-first resolution:
env var → saved DB setting → unset; unset → the feature disables via the capability gate. Same on Workers and VPS. - Auth: PBKDF2 passwords + HMAC-signed session cookies +
requireAuth/requireAdmin— seeauth.md. - See
admin-dashboard.md. - Exit: a fresh deploy walks an admin through setup; missing keys disable their features and are visible in Settings.
Goal: the same app live on mailn.app, server-side on Cloudflare.
- ✅ Second storage impl behind the adapter: D1
SqlDriver(D1Driver) + R2BlobStore(R2BlobStore), via@mailkite/core/server/workers. MailRepo unchanged — providers swap behind the ports. - ✅
apps/webon Workers:assetsbinding (SPA) +worker.tsfetch handler (owns/api/*+/webhook);wrangler.jsonc; D1 migrations inapps/web/migrations/(drift-tested againstSCHEMA_SQL). - ✅ Deployed: D1 + R2 provisioned, migrations applied,
SESSION_SECRETset, live athttps://mailkite-mail.worker-sendhub.workers.dev— seedeploy.md. Health/config/SPA/auth-gate all verified. Remaining: setMAILKITE_API_KEY/_WEBHOOK_SECRET(via Settings orwrangler secret put) and point a MailKite route's webhook at/webhook. - Exit: ✅ hosted inbox works end-to-end on Workers, identical behavior to Node.
Goal: installable app + notifications, no app store.
- Web app manifest, service worker (offline read cache — not authoritative), install prompt.
- Web Push (backend notifies the device on new mail).
- Exit: installs as a PWA on desktop + mobile; push arrives.
Goal: signed desktop apps wrapping the SPA.
apps/desktop: Tauri 2 loading the built SPA, pointed at a configurable backend URL (mailn.appor self-host).- Native: OS notifications, deep links (
mailkite://thread/:id), secure token storage (Keychain/credential store), dock/taskbar badge counts. - Build + code-sign + package macOS / Windows / Linux.
- Exit: installable signed desktop builds; native notifications + deep links work.
Goal: mobile apps from the same SPA.
apps/mobile: Tauri 2 mobile; Xcode/Android Studio prereqs; mobile webview parity check.- Native: push (APNs/FCM), deep links, secure storage, share-sheet for attachments.
- Exit: TestFlight (iOS) + Play internal-testing (Android) builds installable on devices.
Goal: ready to hand to self-hosters and the public.
- The
install.mdflows (npx / Docker / Workers), config validation, error handling, observability, rate limits, attachment size caps, backup/restore. CONTRIBUTING.md, issue/PR templates, CI release pipeline (web deploy, desktop installers, mobile bundles perrepo-structure.md).- Exit: public OSS launch — a stranger can self-host from the README in minutes.
Decision (2026-06): ship a web vertical slice first (Phases 0–4 → V1), then fan out to Workers/PWA/desktop in parallel, with mobile last. Rationale: the Tauri shells are thin wrappers around the SPA + backend, so they have nothing to wrap until the web app is real — front-loading desktop/mobile would mean building shells around a moving target.
- Serial core: 0 → 1 → 2 → 3 → 4.
- Then parallel: 5 (Workers/deploy), 6 (PWA), 7 (desktop) can proceed independently once 4 lands.
- Mobile (8) follows desktop (7) — it reuses the Tauri config, signing, and native-capability wiring established for desktop.
- 9 is continuous polish that closes out for launch.
Start Phase 0 + the Phase 1 slice together: stand up the workspaces skeleton, then immediately
build POST /webhook + the SQLite MailRepo so a real email lands in the store. That single slice
de-risks the whole architecture (signature verify, idempotency, payload mapping, attachment rehost)
before any UI is built.