Skip to content

docs: propose scaffold posture defaults and copy-as-branding-revision ADRs - #714

Merged
fforootd merged 11 commits into
mainfrom
docs/scaffold-posture-and-copy-adrs
Aug 2, 2026
Merged

docs: propose scaffold posture defaults and copy-as-branding-revision ADRs#714
fforootd merged 11 commits into
mainfrom
docs/scaffold-posture-and-copy-adrs

Conversation

@fforootd

@fforootd fforootd commented Aug 2, 2026

Copy link
Copy Markdown
Member

Summary

The two deliberately-parked P2 items from the Wicklore remediation plan, written as Proposed ADRs for team alignment — per the plan's rule that these get no implementation before acceptance (built-ahead-of-alignment is how PR #56/#159 rotted):

  • ADR 044 — Scaffold Embedding Posture Defaults: scaffolded auth/profile pages derive their surface from the fresh-vs-pre-existing hinge that setup already records (scaffolded_framework, ADR 042): fresh scaffolds keep variant="page", pre-existing apps get variant="widget" in a layout-neutral wrapper so the card stops fighting the host app's shell. Posture stays a page edit (no config knob), doctor restores it from the manifest. Open questions flagged: an explicit --surface override, and per-page posture recording.
  • ADR 045 — Copy Overlays as Branding Revisions: audience copy (businessLocales) is branding-shaped data living in code — per-app-build, multiplied across eight SDK/template integration points. Proposal: branding revisions carry per-language copy resolved with the flow response; --use-case business seeds a revision instead of wiring template props; wording edits follow eject→edit→apply and become runtime-effective. Fenced behind the templates-track milestone — direction alignment only; the in-flight per-SDK parity wiring stays the interim mechanism and is explicitly named as transitional.

Validation

  • node scripts/check-adrs-index.mjs — 45 records ok.
  • Docs-only; no changeset.

Notes

Stacked on #713 (the index check enforces contiguous ADR numbers, so this branch carries 043) — the diff collapses to the two proposals once #713 merges.

ADR 043: the supported floors are Next.js 15+ and React 18+. The gate lives
in the framework detectors, so setup refuses a below-floor app before any
mutation and doctor's framework check fails on one — both with an explicit
E_UNSUPPORTED_PROJECT_SHAPE naming the floor and an upgrade hint, never a
silent narrowing. Unparseable version specs pass (no provable violation).
@zitadel/sdk-next's peer range follows the floor (next >=15).
… ADRs

ADR 044 (Proposed): scaffolded pages derive their embedding surface from the
recorded fresh-vs-pre-existing hinge — fresh keeps variant="page",
pre-existing apps get variant="widget" in a layout-neutral wrapper.
ADR 045 (Proposed): audience copy overlays move from bundle-shipped presets
wired per SDK into branding revisions resolved with the flow response;
implementation stays fenced behind the templates-track milestone.
Copilot AI review requested due to automatic review settings August 2, 2026 16:10
@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 2, 2026 5:18pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
nextgen-docs Skipped Skipped Aug 2, 2026 5:18pm
nextgen-mock-zitadel Skipped Skipped Aug 2, 2026 5:18pm

Request Review

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

⚠️ No Changeset found

Latest commit: 536b82f

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

This PR adds three new ADRs (043–045) and updates the ADR index, while also including (currently, in the shown diff) the framework version-floor enforcement work for the CLI detectors and the corresponding release/test updates.

Changes:

  • Add ADR 044 (scaffold embedding posture defaults) and ADR 045 (copy overlays as branding revisions), plus ADR 043 (framework version floors) and register them in the ADR index.
  • Enforce Next.js 15+ and React 18+ floors in CLI framework detectors, with updated unit/integration coverage and CLI contract docs.
  • Update @zitadel/sdk-next peer dependency floor for Next.js and add a changeset describing the shipped behavior.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/sdk-next/package.json Raises next peer dependency floor to >=15.
docs/adrs/README.md Adds ADR index entries for 043–045.
docs/adrs/043-framework-version-floors.md New accepted ADR documenting Next/React floors and enforcement approach.
docs/adrs/044-scaffold-embedding-posture-defaults.md New proposed ADR for page vs widget posture defaults based on scaffolded vs pre-existing app.
docs/adrs/045-copy-overlays-as-branding-revisions.md New proposed ADR to move business copy overlays into branding revisions (templates-track fenced).
apps/cli/tests/unit/lib/orca/detectors/react.test.ts Adds React detector floor/versionMajor coverage.
apps/cli/tests/unit/lib/orca/detectors/next.test.ts Updates Next detector expectations and adds floor/unparseable tests.
apps/cli/tests/unit/commands/doctor/checks.test.ts Adds doctor framework-check coverage for downgraded Next below floor.
apps/cli/tests/integration/setup-next.test.ts Adds setup refusal integration test for Next below floor and parameterizes Next version.
apps/cli/src/lib/orca/detectors/react.ts Adds React 18+ floor enforcement and optional versionMajor capture.
apps/cli/src/lib/orca/detectors/next.ts Adds Next 15+ floor enforcement and preserves optional versionMajor capture.
apps/cli/SKILLS.md Documents supported floors and the shared E_UNSUPPORTED_PROJECT_SHAPE behavior.
.changeset/framework-version-floors.md Adds release intent for CLI + sdk-next floor enforcement and peer range update.

