Skip to content

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

Merged
fforootd merged 19 commits into
mainfrom
claude/testkit-otp-adr
Aug 7, 2026
Merged

docs: propose ADR 050 — the Dev Inbox capability#710
fforootd merged 19 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 050 (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 → 050 (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, 048+049 wide events #459) — with main merged in at each collision; the file is now 050-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.

The #459 merge (wide events, ADRs 048/049) touches the composition lean, which named "wide-events/River" as the natural bus: ADR 048 defines wide events as an audit primitive (deny-by-default PII, export-only), so the lean now cross-references it and folds "can the audit stream double as the dispatch bus, or does the boundary ride a job queue with its own payloads" into the already-open event-shape question. ADR 049's events API stays on plain ADR 027 keyset cursors, so it does not collide with this ADR's high-water-mark cursor contract.

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 7, 2026 1:06pm
nextgen-docs Ready Ready Preview Aug 7, 2026 1:06pm
nextgen-mock-zitadel Ready Ready Preview Aug 7, 2026 1:06pm

Request Review

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

⚠️ No Changeset found

Latest commit: f8b98e3

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
@fforootd
fforootd enabled auto-merge (squash) August 3, 2026 13:48
@fforootd fforootd changed the title docs: propose ADR 047 — the Dev Inbox capability docs: propose ADR 048 — the Dev Inbox capability Aug 4, 2026
livio-a added a commit that referenced this pull request Aug 7, 2026
<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
## Summary

Docs-only ADRs for nextgen audit logging (relational SoT + semantic wide
events):

- **[ADR 048](docs/adrs/048-wide-events-internal-audit-primitive.md)** —
internal `events` table, categories, Path A (batched request events) +
Path B (`AllStatements` / `InsertEvent`), deny-by-default PII +
`x-audit`
- **[ADR 049](docs/adrs/049-events-api-retention-export.md)** — unified
`GET /events`, time-only retention, first-class managed sinks,
project-scoped get

Also updates ADR index, ADR 028 Related, resource-map, api README, and
`user-schema.md`.

### Review decisions (latest round)

- **Pre-claim (a):** emit/store always; gate list/get/shipper until
claim (ADR 046)
- **No `resource_scope_index` for events;** `GET /events/{id}` is
project-scoped
- **Retention:** time-only (not sink-gated); `events_purged_undelivered`
metric when aging out undelivered rows
- **Sinks:** first-class CRUD, managed `sink_…` ids; ≤1 deployment
stdout + ≤1 deployment webhook + ≤1 project webhook (additive)
- **Path A:** bounded buffer, drop new enqueue + metric, retry/backoff,
recompute `wait`
- **`x-sensitive`:** kept complementary to `x-audit` (still used by
OpenAPI/console/config)

**Note:** [#710](#710) also
claims ADR 048 (Dev Inbox) — number collision if both merge; coordinate
renumber.

## Validation

- Docs-only; no `moon ci` / Go tests run
- Grep: no sink-gated purge; pre-claim store+visibility; events not
indexed in RSI

## Release notes / changeset

Docs-only — **no changeset** per [`.changeset/README.md` decision
table](.changeset/README.md#decision-table).

## Notes

PR title remains conventional `docs:` (allowed by
`.github/semantic.yml`).
<!-- CURSOR_AGENT_PR_BODY_END -->

closes #350

<div><a
href="https://cursor.com/agents/bc-ba77f572-bfcf-4606-86d6-51ee221d365e"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-web-light.png"><img
alt="Open in Web" width="114" height="28"
src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a
href="https://cursor.com/background-agent?bcId=bc-ba77f572-bfcf-4606-86d6-51ee221d365e"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img
alt="Open in Cursor" width="131" height="28"
src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a>&nbsp;</div>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Silvan <adlerhurst@users.noreply.github.com>
Co-authored-by: Livio Spring <9405495+livio-a@users.noreply.github.com>
@fforootd fforootd changed the title docs: propose ADR 048 — the Dev Inbox capability docs: propose ADR 050 — the Dev Inbox capability Aug 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants