git:20261013.2a7861b to git:20261020.8d47d7a

69 added, 149 removed. Audit A to A.

# Agent Instructions — `apps/console`
- Scoped instructions for the console SPA. These add to, and where they conflict
- take precedence over, the root [`AGENTS.md`](../../AGENTS.md) for files under
- `apps/console`.
-
- ## Decision source: read the console ADRs first
-
- Before changing console routing, navigation, the app shell, or how the console
- talks to the API, read the console-scoped ADRs in
- [`docs/adrs/`](docs/adrs/README.md) (statuses in that index; 0001/0002 are
- Accepted, 0003/0004 Proposed and largely implemented). They are the agreed
- direction for the console build-out (issue
- [#440](https://github.com/zitadel/nextgen/issues/440)):
-
- - [ADR 0001: Routing](docs/adrs/0001-console-routing.md) — file-based routing,
- one router factory, `basepath` derived from the Vite `base`, route loaders +
- pending/error/not-found boundaries, `staticData`-driven sidebar.
- - [ADR 0002: API access and auth interceptors](docs/adrs/0002-console-api-access.md)
- — the console holds no script-readable credential and calls the API
- same-origin; the embedded browser carries its HttpOnly first-party session
- cookie. Reuse `configureZitadel()` / `getApi()` rather than a bespoke client.
- The base is `/api` **only** under the dev server (whose proxy forwards
- without adding a credential); the embedded build talks to the origin root,
- where the Go binary serves the API. The `/api` shim §1 once deferred to the
- server was withdrawn in the 2026-08-12 revision — do not reintroduce it.
- - [ADR 0003: Console authentication](docs/adrs/0003-console-authentication.md)
- — `/login` embeds the login widget (`@zitadel/sdk-react`); the pathless
- `_authed` layout owns the session guard (`GET /sessions/me`) and the app
- shell; the first-party session cookie is the embedded Console's human
- operator credential, and it authorizes the management calls too (#1300):
- what a person can see and change is what their grants on the selected
- project allow. No project secret is involved in dev or in production.
- - [ADR 0004: Deployment modes](docs/adrs/0004-console-deployment-modes.md)
- — one build and one authorization model serve cloud and self-host. **Target:**
- every deployment uses a reserved platform project for Console identities; an
- explicit testkit or future server-file seed fully provisions it, its initial
- user, membership, separate owner assignment, and optional customer project.
- **Today:** the Console signs into the pinned or first-created project — an
- ordinary customer project — under §2's cutover rule, unless
- `platform.bootstrap_project` is set, which provisions the reserved platform
- project itself (keys, default schema, default login flow — but not the
- initial user/membership/owner assignment the target seed transport adds)
- and is what makes claiming and self-registration work. `zitadel start` is
- the exception to "today": it sets that flag and hands the server an initial
- operator through `--user-file`, so on a CLI-started server with the default
- bootstrap the Console signs into `proj_platform` (setting the flag to
- `false` or pinning another project keeps the fallback). Do not remove that
- fallback (or the `platform.project_id` pin) until the seed transport ships;
- doing so strands self-hosters. Standalone optimizes for one project but does
- not forbid more. The runtime document carries only public sign-in metadata;
- portal surfaces render from target-scoped **effective permissions**, never
- membership, build-time flags, or a parallel console-facing feature array.
- A runtime document the Console cannot read is an **error, not a mode** (§3):
- keep "unreachable/erroring" and "no project yet" separate states, and do not
- reintroduce a silent fallback to `standalone` — backend-less dev and preview
- runs opt in with `VITE_CONSOLE_RUNTIME_FALLBACK` instead.
-
- If an implementation needs to diverge from an ADR, update the ADR in the same
- change rather than letting code and decision drift.
-
- ## Styling: classify before building
-
- Before building any console UI, read
- [`docs/styling.md`](docs/styling.md) — where a component lives decides how it
- is styled (unprefixed shadcn utilities for console UI; login atoms are Lit with
- their own CSS), and the retired `*-zl-*` console utility names must not come
- back. The surface decision and token authority live there; the atom recipe lives
- in [`apps/storybook/AGENTS.md`](../storybook/AGENTS.md).
+ ## Decisions: read the console ADRs first
- ## Screen conventions
+ Before changing routing, navigation, the app shell or API access, read the
+ console ADRs ([index](docs/adrs/README.md)). If the code has to diverge from
+ one, update the ADR in the same change.
- List/detail screens follow the shipped patterns under `src/routes/_authed/`
- (users, schemas, flow-definitions): loader-fetched data, status columns where
- the resource has lifecycle state, `$param` detail routes. A list screen composes
- `src/components/resource-list.tsx` and a detail screen
- `src/components/detail-page.tsx` rather than measuring its own frame — see
- "Resource list layout" and "Resource detail layout" in `docs/styling.md`. There
- is no sessions screen.
+ - [0001 Routing](docs/adrs/0001-console-routing.md): file-based routes, one
+ router factory, `basepath` from the Vite `base`.
+ - [0002 API access](docs/adrs/0002-console-api-access.md): the console holds
+ no script-readable credential. Call the API same-origin through
+ `configureZitadel()` / `getApi()`, never a bespoke client. The base is
+ `/api` only under the dev server; never reintroduce an `/api` shim on the
+ server.
+ - [0003 Authentication](docs/adrs/0003-console-authentication.md): `/login`
+ embeds the login widget, and the pathless `_authed` layout owns the session
+ guard and the shell. The session cookie is the operator's credential for the
+ management calls too: what a person can do is what their grants allow.
+ - [0004 Deployment modes](docs/adrs/0004-console-deployment-modes.md):
+ - Keep the fallback to the pinned or first-created project
+ (`platform.project_id`) until the seed transport ships, or self-hosters
+ are stranded.
+ - Render portal surfaces from effective permissions, never from
+ membership, build-time flags or a console-only feature list.
+ - An unreadable runtime document is an error, not a mode: keep it apart
+ from "no project yet", and never fall back to `standalone` silently.
+ Backend-less runs opt in with `VITE_CONSOLE_RUNTIME_FALLBACK`.
- A screen that acts on one project declares `staticData.scope: "project"` and
- reads the **selected** project — `?project=`, retained by the `_authed` layout —
- through `src/lib/project-scope.ts` (`projectScopeDeps` + `requireProjectScope`
- in loaders, `useRequiredProjectScope` in components). Never scope a management
- call with `getConsoleProjectId()`: that is the project the console signs into
- (ADR 0004 §1), not the one being managed. The flag also hides the screen from
- the sidebar until a project is selected.
+ ## Building UI
- The sidebar is the Projects overview, then the selected project's contents.
- Projects is the one unscoped entry, first, and the only one while nothing is
- selected; the project switcher's `All projects` footer links to it too, and a
- row there opens the project (selects it). The project's own page is the scoped `/project` (`Project
- settings`); `/projects/$projectId` only redirects there with that project
- selected. Screens outside every project (account, billing, deployment-wide
- operators) belong in the switcher or the Settings view, not in this list.
+ Read [`docs/styling.md`](docs/styling.md) before building any console UI: it
+ decides the surface (shadcn with unprefixed utilities here; Lit atoms belong to
+ the login surface) and owns the token rules. The atom recipe is in
+ [`apps/storybook/AGENTS.md`](../storybook/AGENTS.md).
- ## Generated files
+ ## Screens
- `src/routeTree.gen.ts` is generated by the TanStack Router Vite plugin. Do not
- hand-edit it — add or change files under `src/routes/` and let the plugin
- regenerate the tree (consistent with the root `AGENTS.md` "Generated Files"
- rule).
+ - A list screen composes `src/components/resource-list.tsx` and a detail
+ screen `src/components/detail-page.tsx` ("Resource list layout" and
+ "Resource detail layout" in `docs/styling.md`). Follow the shipped screens
+ under `src/routes/_authed/`: loader-fetched data, `$param` detail routes.
+ - A screen that acts on one project declares `staticData.scope: "project"`
+ and reads the selected project (`?project=`) through
+ `src/lib/project-scope.ts`: `projectScopeDeps` + `requireProjectScope` in
+ loaders, `useRequiredProjectScope` in components.
+ - Never scope a management call with `getConsoleProjectId()`: that is the
+ project the console signs into, not the one being managed.
+ - The sidebar is Projects (the one unscoped entry), then the selected
+ project's screens. Screens outside every project (account, billing,
+ operators) go in the project switcher or Settings, not the sidebar.
+ - Never hand-edit `src/routeTree.gen.ts`; the TanStack Router plugin
+ regenerates it from `src/routes/`.
+ - In specs, assert on `router.state.resolvedLocation`, never
+ `router.state.location`: ending on `location` can leave a loader's request
+ in flight, which MSW reports as unhandled in a later test.
- ## Local tasks
+ ## Local loop
```sh
- moon run console:dev-real # seeded real backend + dev server (default loop)
- moon run console:dev-claim # same, but for the claim page (see below)
- moon run console:dev # dev server only on http://localhost:5174
- moon run console:typecheck
- moon run console:test
- moon run console:build
+ moon run console:dev-real # real seeded instance + dev server (the default)
+ moon run console:dev-claim # the same for the claim page
+ moon run console:dev # dev server only, http://localhost:5174
+ moon run console:typecheck console:test console:build
```
- ## Asserting on the route in specs
-
- Read `router.state.resolvedLocation`, never `router.state.location`. `location`
- changes when a navigation starts; `resolvedLocation` changes once its loaders
- have finished. A spec that ends on `location` can leave a loader's request in
- flight, which MSW then reports as unhandled in a later test.
-
- ## The claim page needs `dev-claim`, not `dev-real`
-
- `dev-real` boots one ordinary project and signs the console into it. The claim
- page is the console acting as the *platform's* claim surface, and
- `claim/complete` only accepts a session belonging to the platform project — so
- on `dev-real` the page renders but the claim always fails with "This account
- can't claim the project". That is the harness, not a defect.
-
- `moon run console:dev-claim` boots the platform project, pins the console to it,
- and prints a ready claim link. The trade-off is why it is opt-in: pinning the
- console to `proj_platform` changes the standalone semantics the demo and
- embedded suites rely on, and the seeded users live in the project being claimed
- rather than the platform one, so list screens read empty and you register on the
- claim page instead of signing in with the seeded credentials.
-
- Three things that break either loop before it starts, none of which say so
- clearly:
-
- - **Build the CLI first** (`moon run cli:build`). Both loops shell out to it to
- start the server; without a built bundle the server starts unmigrated and dies
- with `no such table: projects`.
- - **Reinstall after a rebase** (`pnpm install`). A stale `node_modules` fails the
- built CLI on a missing transitive dependency, not on anything you changed.
- - **A raw binary needs `--migrate`.** Migrations are opt-in since #1152; the CLI
- passes the flag for you, so this only bites when launching `dist/server/nextgen`
- by hand.
-
- `CONSOLE_DEV_ORIGIN` sets the console's port for both loops, so a second worktree
- can run beside the first.
-
- ## Develop against real data, not the mock
-
- The console manages an instance, so **use `console:dev-real`** — it boots a real
- ephemeral instance and seeds users, so list screens show real API responses.
- `@zitadel/api-mock` has no user store; a users list read from it is a fiction.
-
- Why the mock cannot substitute (it has no user store, and nothing about
- authorization can be proven against it) is documented canonically in the Local
- development section of [`README.md`](README.md) — read it for all three backends
- and when each applies. The mock's flow-shape authority rule lives in
- [`packages/api-mock/AGENTS.md`](../../packages/api-mock/AGENTS.md).
+ - Develop against real data: `@zitadel/api-mock` has no user store, so a list
+ read from it is fiction. When each backend applies is in the Local
+ development section of [`README.md`](README.md).
+ - The claim page needs `dev-claim`: `claim/complete` accepts only a
+ platform-project session, so on `dev-real` every claim fails with "This
+ account can't claim the project". `dev-claim` pins the console to
+ `proj_platform`, so list screens read empty and you register on the claim
+ page instead of using the seeded users.
+ - Both tasks build the CLI they boot the server through. A direct
+ `pnpm run dev:real` does not: run `moon run cli:build` first, or the server
+ starts unmigrated and dies with `no such table: projects`.
+ - Run `pnpm install` after a rebase: a stale `node_modules` fails the built CLI
+ on a missing transitive dependency.
+ - A server binary launched by hand (`dist/server/nextgen`) needs `--migrate`;
+ the CLI passes it for you.
+ - Set `CONSOLE_DEV_ORIGIN` to run a second loop beside the first.