Comment thread apps/cli/src/lib/orca/detectors/next.ts Outdated
Comment thread apps/cli/src/lib/orca/detectors/react.ts Outdated
Comment thread .changeset/framework-version-floors.md Outdated
Comment thread docs/adrs/README.md Outdated
…ugh doctor

The floor gate now judges the declared spec with real range semantics
(semver.validRange + intersects against >=floor.0.0-0): "<15" rejects,
"file:../next-14-patched" and dist-tags pass — a first-digits parse
inverted both. Doctor stops laundering the typed error: CheckOutcome
carries the ZitadelError code and hint, the envelope prefers a coded
failure over generic E_VALIDATION, and the advice ladder surfaces the
check's own remedy instead of recommending a --fix that cannot repair an
unsupported version. Changeset loses the internal ADR reference; the Next
floor comment loses the unrelated React 19 clause.
…he release boundary

Review round: ADR 044 now applies only to integrations that add routes
without owning the app shell (Next, Nuxt) — the SPA patchers write the app
root, so a pre-existing shell is conflicted-with or replaced, never
inherited; non-destructive insertion moves to open questions. The posture
is recorded in the scaffold manifest, and absence of a record (all legacy
scaffolds) restores page, so widget restoration needs positive evidence.
ADR 045 no longer implies revision creation changes runtime wording: under
ADR 035 a copy-bearing revision is inert until a configuration release
containing it is deployed, with ADR 040's latest-revision resolution named
as the acknowledged interim.
@fforootd

fforootd commented Aug 2, 2026

Copy link
Copy Markdown
Member Author

Codex round (relayed) addressed on 48d47b0 — all three findings amended into the proposals.

1. [P1] ADR 044 scoped to route-based integrations. Correct — the SPA patchers write the app root, so a pre-existing shell is conflicted-with or --force-replaced, never inherited; "widget inherits your layout" was only ever coherent where the integration adds routes. The Decision now applies to Next and Nuxt only, states the SPA-family reason explicitly, and a non-destructive route/layout insertion contract for the SPA routers moves to open questions as the prerequisite for lifting the restriction.

2. [P1] ADR 045 aligned with ADR 035's release boundary. The Decision now states that effectiveness follows the release boundary, not revision creation: under ADR 035 a copy-bearing branding revision is an inert draft until a configuration release containing it is deployed to the environment, and the flow response serves copy from the environment's current release — never undeployed drafts. ADR 040's latest-revision resolution is named as the acknowledged interim, with copy edits reaching runtime through whichever lifecycle is in force. The "runtime-changeable" consequence is rephrased to "no app rebuild or redeploy — wording ships as configuration changes with release/deployment semantics", which also gains the per-environment wording capability the bundle approach cannot express.

3. [P2] Legacy posture ambiguity resolved by recording + a positive-evidence rule. The manifest gains an explicit posture record; doctor --fix restores from it, and absence of a record — every pre-044 manifest and every manifest-less legacy scaffold — restores page, which is what all earlier scaffolds were. Widget restoration only ever happens on positive evidence, so the undefined-vs-pre-existing ambiguity in loadPatchContext cannot misrestore a legacy page as a widget.

The four Copilot threads are replied-to and resolved: the two version-spec cases and the changeset wording were fixed on #713's bb22a1c and carried here by the stack-forward merge; the docs-only/diff mismatch is the deliberate stacking (ADR index contiguity) documented in the PR notes, collapsing on #713's merge. Branch is reconciled with main (the BEHIND flag).

🤖 Addressed by Claude Code

…-and-copy-adrs

# Conflicts:
#	docs/adrs/README.md
@vercel
vercel Bot temporarily deployed to Preview – nextgen-mock-zitadel August 2, 2026 17:17 Inactive
@vercel
vercel Bot temporarily deployed to Preview – nextgen-docs August 2, 2026 17:17 Inactive
@fforootd
fforootd enabled auto-merge (squash) August 2, 2026 17:20
@fforootd
fforootd requested a review from peintnermax August 2, 2026 17:21
@fforootd fforootd self-assigned this Aug 2, 2026
@fforootd
fforootd merged commit 51d3804 into main Aug 2, 2026
14 checks passed
@fforootd
fforootd deleted the docs/scaffold-posture-and-copy-adrs branch August 2, 2026 17:27
@github-project-automation github-project-automation Bot moved this from Inbox to Done in Engineering Kanban Aug 2, 2026
fforootd added a commit that referenced this pull request Aug 2, 2026
…#717)

## Summary

The 2026-08-02 Wicklore rerun (finding N3) showed that after embedding
the scaffolded login/session widgets in a real app, the app's own chrome
has no supported way to know session state — the header keeps rendering
"Sign in"/"Join" beside a live session. The widgets read `GET
/sessions/me` internally, but nothing documented how the host page gets
the same answer.

Root cause is structural on Next: the scaffolded request boundary's
`matcher` covers only `/__nextgen/:path*` and `/profile/:path*`, so on
any other page the middleware never runs, no auth header is tunnelled,
and server-side `auth()` reports signed-out. Nuxt doesn't share the gap
(its scaffolded auth plugin seeds `useAuth()` on every render) — its gap
was purely documentation.

This PR ships the smallest honest surface:

- **`@zitadel/sdk-next/session`** — new dependency-light entry exporting
`getSession()`: a client-side fetch of the same-origin
`{proxyPath}/sessions/me`, the exact read `<zitadel-session>` performs.
Works on any page (only the proxy path must be matched, which the
scaffold always does), needs no `configureZitadel()`, and returns the
client-safe `ClientAuthResult` — no token. `401`/`404` and anonymous
sessions map to signed-out; other failures throw rather than silently
rendering signed-out; calling it server-side throws with a pointer to
`auth()`. It's a dedicated subpath because the root barrel pulls
`next/headers` and `./client` pulls Lit — both unsafe to import from an
SSR'd header component.
- **`ClientSession` / `ClientAuthState` / `ClientAuthResult`** lifted
from sdk-nuxt into `@zitadel/sdk-core` as the single source; sdk-nuxt
re-exports unchanged, so `useAuth()` (Nuxt) and `getSession()` (Next)
return the identical shape.
- **Scaffold guidance**: one framework-branched paragraph in the
generated `AGENTS.md` managed section — Next names `getSession()` (plus
`auth()` with its matcher precondition), Nuxt names `useAuth()`, SPA
frameworks get the raw `/__nextgen/sessions/me` read (no claimed helper
that doesn't exist there). Matching one-line comments emitted in the
Next and Nuxt profile-page templates, plus a README section with a
conditional-header example.

Deliberately **not** in scope: no state-management layer, no new widget
events (`zitadel-signout` / `zitadel-flow-complete` already exist, and
the scaffolded posture navigates on both transitions, so read-on-load
suffices), no `NextgenProvider`-based pattern (see Notes).

## Validation

- `sdk-next:test` 103/103 (8 new `getSession()` tests: identity mapping,
null-attribute handling, anonymous session, 401/404 vs throw semantics,
proxy-path resolution order, server-side guard)
- `cli:test` 942/942 (new guidance test pinning the per-framework
branching, incl. that non-Next frameworks never reference
`@zitadel/sdk-next/session`)
- `sdk-nuxt:test`, plus build / typecheck / lint green across sdk-core,
sdk-next, sdk-nuxt, cli; `demo-nuxt:typecheck` green (consumes the
re-exported types through the built dist)
- Built `dist/session.js` smoke-tested by direct import (function
resolves; server-side guard fires)
- `changeset status` verified
- Known-good caveat: `demo-next:typecheck` fails in this fresh worktree,
stash-verified identical **without** these changes (pre-existing
JSX-augmentation issue)

## Release notes / changeset

`.changeset/session-state-for-app-chrome.md` — `@zitadel/sdk-next` minor
(new `./session` entry), `@zitadel/sdk-core` minor (new exported
client-safe types), `@zitadel/cli` minor (guidance + emitted comments),
`@zitadel/sdk-nuxt` patch (type re-export, no behavior change).

## Notes

- ADR 044/045 (#714) checked before building: 044 (page/widget posture)
is adjacent but doesn't cover session-state consumption — the new
guidance paragraph and the profile-page comment are posture-neutral and
survive 044's template branching; 045 is unrelated.
- Found in passing, deliberately excluded, and being hardened in a
separate session/PR: `auth()` on matcher-uncovered routes trusts a
client-forgeable unverified `x-nextgen-auth-token` header, and the
README's `NextgenProvider` pattern serializes the raw session token into
the RSC payload (sdk-nuxt strips it for exactly this reason). Both are
behavior changes to existing surface with a design decision in the
`auth()` fix; this PR only narrows documented `auth()` usage to the safe
(matcher-covered) region. The follow-up builds on the `ClientAuthResult`
types this PR moves to sdk-core.
fforootd added a commit that referenced this pull request Aug 7, 2026
## 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.
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.

2 participants