Nuncio · backend roadmap

From here to a client-ready backend

The plan to take the daemon, reference CLI, and gRPC API from today's working spine to a frozen, versioned contract that native front-ends can start consuming — with parity guaranteed by the API, not by discipline.

Done & on dev In progress Planned Deferred

Where we are · 2026-08-07

Phases 0–2 complete, feature surface (Phase 3–M5) done, M6 nearly done, OBS complete

The daemon boots gRPC-only on loopback 127.0.0.1:9420 behind a keyring-minted bearer token. IMAP → store → read → SMTP send runs end-to-end over gRPC, driven by the nuncio-cli reference client, tested fully offline. The legacy JSON-RPC IPC is gone, and nuncio.v1 is published with a byte-deterministic contract-stability golden. Milestones M1–M5 are now complete on dev (M1–M4 already promoted to main): Calendar, Contacts, JMAP, live Filters, and mail lifecycle + incremental sync are all real, wired, and API-complete. M6 (security & release hardening) is 13 of 16 stories closed, and the OBS observability epic is complete (tracing subscriber, per-RPC spans with request-id correlation, a real GetStatus health surface, CLI verbosity, and a no-secrets-in-logs redaction canary). Work after M5 reorganized into M6.5 (de-fabrication) and the WS-A … WS-F pre-freeze reshape, which now carry the bulk of the remaining effort. M7 has not started; it gates on WS-A.

SystemAccountsMail FiltersCalendarContacts ExportAudit — 8 services, each behind the auth interceptor
Phase 0 · Honesty & foundation Phase 1 · gRPC skeleton + spine Phase 2 · API completeness (spine) 3.A · Filter correctness M1 · Calendar (CalDAV + recurrence) M2 · Contacts (CardDAV) M3 · JMAP M4 · Filters made live M5 · Mail lifecycle + incremental sync OBS · Observability (OBS-1 … OBS-9)

The path · the milestones to the goal line

M1

M1Finish Calendar

Done

The calendar engine is published over the API: a Calendar gRPC service (Sync / ListEvents / GetEvent — honest surface only) behind the auth interceptor, daemon wiring, CLI commands replacing the stub, and recurrence (rrule) wired into the fetch/store path.

Exit: a Calendar view over gRPC, offline-tested sync→list→get, recurring events materialize.
#178done — #185, #186 merged
M2

M2Contacts · CardDAV

Done

The fake CardDAV client (returned one hardcoded contact) is replaced with a real, wiremock-tested client + vCard round-trip; contacts persist through the daemon's store (the CLI previously wrote a throwaway in-memory DB); a Contacts service, CLI, and offline E2E are in place.

Exit: contacts sync and persist across restarts, driven over gRPC.
#179done — #187, #188 merged
M3

M3JMAP

Done

A real JMAP HTTP client (session, Email/query/get/set) with the camelCase wire fix is wired into the same MailBackend fetch→store path as IMAP; wiremock-tested.

Exit: a JMAP account fetches → stores → lists offline, via the existing Mail service.
#180done — #189 merged
M4

M4Filters made live

Done

Rules could previously be created, validated, and previewed as “MATCH” but never fired. FilterEngine::evaluate is now wired into the real sync path so matches trigger their action (move/flag/delete/forward/webhook), the header + account-field bugs are fixed, and filter edit/export/import/logs plus bulk triage are re-exposed over the service.

Exit: an E2E proves a matching rule fires an action against synced mail — no fabricated preview.
#175#176#181done — #190–#193 merged
M5

M5Mail lifecycle + incremental sync

Done

Account lifecycle (edit / delete / test-connection) is real over gRPC, with TLS modes (implicit / STARTTLS / plain) persisted and actually driving the IMAP/SMTP connection. Sync is now genuinely incremental via a per-folder {uidvalidity}:{uidnext} checkpoint (a UIDVALIDITY change forces a safe full re-fetch — no silent mail loss), with a per-item FETCH timeout and SyncProgress events. SendMessage takes an explicit account id (honest AccountNotFound, no silent wrong-account fallback), and a failed AddAccount rolls back its orphaned keyring secret.

Exit: full account lifecycle + incremental sync, offline-E2E-proven, no fabricated IMAP fallbacks.
#170#169#182#183#184done — #194–#197 merged
M6

M6Security & release hardening

13 / 16 done

Fail-closed everywhere: fix the silent empty-ciphertext data-loss path; a signature-verifying updater that refuses to apply without checksums; zeroize key material; validate vault key lengths; gRPC bind hardening with an interceptor guard; real CIDR-based SSRF checks; sandboxed HTML rendering. All of the above have landed except inbound HTML sanitization (#204), Rust static analysis in CodeQL (#250 — CodeQL scans JS and actions only today), and removal of assertion-free test theater (#251).

Exit: a security review passes — no fail-open crypto, update, or egress path remains.
WS

M6.5 · WS · OBSThe work between M6 and M7

In progress

Opened after M5, when the reshape needed before an honest v1 freeze proved larger than M6 alone. M6.5 de-fabrication (3/11) removes the remaining silently-wrong paths: Contacts sync wiring (#255), namespace-aware DAV XML (#257), the TLS-mode fallback (#271), the NL scheduler (#258). WS-A … WS-F (22/78) is the pre-freeze reshape: contract hardening (WS-A, gates M7), mail model & mutations (WS-B), sync/push/lifecycle (WS-C), security to 9/10 (WS-D — SQLCipher #299, UDS & named-pipe IPC #305, signed updates #300), calendar/contacts write-back & search (WS-E), and ops, usability & maintainability (WS-F — including the grpc.rs #333 and DatabaseEngine #334 decompositions). WS-B and WS-C grew when ADR 0002 decided the convergent multi-engine sync model and broke it into #386–#397; the message/placement identity split, change enumeration and verified mutations are implemented and green on a branch, not yet merged. OBS-1 … OBS-9 is complete (9/9, #357–#365): layered tracing subscriber, per-RPC spans with request-id correlation, domain and lifecycle coverage, protocol instrumentation, a real System.GetStatus, CLI -v/-vv with typed errors, and a Redacted<T> policy with a no-secrets-in-logs canary.

Exit: WS-A closes the contract reshape, unblocking the M7 freeze.
★

M7API freeze & client-readiness

The goal line

Freeze nuncio.v1: every backend capability is represented, the contract-stability golden covers the full surface, and the additive-only versioning policy is enforced by test. Verify Swift / C# / TypeScript codegen with a “hello, daemon” per language. The CLI reference client exercises every RPC in offline E2E — the proof the contract is complete and usable. Replace the gamed coverage gate; CI green; tag an honest pre-alpha backend release. Not started — 5 stories open, 0 closed; gates on WS-A completing the contract reshape.

Exit / client-ready: the daemon publishes a frozen, documented, contract-tested gRPC API that the CLI fully exercises offline. Front-end repos may now begin.

After the goal line

Phase 5 · Client projects

Separate repositories consume the frozen API, parity guaranteed by the contract: nuncio-tui (Rust) · nuncio-gui-macos (Swift + grpc-swift) · nuncio-gui-windows (WinUI / C#) · nuncio-mcp (MCP ↔ gRPC bridge).

Deferred until re-justified · not on the critical path

OpenPGP / S-MIME E2EE (#79), WASM / QuickJS plugins (#80), and local-LLM summarization (#81) remain honestly backlogged as stubs. They re-enter scope only as deliberate, scoped decisions after the backend is client-ready — never built as filler to clear a badge.

Contract = the .proto files · a capability is done only when engine + proto + CLI + offline E2E exist and CI is green. nunciod · nuncio.v1 · loopback 127.0.0.1:9420