Skip to content

docs: propose ADR 048 — the Dev Inbox capability - #710

Open
fforootd wants to merge 17 commits into
mainfrom
claude/testkit-otp-adr
Open

docs: propose ADR 048 — the Dev Inbox capability#710
fforootd wants to merge 17 commits into
mainfrom
claude/testkit-otp-adr

Conversation

@fforootd

@fforootd fforootd commented Aug 2, 2026

Copy link
Copy Markdown
Member

Why

Flows that deliver a secret out of band — email verification, email-OTP login, password recovery (ADR 038) — cannot be tested end-to-end, and won't be when they arrive, unless tests can observe what the server sent. The server has no email delivery subsystem at all yet, which makes this the cheapest possible moment to fix the contract. And the platform overview already narrates the capability this ADR specifies: the scratch dashboard's dev inbox and the "Dev Inbox / Bring Your Own / Managed" delivery picker. This ADR is the server-side contract behind that story.

What changed

ADR 048 (Proposed): The Dev Inbox — Captured Outbound Messages. Rewritten from the initial "capture transport" draft after two review inputs — a competitor scan (Supabase, Resend, Clerk, Firebase emulator, Stytch, Better Auth) and a codex review — into a product-capability framing:

  • Composition/delivery split with per-environment delivery modes; capture stores messages in the project/environment-scoped Dev Inbox (ADR 035 scoping, ADR 036 operator plane).
  • Messages are a versioned record contract (schema_version) carrying typed, purpose-discriminated artifacts (code/link — populated by composition from the flow engine's own state, never parsed from templates; variables stays a non-API record of render inputs) plus rendered forms for humans. Two timestamps with distinct meanings: expires_at = artifact/challenge validity (guaranteed shared per record), retained_until = store retention.
  • One cursor-paginated API, three consumers: inbox UI, zitadel dev-inbox JSON commands, and @zitadel/testing (cursor() + waitForMessage({after, to, purpose, artifact}) — the artifact-key predicate makes waitForCode() wait for a code-bearing record, since a purpose may produce link-only messages; keys, never values, appear in list metadata) — no test-level clear(), which would be cross-worker destructive under the kit's shared-instance parallelism. Waits are bounded and cancellable (timeoutMs, AbortSignal, typed timeout error echoing the resolved filters), and every consumer call binds an explicit environment (InstanceHandle.environmentId; CLI --env with deterministic resolution, echoed in the envelope).
  • Zero-config at the front doors, explicit in the binary: zitadel start / startLocalZitadel() enable it, connectZitadel() discovers it; no tee mode.
  • Production-class environments require a real provider, enforced by a provider_required environment policy (ADR 035 defers env-classes, so the ADR defines the enforceable bit instead of a classification) — enforced at deploy, on every provider/policy mutation, at startup, and at send time (a send without a working provider fails loudly, never falls back to capture); one deliberate divergence from the overview's current "Dev Inbox (default)" for claimed production, flagged in the ADR.
  • Browser inbox surfaces never hold the operator credential — and must earn their session (ADR 036): BFF with a scoped HTTP-only session minted only through an authenticated exchange — human auth on claimed surfaces, or a one-time handoff minted by the explicit zitadel dev-inbox open, with the transfer specified: fragment-carried token (never in access logs), POST exchange, history.replaceState, Referrer-Policy: no-referrer, log redaction. zitadel start never emits a bearer-bearing URL — it reports available vs configured capability state and points at open only via a human-facing next_actions hint, never next_commands (which agents execute by contract, and which would also fail pre-setup when no secret exists). The scratch dashboard's anonymous first-visit cookie is called out as insufficient for inbox content — a second overview amendment.
  • List responses are metadata-only — masking variables alone would be theater while rendered.* embeds the same secret; the full message is a separately authorized read. Any durable backend encrypts message content at rest (ADR 029); only the ephemeral in-memory store is exempt.
  • The kit cursor is a high-water mark, not an ADR 027 page token: defined on empty inboxes, ordered by the store's monotonic per-environment sequence, purge-stable, carrying a store epoch (a restarted in-memory store fails old cursors fast instead of hanging waits), with distinct stale-token errors. The rendered preview is sandboxed (iframe, no scripts, restrictive CSP) per the flow-engine template-security guidance.
  • Alternatives rejected with reasoning: external SMTP sink (Supabase's own layering as evidence), log capture, fixed test codes on the real server (Clerk-style — that experience belongs in @zitadel/api-mock), admin-mint (hash-only storage + flow-binding), kit-embedded SMTP listener.
  • Staged build order: contracts → store/API → kit → CLI → UI → real providers.

Also in this branch's history: renumbered as main claimed numbers in flight — 042 → 043 → 044 → 046 → 047 → 048 (042 scaffolded-file ownership, 043 framework version floors #713, 044+045 scaffold posture and copy overlays #714, 046 claim lifecycle v2 #627, 047 dialect-owned identifier generation #663) — with main merged in at each collision; the file is now 048-dev-inbox.md.

Validation

Docs-only.

Review notes

Status is Proposed — a conversation starter. The first review round (livio) recorded a lean on every open question, now folded into the ADR: composition via a typed flow intent + async notification boundary (v4-shaped enqueue → compose → capture|deliver, wide-events/River as the bus); durable store only on multi-replica / shared scratch inbox / unread-survives-restart (ADR 029 bar applies equally to queue payloads); default-deny support access until #333; optional explicit consumed_at over auto-consume; one operator port with an optional path-prefix kill-switch. The same round added a normative purpose × artifact-key table, the static email-dependence detection signal for the provider_required gates, and an "On acceptance" checklist carrying both platform-overview amendments (claimed-production delivery default; scratch-inbox session acquisition). The leans harden into decisions when the ADR moves to Accepted.

A later main merge brought #722's ADR 036 amendment (credential exposure contracts), which governs exactly the fragment-carried handoff token this ADR specifies — the BFF section now carries an explicit conformance passage instead of a silent divergence: the exchange behaves as a credential-establishing operation (browser-attested Origin enforced, so an exfiltrated token can't be exchanged from a foreign context), the minted session is inbox-scoped and can never become a login session, and the endpoint adopts ADR 036's PKCE-style proof binding for URL-transiting handoffs once that machinery ships.

Copilot AI review requested due to automatic review settings August 2, 2026 15:59
@vercel

vercel Bot commented Aug 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
nextgen Ready Ready Preview Aug 4, 2026 6:23am
nextgen-docs Ready Ready Preview Aug 4, 2026 6:23am
nextgen-mock-zitadel Ready Ready Preview Aug 4, 2026 6:23am

Request Review

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

⚠️ No Changeset found

Latest commit: 25d30be

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Proposes ADR 043 to define a future server-side “capture” transport for outbound email/OTP messages so end-to-end tests (via @zitadel/testing) can reliably observe verification codes / links without relying on SMTP sinks or log scraping.

Changes:

  • Adds proposed ADR 043 describing a transport abstraction with an opt-in, bounded capture buffer and an operator-authenticated read API.
  • Updates the ADR index to include ADR 043.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
docs/adrs/README.md Adds ADR 043 to the ADR index table.
docs/adrs/043-test-visible-email-otp-capture.md New proposed ADR specifying the test-visible capture contract for future email/OTP delivery.

Comment thread docs/adrs/043-test-visible-email-otp-capture.md Outdated
@fforootd fforootd changed the title docs: propose ADR 043 for test-visible email and OTP capture docs: propose ADR 043 — the Dev Inbox capability Aug 2, 2026

@livio-a livio-a left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

Approve as Proposed. Strong capability ADR — composition/delivery split, one operator API for UI/CLI/kit, parallel-safe cursor semantics, and the security/handoff rules are in good shape. Greenfield timing is right.

I love that this solves several pain points we have with Zitadel v4 in one contract: no SMTP-sink / log-scraping for E2E, structured artifacts instead of parsing rendered mail, a real inbox for humans and agents over the same API, parallel-safe waits without destructive clear(), capture as a first-class delivery mode instead of a test-only side channel, and cloud/dev deployments that no longer need a default SMTP just to get a working environment — capture is the zero-config path until a real provider is required.

Accept gate

The two platform-overview amendments already named in the ADR should be scheduled (or linked as follow-ups) when this moves to Accepted:

  1. Claimed production must not default to Dev Inbox (provider_required).
  2. Scratch dashboard anonymous cookie is insufficient for inbox secrets — secret-mediated handoff.

Open questions — provisional leans (happy to discuss)

Recording these so build step 2 doesn’t thrash; fine to keep as open questions with a “lean” note:

  1. Composition locus — prefer an async notification path (flow emits intent; worker composes/delivers or captures). Thin enqueue-from-flow is fine; flow-owned mailer is not. Artifacts still sourced from flow/challenge state as the ADR says.
  2. Durable store — in-memory until shared/cloud/multi-replica (or restart must preserve unread). ADR 029 applies once durable.
  3. Support access — inherit #333; default deny for staff until that model exists. Don’t invent inbox-specific policy here.
  4. Consumed-state — expiry display is enough for correctness. Prefer an optional explicit mark-consumed API (operator plane) over auto-consume on verify (avoids flow coupling / resend races). Kit ignores it.
  5. Port — same operator port/API. Later optional runtime flag (or gateway rule) to disable/block the inbox path prefix wholesale — no separate debug listener.

Nice-to-haves (non-blocking)

  • Normative purpose → required-artifacts table (prose “code and/or link” is soft for kit types).
  • One line on how “email-dependent flows” are detected at deploy/mutation time.

Comment thread docs/adrs/047-dev-inbox.md Outdated
Comment thread docs/adrs/048-dev-inbox.md
Comment thread docs/adrs/048-dev-inbox.md
Comment thread docs/adrs/048-dev-inbox.md
Comment thread docs/adrs/047-dev-inbox.md Outdated
Comment thread docs/adrs/047-dev-inbox.md Outdated
Comment thread docs/adrs/047-dev-inbox.md Outdated
Comment thread docs/adrs/047-dev-inbox.md Outdated
Comment thread docs/adrs/047-dev-inbox.md Outdated
@fforootd
fforootd enabled auto-merge (squash) August 3, 2026 13:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Inbox

Development

Successfully merging this pull request may close these issues.

3 participants