AGENTS.md · diff
git:20261014.c1def64 to git:20261017.9e45fa5
1 added, 0 removed. Audit A to A.
# Agent Instructions
These instructions apply to the whole repository. A nearer `AGENTS.md` may add
more specific rules for a subtree.
## Instruction Scope And Precedence
Use `AGENTS.md` as the primary and tool-agnostic instruction source.
Always resolve instructions in this order:
1. Root `AGENTS.md` (this file)
2. The nearest scoped `AGENTS.md` for the files you read or change
If instructions conflict, the nearest scoped `AGENTS.md` for the touched path
takes precedence over broader scope files.
## ADR Context
Before changing behavior, review relevant ADRs in `docs/adrs/` and align
proposals and implementations with recorded decisions.
- `docs/adrs/004-agent-contract-and-agents-md.md` defines how external agents
should consume the Zitadel CLI contract; treat it as product-surface context,
not as the primary implementation contract for developing this repository.
- `docs/adrs/` contains product and architectural decisions that should be
treated as implementation context, not optional references.
## Project Shape
This repo is the pre-release next generation of Zitadel. Moon owns the
monorepo task graph, release artifact builds, and the draft GitHub Release
shell; Changesets owns package versions, changelogs, npm publishing, and
public package tags (see "Release, Licensing, And Secrets"). This section
maps roles, not an exhaustive inventory — the authoritative project list is
`moon projects`, and workspace globs live in `pnpm-workspace.yaml`.
Go server:
- `main.go` + `cmd/` — Go entrypoint and command wiring.
- `internal/` — Go server implementation. `internal/api/` owns the HTTP
handlers, `internal/service/` the service layer, `internal/domain/` the
domain model and error sentinels, `internal/storage/` the multi-dialect
storage layer (see [internal/storage/AGENTS.md](internal/storage/AGENTS.md)),
`internal/authz/` the authorization subsystem (OpenFGA parser, profile
validator, compiler — ADRs 032–034), and `internal/staticui/` serves the
embedded UI builds.
- `api/openapi/` — OpenAPI 3.1 source files; wire rules in
[api/openapi/AGENTS.md](api/openapi/AGENTS.md). `api/generated/` is ogen
output; edit the OpenAPI source instead.
Product surfaces (`apps/`):
- `apps/cli/` — the `zitadel` npm CLI. `apps/cli/skills/zitadel-cli/SKILL.md` is the canonical
consumer-agent contract.
- `apps/server/` + `apps/server-*/` — the `@zitadel/server` npm binary
wrapper and its per-platform binary packages (one per OS/arch).
- `apps/console/` and `apps/login-ui/` — the two SPAs embedded into the Go
server at `/ui/console/` and `/ui/login/`.
- `apps/docs/` — the documentation site: Fumadocs on Next.js, served under
`/docs`, in the hosted cloud too.
- `apps/website/` — the hosted cloud's coming-soon page at `/`: Next.js in
the new zitadel.com's look, the seed of the public website.
Development and test surfaces (`apps/`):
- `apps/storybook/` — the component workbench for the login surface
(`@zitadel/components`), served by the hosted cloud at `/storybook/`.
- `apps/demo-next/` and `apps/demo-nuxt/` — reference integrations of the
embedded sign-in component on each framework.
- `e2e/` — the single root-level end-to-end project (a system test of the
assembled product). Subfolders `console/`, `demo-next/`, `demo-nuxt/` and
`journey/` hold the lanes; the journey covers the fresh consumer path (CLI
setup plus real registration/login, installing local package tarballs through
a temporary registry). CI runs the whole suite through `e2e:all`; `:test`
stays unit tests.
- `apps/mock-zitadel/` — thin deployment wrapper that serves
`packages/api-mock` as a live per-PR preview endpoint.
- `apps/cloud/` — deployment wrapper of the hosted cloud: `main` on Vercel
with PlanetScale Postgres, a preview with a database of its own per branch.
The repo-root `vercel.json` defines its services; operations live in
[docs/runbooks/cloud.md](docs/runbooks/cloud.md).
Published libraries (`packages/`):
- `packages/sdk-*` — public TypeScript SDKs: `sdk-core` plus one package per
supported framework (currently next, nuxt, react, vue, angular, qwik,
solid, svelte).
- `packages/components/` — the Lit atoms (`<zl-*>`) and the `<zitadel-login>`
orchestrator that make up the login surface; each atom's CSS sits beside it.
- `packages/design-tokens/` — the Figma-driven `--zl-*` variables, the one
contract the login surface and the console share (ADR 055).
- `packages/api/` — generated TypeScript API client.
- `packages/config/` — versioned local config schemas and defaults.
- `packages/api-mock/` — in-process MSW handlers and the standalone mock
auth server used by demos and e2e tests (defaults to port 8080; set
`PORT` to override).
Repo tooling:
- `tools/release/` — the Moon `release` project (snapshot, artifacts, draft
GitHub Release shell).
- `tools/bench/` — the k6 benchmark harness, a nested Go module (ADR 067):
`moon run bench:sweep` measures a server that is already running (locally
`moon run workspace:server`). AGPL-only and excluded from commercial
licensing because it links k6.
- `scripts/` — the Node `.mjs` orchestration behind the `workspace:*` tasks
(doctor, check, journey, server, cli, release helpers).
- `docs/` — design notes, runbooks, and ADRs that explain product intent.
## Workflow Front Doors
### I am contributing to Zitadel
| I want to... | Run |
| ---------------------------------- | ---------------------------------------- |
| Check my setup | `moon run workspace:doctor` |
| Try the local Zitadel CLI | `moon run workspace:cli -- --help` |
| Preview the docs site | `moon run docs:dev` |
| Run the server from source | `moon run workspace:server -- --help` |
| Test the fresh-app onboarding path | `moon run e2e:journey` |
| Run normal local checks | `moon ci :lint :typecheck :build :test` |
| Mirror CI locally | `moon run workspace:check -- --full` |
| Rerun one failed task | `moon run <project>:<task>` |
+ | Run the hosted cloud locally | `moon run workspace:cloud` |
| Check a deployment of the cloud | `CLOUD_URL=<url> pnpm check:cloud` |
### I am adding Zitadel to my app
The consumer command table lives in
[README.md](README.md#i-am-adding-zitadel-to-my-app); the canonical
consumer-agent contract is [apps/cli/skills/zitadel-cli/SKILL.md](apps/cli/skills/zitadel-cli/SKILL.md).
### Contributor commands vs product commands
`moon run workspace:server` is a repository contributor command that runs the Go
server from source. `zitadel start` is a published product CLI command that
runs the released `@zitadel/server` npm binary for app developers and agents;
it must not rely on Docker, Go, Moon, or this source checkout. Docker remains
available through `zitadel start --runtime docker`.
`moon run workspace:cli -- start` and `moon run workspace:cli -- setup ...` are
the contributor exceptions: the root wrapper builds and packs the workspace
packages into a local Verdaccio registry so the CLI and generated apps run
against local `@zitadel/*` tarballs instead of public npm (details in
[CONTRIBUTING.md](CONTRIBUTING.md#testing-the-cli-from-source)). Set
`ZITADEL_CLI_USE_PUBLIC_PACKAGES=1` only when intentionally testing published
packages; pass `--runtime docker`, `--image`, or `ZITADEL_LOCAL_IMAGE` when
intentionally testing the Docker backend. Invoking `apps/cli/bin/run.js`
directly runs the last-built `dist/` — in a fresh checkout or after source
changes, run `pnpm install` and `moon run cli:build` first (the wrapper above
does this for you).
## Local Checks
Use Node.js from `.nvmrc` and the pinned pnpm version from `package.json`.
```sh
moon run workspace:doctor
moon ci :lint :typecheck :build :test
```
The root doctor treats Moon and the local toolchain as required. Docker is
needed for container builds, Docker fallback journeys, and container-backed
integration tests; it is not required for the default npm-binary local runtime.
Playwright browsers remain warning-only because CI provisions them where they
are needed: the components/api-mock/storybook browser test lanes (now part of
the default `moon ci … :test` gate) pull Chromium in through an
`install-browsers` task dependency, and the standalone e2e and journey suites
get it from the ci-mode-gated Chromium install step in the CI workflow.
Use `moon run workspace:check -- --full` for slower CI-parity phases and
`moon run <project>:<task>` to rerun one named task.
Use `moon run workspace:check -- --only release` after touching Changesets,
Moon release workflows, or package versioning behavior.
Prefer Moon project tasks for narrow package work, for example
`moon run cli:test`.
Moon manages TypeScript workspace targets, Go checks, and release build tasks.
Long-running customer-style local orchestration still runs through repository
scripts so server processes are signaled and cleaned up directly.
`moon run workspace:server` builds the embedded console/login UI before non-help
startup, then runs `go run .`. Direct `go run .` callers must build the embedded
UI surfaces themselves or disable both embedded UI surfaces.
Checked-in end-to-end tests run in CI as one moon target: the root `e2e`
project's `all` task depends on every lane (console
smoke/real/embedded/platform, demo mock/real, the CLI journey matrix, and the
kit's real-instance suite), and ci.yml runs `e2e:all` in a step of its own
after the build-and-test graph, whenever moon finds it affected. A plain
`:test` (and `pnpm run check`) stays unit tests; `moon run e2e:all` runs the
whole suite locally, which boots real servers and needs browsers installed; the
journey lane also needs prebuilt tarballs (`moon run release:pack`, then
`JOURNEY_TARBALLS_DIR`). The canonical details are in
[packages/testing/AGENTS.md](packages/testing/AGENTS.md). (More in
[CONTRIBUTING.md](CONTRIBUTING.md#running-integration-and-end-to-end-tests)):
```sh
corepack pnpm --filter @zitadel/e2e exec playwright install
moon run e2e:demo-next-mock
moon run e2e:demo-nuxt-mock
```
The local reproduction command for the fresh-app consumer journey gate is:
```sh
moon run e2e:journey
```
It proves the fresh-app path end to end and does not require Docker by
default; use `-- --framework next` to run one framework and
`-- --runtime docker --image <docker-tag>` to exercise the Docker fallback
(details in [CONTRIBUTING.md](CONTRIBUTING.md#fresh-app-journey)). Use it for
deterministic CI-style proof of the fresh-app path, and
`moon run workspace:cli -- ...` for manual browser or agent experiments
against the same local package train.
In CI the branch-protection check is the GitHub Actions context `full-pr`,
shown in the pull request UI as `ci / full-pr`; it consumes the workflow's
packed npm tarballs instead of public Zitadel packages. Changesets PR comments
are informational release-intent feedback and are not branch-protection
requirements. Pull requests land through the merge queue, which runs
`full-pr` again on the latest `main`, so merge `main` into a branch only to
resolve a conflict or pick up a change it needs, never just to keep it
current ([CONTRIBUTING.md](CONTRIBUTING.md#merging)). Mirror the gate
locally with `moon run workspace:check -- --full`; the full step list is in
[CONTRIBUTING.md](CONTRIBUTING.md#what-ci-runs).
### Interactive demo verification
To test the sign-in flow interactively against the mock backend (two
terminals):
1. Start the mock auth server: `moon run api-mock:start` (port 8080)
2. Start demo-next: `moon run demo-next:dev` (port 3002), or demo-nuxt:
`moon run demo-nuxt:dev` (port 3001)
The demos default to `ZITADEL_URL=http://localhost:8080`, matching the mock's
default port. If port 8080 is taken (for example by the Go server), start the
mock with `PORT=4000 moon run api-mock:start` and export
`ZITADEL_URL=http://localhost:4000` for the demo. See
`apps/demo-next/README.md` and `apps/demo-nuxt/README.md` for environment
details.
## Testing Layers
Pick the lowest layer that can prove the property and **do not duplicate
upward**. When deciding where a new test belongs:
1. **Unit (Vitest, `jsdom`)** — markup, props, ARIA, slot projection,
event-contract shape, pure logic. Fastest; covers the bulk of behaviour.
2. **Browser (Vitest, real Chromium via `@vitest/browser-playwright`)** —
form-association, focus delegation, Enter-to-submit, anything that
needs a real `ElementInternals` or `HTMLFormElement`. Lives in
`*.browser.spec.ts`.
3. **End-to-end (Playwright)** — full HTTP path through framework
middleware, the `/__nextgen` proxy, real `Set-Cookie` round-trip,
and full-page navigation. Owned by the single root-level `e2e/` project, as
lanes: demo-next/demo-nuxt (each a mock lane and, for next, a real lane on
`@zitadel/testing`), console (shell smoke, dev-proxy real-instance,
binary-served embedded, and platform-project lanes), and the journey (the
framework matrix across all 8 SDKs). Middleware-owning SDKs get their own
lane because
the proxy and route-protection layers are framework-specific.
The docs site's suite is the one that lives beside its app,
`apps/docs/tests/`: written against a URL, it runs on the local build
(`docs:e2e`) and on every deployment of the hosted cloud
(`docs:e2e-deployment`; `pnpm check:cloud` runs it beside the console's
deployment suite, see
[docs/runbooks/cloud.md](docs/runbooks/cloud.md#deployment-checks)).
The consumer journey suite is the exception to the checked-in demo ownership
rule: it belongs in `e2e/journey/` and must exercise a freshly
generated app because it protects the customer local setup path.
A new test belongs at e2e level only when the boundary it covers is
exclusively the framework integration (middleware, cookie origin, full
navigation). Component, atom, and orchestrator behaviour stays in
Vitest.
E2E tasks depend on the relevant build tasks, so the components `dist/` is rebuilt
automatically before each run — be aware of this when iterating with a
long-running dev server: stale `dist/` will silently mask orchestrator
changes.
## Building UI (console and design system)
Console screens and shared UI are driven by the Figma **Design System** file
(`Zitadel - Design System - External`), not by flattened app mocks. There are
two UI surfaces and they do not share components — only tokens (ADR 055). Before
building any UI under `apps/console/**` or
`packages/{components,design-tokens}/**`, **decide which surface it belongs to**
(see [`apps/console/docs/styling.md`](apps/console/docs/styling.md)):
1. **The console** (shell, page layout, tables, app widgets — most console UI) —
compose **shadcn/ui** from `apps/console/src/components/ui/`, installing from
the registry when a component is missing, and style with the **unprefixed
shadcn utility contract** (`bg-background`, `text-muted-foreground`, …) from
`@zitadel/design-tokens/css/shadcn.css`. Do **not** use `bg-zl-*`/`text-zl-*`
utilities there — the canonical statement of this console exception is in
[`packages/design-tokens/AGENTS.md`](packages/design-tokens/AGENTS.md).
Do not import login atoms into console UI, and do not build a Lit twin for it.
2. **The login surface** (`<zitadel-login>` and the `<zl-*>` atoms) — Lit in
shadow DOM, vanilla CSS keyed to `--zl-*`. No Tailwind, no shadcn components:
neither survives the shadow boundary. Add or change an atom via the Storybook
recipe ([`apps/storybook/AGENTS.md`](apps/storybook/AGENTS.md)) and iterate
there behind the a11y gate, not on a console mock.
Both surfaces are theme-portable: tokens are authored as `{ dark, light }` pairs
and flip via `[data-theme="light"]` (amended ADR 014 §5). When adding atom CSS,
never hardcode a colour; use the semantic `--zl-*` roles.
Where the component lives decides the iteration tool: console UI iterates on the
console dev server, verified at light/dark and each breakpoint; login atoms
iterate in Storybook. A missing visual value is a **new token** in
`@zitadel/design-tokens`, never a magic value — except licensed brand assets
(e.g. display fonts), which stay in the consuming app because the tokens are
inlined into the public `@zitadel/components` bundle.
## Resource identifiers
Resource primary keys are dialect-owned `prefix_<opaque>` strings
([ADR 047](docs/adrs/047-dialect-id-generation.md)). Never add custom
ULID/UUID generation in domain, service, API, or other packages. The canonical
minting contract (`Ensure` / `NewManagedID`, the `idgen` boundary) lives in
[`internal/storage/AGENTS.md`](internal/storage/AGENTS.md).
## Generated Files
- Do not hand-edit `api/generated/**`; update `api/openapi/**` and run
`moon run server:generate`. CI enforces committed generated output through
`server:check-generate` (via `server:test`). Bare `go generate ./...` produces
the same output, but runs every directive serially — slower, though not by the
multiple the parallelism suggests, because `./api` is itself a serial chain and
the larger half of the run (`scripts/go-generate.mjs` carries the numbers). Both go
through the same `//go:generate` directives; the moon task just schedules them
(`scripts/go-generate.mjs`). Either path works over a pruned tree. The one
thing that makes that non-trivial lives in `api/cmd/gen_openapi_errors`, which
type-loads `./internal/...` to infer each operation's error set: the load needs
`api/generated`, which only ogen — the last link of api's own chain — writes,
and it needs enumer's output, which `./...` reaches only after `api`. The
generator resolves both itself, running those two directives (`go generate
-run`) over a placeholder spec before analyzing. Neither caller knows about the
cycle; do not reintroduce a scheduler-side bootstrap pass.
Wire fields are `snake_case`, enforced by the redocly rules and
`workspace:check-openapi-rules` — the wire contract rules live in
[api/openapi/AGENTS.md](api/openapi/AGENTS.md).
- The consolidated `mockgen` directives live in the mock packages
(`internal/domain/mock`, `internal/service/mocks`, `internal/crypto/mock`),
not beside the interfaces they mock. mockgen has to type-check the source
package, which does not compile until its `*_enumer.go` files exist, so the
directive has to run after them. `go generate ./...` walks packages in
import-path order, which sequences it correctly. Moving one of these
directives back next to its interfaces breaks generation from a clean tree.
- Generating never deletes. A generated file whose directive stopped producing
it — a renamed destination, an interface no longer mocked — stays on disk and
stays committed, still compiling and still importable. `server:generate`
cannot tell you about it, because it only ever writes.
To prune, remove everything and regenerate:
```sh
moon run server:clean-generated && moon run server:generate
```
Whatever does not come back was orphaned; commit its deletion. Do this when
you change where a generator writes, drop an interface from a mockgen
directive, or delete a type that had an `enumer` directive.
`moon run server:clean-generated -- --dry-run` lists what would go without
touching anything. Generation restores everything that is still generated —
which is the point: what stays missing was the orphan — so cleaning is safe at
any time.
- Do not hand-edit generated package output under `dist/`.
- Do not hand-edit `apps/console/src/routeTree.gen.ts`; update route files and
let the TanStack Router plugin regenerate it.
- Keep `apps/cli/skills/zitadel-cli/SKILL.md` aligned with the current CLI command surface and
agent contract when CLI behavior changes.
## CLI Contract
The CLI is an agent-facing product surface;
[apps/cli/skills/zitadel-cli/SKILL.md](apps/cli/skills/zitadel-cli/SKILL.md) is the canonical agent-facing
reference for consuming it. Preserve the JSON envelope contract:
`--json` output must be parseable JSON on stdout, include top-level
`cli_version`, `command`, `source`, and `status`, and avoid stray stdout text.
Agent scripts should pass `--non-interactive --json` and prefer structured
`next_commands` over prose hints. When invoking the local root wrapper for JSON
capture, use `corepack pnpm --silent run cli -- ... --json`; plain `pnpm run`
prints its own script prelude before the CLI output.
For customer-local runtime workflows, agents should prefer
`zitadel doctor`, `zitadel start`, and `--server local` before running
`zitadel setup`.
## Release, Licensing, And Secrets
- PR titles use `<type>(optional-scope): <summary>`, with
[`.github/semantic.yml`](.github/semantic.yml) as the source of truth for
allowed types and scopes. Omit the scope when unsure; do not invent scopes.
- Pick the type by **who the change reaches, not how much work it was**. If the
change needs no changeset it is not `feat` or `fix`; if it changes what a user
receives it is not `docs` or `chore`. CI enforces the first of those. Ladder,
worked examples, and summary voice:
[CONTRIBUTING.md](CONTRIBUTING.md#title-format). Self-check before opening:
`node scripts/check-pr-title.mjs --title "<title>"`.
- Agent-created or agent-updated PRs must include a concise description with
`Summary`, `Validation`, `Release notes / changeset`, and `Notes` sections
before handoff. In **Release notes / changeset**, state the outcome from
the [decision table](.changeset/README.md#decision-table). Full conventions:
[CONTRIBUTING.md](CONTRIBUTING.md#opening-a-pull-request).
### Changesets
The changeset decision — including when a Go-only change needs one — lives in
[`.changeset/README.md`](.changeset/README.md); follow its
[decision table](.changeset/README.md#decision-table). Agents write the
`.changeset/*.md` file directly rather than using the interactive prompt, then
verify with `corepack pnpm exec changeset status --since origin/main`.
- npm packages under `apps/cli/` and `packages/*` must stay MIT-licensed.
- Server npm packages under `apps/server/` and `apps/server-*/` and console
application paths are AGPL-3.0-only by default.
- Keep local secrets, private keys, tokens, and `.zitadel/secret`-style files out
of source control and browser-safe runtime metadata.
## General Guidelines for working with Moon
- Use Moon as the task front door for workspace build graph work:
`moon ci`, `moon run <project>:<task>`, and release tasks under
`moon run release:*`.
- Keep direct underlying tools for implementation details: `vite`, `vitest`,
`tsc`, `playwright`, `biome`, `go`, `docker buildx`, and `changeset`.
- Do not reintroduce Nx or GoReleaser without updating the ADRs first.
- When unsure about Moon flags, check `moon --help` or the task definition in
the nearest `moon.yml` before guessing.
- Before editing a `moon.yml` or `.moon/`, read
[.moon/AGENTS.md](.moon/AGENTS.md): the shared tasks, the inputs rule and the
dependency rules, and the `moon run workspace:check-moon-graph` check to run
afterwards.
- Turborepo runs the same tasks beside moon during the migration
([docs/design/build/turborepo.md](docs/design/build/turborepo.md)): a task
change goes into `moon.yml` and the package's `turbo.json`, and the command
is a `package.json` script.
## Ephemeral Development Environments
Fresh cloud-agent VMs and containers commonly hit the following; none of it
is tool-specific.
### Node.js version
The repo requires the Node.js version from `.nvmrc`; sandbox images often
ship an older default. Ensure the `.nvmrc` version is first on `$PATH` (for
example via nvm) before running any `corepack` or `pnpm` command.
`.nvmrc` names only the Node.js major (`24`), and every runtime in the repo
uses that major. Minor and patch versions do not matter, so never pin one:
- `.devcontainer/devcontainer.json` (the `node` feature `version`)
- `apps/cli-skill-e2e/scripts/run.mjs` (the `node:<major>` image)
- the Vercel-deployed apps (`console`, `docs`, `mock-zitadel`, `storybook`)
set `engines.node` to `<major>.x`
- CI workflows read `.nvmrc` through `node-version-file`; never hard-code a
`node-version`
`@types/node` in the pnpm catalog tracks the `.nvmrc` major, so code cannot
type-check against APIs the runtime lacks. When the major changes, bump all
of these together.
The same rule holds for dependencies:
- Use caret ranges (`^1.2.3`), not exact versions. An exact pin needs a
comment next to it saying why (see `@redocly/openapi-core` in
`pnpm-workspace.yaml`).
- A dependency used by more than one package lives in the pnpm catalog and
is referenced as `catalog:`, so the whole workspace resolves one version
of it.
- Peer dependency ranges of the published SDKs stay wide (`>=18`); they
describe what consumers may install, not what the repo uses.
### Playwright browser install gotcha
The standard `playwright install --with-deps chromium` may hang during zip
extraction in minimal VMs. If that happens, download and extract manually:
```sh
curl -fsSL -o /tmp/chrome-linux64.zip \
"https://cdn.playwright.dev/builds/cft/<version>/linux64/chrome-linux64.zip"
mkdir -p ~/.cache/ms-playwright/chromium-<rev>
unzip -q /tmp/chrome-linux64.zip -d ~/.cache/ms-playwright/chromium-<rev>
```
Repeat for `chrome-headless-shell-linux64.zip` into `chromium_headless_shell-<rev>`.
Look up `<version>` (browserVersion) and `<rev>` (revision) in the installed
package's `browsers.json` at
`node_modules/.pnpm/playwright-core@<pkg>/node_modules/playwright-core/browsers.json`.
### Stale Nuxt lock files
If a Nuxt e2e test or dev server was killed ungracefully, a stale lock file at
`apps/demo-nuxt/.nuxt/dev/.lock` may block the next startup. Remove it and
the `.nuxt` directory if you see "Another Nuxt dev server is already running".