Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions docs/adrs/044-scaffold-embedding-posture-defaults.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# ADR 044: Scaffold Embedding Posture Defaults

> **Status:** Proposed
> **Date:** 2026-08-02
> **Context:** The `zitadel` CLI's scaffolded auth pages and the `<zitadel-login>`/`<zitadel-session>` `variant` surface contract.
> **Relates to:** [ADR 042](042-scaffolded-file-ownership-and-drift-detection.md)

## Context

The widgets carry a two-value surface contract: `variant="page"` paints the
full-page chrome (viewport height, surface background from design tokens),
`variant="widget"` renders an embeddable card that inherits the host page's
layout. Since the session card went widget-first, every scaffolded page pins
`variant="page"` explicitly, and the generated pages name the widget
alternative in a comment.

That single default fits only half the audience. `setup` already
distinguishes the two cases and records the distinction (`scaffolded_framework`
in the scaffold manifest, ADR 042):

- **Fresh scaffold** — the CLI created the app skeleton. There is no design to
respect; a full-page auth surface is the strongest start, and the homepage
already redirects to `/login` on the same reasoning.
- **Pre-existing app** — the app has its own shell, theme, and navigation. A
generated page that takes over the viewport with token-colored chrome
fights the host design; the agent-evaluation friction log hit exactly this
(the demo shop rebuilt the generated pages around the widget surface by
hand).

## Decision (proposed)

**Scope: route-based integrations only** — frameworks whose patchers add
route files without owning the app shell (today: Next, Nuxt). The SPA
families (React/Vue/Solid/Svelte/Qwik/Angular) are explicitly out of scope
for the widget posture: their patchers write the app's root component, so on
a pre-existing app there is no preserved shell for a widget to inherit —
setup either conflicts with the existing root or, under `--force`, replaces
it. They keep today's page posture until a non-destructive route/layout
insertion contract exists for their routers (open question below).

Within scope, scaffolded auth and profile pages derive their default posture
from the same hinge the homepage uses:

- Fresh scaffold (`scaffolded_framework: true`) → pages pin
`variant="page"` — unchanged from today.
- Pre-existing app → pages pin `variant="widget"` inside a minimal,
layout-neutral wrapper (no forced color scheme, no viewport styling), so
the card drops into the host app's own layout and theme.

**The chosen posture is recorded in the scaffold manifest** (a
`posture: "page" | "widget"` field beside `scaffolded_framework`), and
`doctor --fix` restores from that record. Absence of a posture record —
every manifest written before this decision, and every manifest-less legacy
scaffold — restores `page`, which is what all earlier scaffolds were; the
widget posture is only ever restored on positive evidence. In both postures
the emitted comment names the other variant, and editing the generated page
remains the sanctioned way to change posture (presentation is user-owned per
ADR 042 — no config knob is introduced).

## Consequences

- Templates branch on `PatchContext.scaffoldedFramework`; the manifest
gains the posture record, and restoration reads it rather than
re-deriving the hinge (which a manifest-less legacy scaffold could not
answer).
- The journey matrix needs a pre-existing-app lane (Next, Nuxt) to cover
the widget posture end to end (today's journeys always scaffold fresh).
- Copy in the generated pages' comments and the scaffold guidance must
describe both postures rather than assuming full-page.

## Open questions

- A non-destructive route/layout insertion contract for the SPA routers
(React Router, Vue Router, Angular routes, …) that would make the widget
posture reachable there without touching the app root — a prerequisite
for lifting the scope restriction.
- Whether `--preset`-style explicitness is wanted anyway (e.g. a
`--surface page|widget` override at setup time) or whether editing the
generated page stays the only override.
- Whether the posture should be recorded per page rather than per scaffold,
so a later template revision can tell a deliberate user choice from the
scaffold default.
75 changes: 75 additions & 0 deletions docs/adrs/045-copy-overlays-as-branding-revisions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# ADR 045: Copy Overlays as Branding Revisions

> **Status:** Proposed
> **Date:** 2026-08-02
> **Context:** The widgets' locale dictionaries and copy overlays (`businessLocales`), the branding-revision resource (ADR 040 / the templates track), and the CLI scaffold templates.
> **Relates to:** [ADR 040](040-tenant-login-templates-editable-config.md)

## Context

The widgets ship neutral built-in copy, and audience-flavored wording is an
overlay: `businessLocales` lives in `@zitadel/components`, is re-exported
through the framework SDKs, and is wired into generated pages at scaffold
time when the project chose the business use case. This works, but the
mechanism has a shape problem:

- The overlay is **branding-shaped data living in code**. Changing wording
means a package release and an app redeploy, while the platform already
has a home for exactly this kind of data — immutable, per-project branding
revisions resolved at flow time (ADR 040).
- Every framework SDK must re-export the overlay and every scaffold template
must wire it, multiplying one dictionary into eight integration points
(the cross-framework parity work now in flight does precisely this
multiplication).
- The overlay applies per **app build**, not per project or environment —
two apps on one project can disagree about the project's own voice.

## Decision (proposed)

Copy overlays become part of the **branding revision** resource:

- A branding revision carries optional per-language copy entries
(key → string over the built-in dictionary keys, same shape as today's
`locales` property values). The flow response delivers the merged copy of
the revision that is **effective for the environment**, and the widgets
apply it exactly as they apply a `locales` property today (element-level
`locales` remains as the app-level override with higher precedence).
- **Effectiveness follows the release boundary, not revision creation.**
Under the accepted release/deployment model (ADR 035), a copy-bearing
branding revision is an inert draft until a configuration release
containing it is deployed to the environment — the flow response serves
copy from the environment's current release, never from undeployed
drafts. The latest-revision-on-flow-response resolution from ADR 040 is
the acknowledged interim until that model lands; this ADR inherits the
boundary rather than bypassing it, and copy edits reach runtime through
whichever lifecycle is in force (today's eject → edit → apply; release
construction + deployment once ADR 035 is implemented).
- `setup --use-case business` seeds a branding revision carrying the
business overlay instead of wiring template props — included in the
initial configuration the same way setup's other seeded resources become
active — so the generated pages stay copy-agnostic and every framework
gets the overlay through the same server path with zero per-SDK wiring.

The bundle keeps only the neutral built-ins; `businessLocales` remains
exported as a convenience preset for hand-integrators, but the scaffold and
platform path no longer depend on it.

## Consequences

- One source of truth for copy across all eight framework scaffolds; the
per-SDK re-exports and template wiring become a transitional mechanism to
retire once this lands.
- Copy joins branding's governance story: wording changes ship as
configuration changes — no app rebuild or redeploy — and, under ADR 035,
carry release/deployment semantics, so two environments can run different
wording by running different releases, which a build-time constant cannot
express.
- The flow response grows a copy payload; the widgets' locale resolution
gains one precedence layer (element property > revision copy > built-ins).

## Fence

Implementation is deliberately fenced behind the templates-track milestone:
this ADR seeks direction alignment only, and no code should be built ahead
of acceptance (the built-ahead-of-alignment pattern is how parallel work has
rotted before). The interim per-SDK wiring keeps delivering value until then.
2 changes: 2 additions & 0 deletions docs/adrs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,5 @@ This directory contains architecture decision records (ADRs) for nextgen.
| [041](041-storage-statement-contract-tests.md) | Storage Statement Contract Tests | Accepted | Shared `stmttest` suites assert `AllStatements` behavior across dialects via build-tag registration and `forEachDialect`; dialect packages keep engine-specific tests. |
| [042](042-scaffolded-file-ownership-and-drift-detection.md) | Scaffolded File Ownership and Drift Detection | Accepted | Scaffolded app files carry infrastructure/presentation classes recorded in a `.zitadel/state.json` manifest; `doctor` verifies them (missing infra fails, missing pages warn) and `--fix` restores missing files only, never overwriting edited or user-adopted ones. |
| [043](043-framework-version-floors.md) | Framework Version Floors | Accepted | Supported floors are Next.js 15+ and React 18+, enforced in the framework detectors so `setup` and `doctor` share one loud `E_UNSUPPORTED_PROJECT_SHAPE` gate; unparseable versions pass, and `@zitadel/sdk-next` peers follow the floor (`next >=15`). |
| [044](044-scaffold-embedding-posture-defaults.md) | Scaffold Embedding Posture Defaults | Proposed | Route-based scaffolds (Next, Nuxt) derive their surface from the fresh-vs-pre-existing hinge — fresh keeps `variant="page"`, pre-existing gets `variant="widget"` in a layout-neutral wrapper — with the posture recorded in the scaffold manifest; no record (legacy) restores `page`. SPA families stay page-postured until a non-destructive route-insertion contract exists. |
| [045](045-copy-overlays-as-branding-revisions.md) | Copy Overlays as Branding Revisions | Proposed | Audience copy overlays move from bundle-shipped presets wired per SDK into the branding-revision resource: the flow response serves the copy effective for the environment, with effectiveness following ADR 035's release/deployment boundary (latest-revision resolution only as the ADR 040 interim); `--use-case business` seeds a revision instead of template props. Implementation fenced behind the templates-track milestone. |
Loading