@zitadel/cli 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,124 @@
1
+ ### Driving the login UI
2
+
3
+ `<zitadel-login>`, `<zitadel-session>`, and `<zitadel-logout>` are Lit elements
4
+ with open shadow roots. The stable automation hooks live inside nested shadow roots, so a flat
5
+ `document.querySelector('[data-testid="zitadel-input-email"]')` will not find
6
+ the native control. Browser drivers with shadow-DOM-aware locators, such as
7
+ Playwright, can target the hooks directly. Generic DOM-eval drivers should
8
+ pierce shadow roots recursively:
9
+
10
+ ```js
11
+ function deepQuery(sel, root = document) {
12
+ const hit = root.querySelector(sel);
13
+ if (hit) return hit;
14
+ for (const el of root.querySelectorAll("*")) {
15
+ if (el.shadowRoot) {
16
+ const result = deepQuery(sel, el.shadowRoot);
17
+ if (result) return result;
18
+ }
19
+ }
20
+ return null;
21
+ }
22
+ ```
23
+
24
+ Use host hooks such as `zitadel-field-email`, `zitadel-field-password`, and
25
+ `zitadel-action-submit` when targeting the Lit atoms. Use native shadow-control
26
+ hooks such as `zitadel-input-email`, `zitadel-input-password`, and
27
+ `zitadel-action-submit-button` when filling or clicking the underlying input or
28
+ button. Hooks stay method-named even when the flow engine names a credential
29
+ field `x-auth-methods#<method>`; only the `name` attribute carries that raw
30
+ form key. Enter inside a field submits the step's primary action, but only for
31
+ key events that carry `key: "Enter"` — drivers whose synthesized key events
32
+ omit it (some CDP wrappers) should click `zitadel-action-submit` instead.
33
+
34
+ For sign-out, the profile page that setup generates renders `<zitadel-session>`:
35
+ click its `zitadel-session-logout` host, or the native button inside it,
36
+ `zitadel-session-logout-button`. An app that renders `<zitadel-logout>` instead
37
+ hides sign-out behind an avatar menu: click the "Open user menu" button, then
38
+ pierce to `.signout-btn` (Playwright: `zitadel-logout .signout-btn`), which
39
+ carries no hook. A `<zitadel-logout>` with a `<template>` child shows no menu:
40
+ click the app's own element marked `data-action="logout"`. The canonical
41
+ component hook list lives in `packages/components/README.md`.
42
+
43
+ The checked-in automated regression path is `moon run e2e:journey`, which
44
+ exercises fresh-app setup plus registration, logout, and login across the
45
+ supported frameworks.
46
+
47
+ Repo config is authoritative: edit `zitadel.json` or files under `.zitadel/`,
48
+ then re-run `deploy` (or `deploy --dry-run` to validate and preview the diff
49
+ without publishing). Schema and flow files are synced from
50
+ `.zitadel/schemas/*.json` and `.zitadel/flows/*.json`. Login templates
51
+ (branding) are synced from `.zitadel/branding/`: a single `branding.json`
52
+ descriptor (layout, asset URLs) plus a sibling `login.liquid` LiquidJS
53
+ template referenced as `"liquid_template": { "$file": "./login.liquid" }`. Scaffold them with the
54
+ `branding eject` command (`--design centered|minimal`,
55
+ interactive picker on a TTY); setup never scaffolds them. Branding is
56
+ revisioned and immutable: every edit — including a `.liquid`-only edit —
57
+ is published as part of the next release when you `deploy`, and the login
58
+ serves the newest release. `deploy` validates templates with the authoritative
59
+ LiquidJS validator (and `deploy --dry-run` runs the same validation without
60
+ publishing; `E_VALIDATION` lists rule ids such as `no-script-tag` and
61
+ `mandatory-gates`; every template must keep a `{% mandatory_gates %}` tag,
62
+ where the login adds any required field and the primary action the template
63
+ left out). Asset URLs must be absolute `https://`; during development, images
64
+ may use `http://` on `localhost`, `127.x.x.x`, or `[::1]` (not `*.localhost`).
65
+ `typography.font_url` needs a `typography.font_family`, is always `https://`,
66
+ and is loaded only when the login runs as `variant="page"`, which setup
67
+ generates only when it creates a Next.js or Nuxt app itself, in an empty
68
+ directory. Keep exactly one descriptor in
69
+ `.zitadel/branding/` — extra `*.json` files there fail the scan.
70
+ Server-provisioned defaults remain a fallback for non-CLI project
71
+ creation, but CLI-created projects are authored from local files first. Flows are
72
+ revisioned like branding: an edit is published as a new immutable flow revision
73
+ in the next `deploy`. A flow names its schema by `objectType` in `user_schema`
74
+ and runs against the schema's newest revision, so a schema revise needs no
75
+ change to `user_schema`; one that removes or renames a property a step
76
+ collects needs that step's `fields` updated, and `deploy` checks each flow
77
+ against the schema it names. A login pinned by `flow-name` serves that flow's
78
+ newest active revision; an unpinned login serves the newest active unscoped
79
+ revision for its purpose in the project, whatever its flow's name. Older
80
+ revisions stay active, so an `audience` added after a flow was deployed
81
+ without one doesn't take that earlier revision out of the running. Removing a
82
+ flow file does not retire the
83
+ flow. Operation policies live in `.zitadel/policies/`: one `<operation>.json`
84
+ per guarded operation, named after its `operation`, holding
85
+ `{"$schema": "../meta/policy.json", "operation": "auth.factor.password.set",
86
+ "config": {"min_length": 15}}`. `deploy` checks each file against
87
+ `.zitadel/meta/policy.json` (known operation, only the template's settings,
88
+ within their bounds) and its name before anything is sent; settings that
89
+ contradict each other (a `min_length` above `max_length`) are refused by the
90
+ server's release build with `rel.invalid`, the policy's entry carrying
91
+ `pol.invalid`. A changed file publishes a new revision of the same policy,
92
+ which applies the next time a password is set. A setting left out is off, and
93
+ `"config": {}` switches every rule off. A sign-in or sign-up uses the policy
94
+ the deployed release carries, so deploying without the file switches it off
95
+ there; a password set through the API, or before the first deploy, uses the
96
+ newest revision, which only `"config": {}` switches off. `policies list` and
97
+ `policies get <id|operation>` read them back. A policy's cases live beside it
98
+ in `<operation>.test.json` (`.zitadel/meta/policy-test.json`): each names an
99
+ `input` in the operation's terms (`{"password": "…"}`), optional `facts` the
100
+ operation would look up (`self_service`, `history_matches`), and the expected
101
+ decision (`allowed`, optionally `code` and exactly the `violations`). The
102
+ decision and rules hold in the login too; `code` is the API's answer, and a
103
+ flow shows each rule as `error.x-auth-methods#password_<rule>` instead.
104
+ `policies test [operation]` evaluates every case on the server against the
105
+ sibling policy file, or with `--release <id>` against the revision that
106
+ release pins; a missing policy is tested as none. It fails with `E_VALIDATION`
107
+ (exit 3) when a case does not hold, `data`/`details` carrying `source`,
108
+ `operations[].cases[]` (`name`, `passed`, `expected`, `actual`), `passed` and
109
+ `failed`; with no test files it is `skipped` (`no-policy-tests`). `deploy`
110
+ never carries test files. Until #2331, a release that pins no flow is tested
111
+ with a warning: its sign-ins use the newest revisions instead. Write cases from
112
+ what the user wants enforced; never invent expectations for them. `setup` writes and publishes
113
+ the password policy with a `min_length` of 15; pass `--password-min-length <n>`
114
+ only when the user asks for another value (`0` sets none). Managed files carry
115
+ a marker comment; `eject` removes only
116
+ files that still carry it, preserving anything the user replaced. For app-local
117
+ development, `--server local` resolves through `.zitadel/local/runtime.json` and
118
+ requires a healthy
119
+ `npx @zitadel/cli@latest start` runtime. The `.zitadel/local/` that `start`
120
+ writes into the directory still counts as empty, so a same-directory `setup`
121
+ scaffolds without `--force` (this is exactly what the golden path does).
122
+ `setup` installs dependencies
123
+ with the detected package manager by default; pass `--skip-install` when the
124
+ agent or host workflow will install dependencies separately.
@@ -0,0 +1,152 @@
1
+ ---
2
+ name: zitadel-components
3
+ description: >-
4
+ Embed, theme, customise, or drive the Zitadel login web component directly —
5
+ the framework-agnostic `<zitadel-login>` / `<zitadel-logout>` Lit elements
6
+ from `@zitadel/components`. Use when dropping the login widget onto a plain
7
+ HTML page, styling it with `--zl-*` design tokens or `::part(...)` hooks,
8
+ pinning its theme/variant/locale, wiring its flow events, or automating it in
9
+ a browser via its shadow-DOM hooks — i.e. using the Zitadel login widget
10
+ itself, outside any framework SDK wrapper.
11
+ ---
12
+
13
+ # @zitadel/components
14
+
15
+ Lit web components for the Zitadel auth UI: the `<zitadel-login>` orchestrator
16
+ (a drop-in element that calls the flow API and renders every step),
17
+ `<zitadel-logout>` / `<zitadel-session>`, and the `<zl-*>` atoms.
18
+
19
+ ## When to use
20
+
21
+ Use this skill when you embed, theme, or automate the login/logout web
22
+ component **directly** — on a static page, in a server-rendered app, or inside
23
+ any shell where you hold the custom element yourself. The framework SDK skills
24
+ (Next, Nuxt, React, etc.) wrap this same element for a specific framework; reach
25
+ for those when the user is in that framework. Reach for this one when the work
26
+ is the component itself: attributes, tokens, parts, events, shadow-DOM hooks.
27
+
28
+ ## Install / use
29
+
30
+ ```sh
31
+ corepack pnpm add @zitadel/components@latest
32
+ ```
33
+
34
+ `lit`, `liquidjs`, `dompurify` are externalised peer/runtime deps (consumers
35
+ dedupe their own copies). A CDN `standalone` build is published (`unpkg` /
36
+ `jsdelivr` → `./dist/standalone.mjs`). Subpath exports exist for `./atoms`,
37
+ `./orchestrator`, `./tokens`, `./manifests`, and JSX types at `./jsx`. Importing
38
+ the barrel side-effect-registers every element; import leaf subpaths to
39
+ tree-shake.
40
+
41
+ ## Embed & configure
42
+
43
+ Configure the SDK once, then drop the element — it reads the global project
44
+ handle from `configureZitadel()` via `getZitadelConfig()`:
45
+
46
+ ```html
47
+ <script type="module">
48
+ import '@zitadel/components';
49
+ import { configureZitadel } from '@zitadel/api/config';
50
+ configureZitadel({ projectId: 'proj_123', proxyPath: '/__nextgen' });
51
+ </script>
52
+ <zitadel-login id="login" variant="page" purpose="login"></zitadel-login>
53
+ ```
54
+
55
+ `<zitadel-login>` props (the authoritative list is in the README table and the
56
+ TS types — discover, don't assume): `variant` (`widget` default: content-sized,
57
+ transparent, no font injection, no focus grab / `page`: full-page chrome),
58
+ `theme` (`light`/`dark`/`auto`, resolved value lands on `data-theme`), `purpose`
59
+ (`login`/`register`/`reset_password`/…), `flow-name`, `project` (object property,
60
+ not an attribute — assign via `ref`/JS, not markup), `lang` + `locales`,
61
+ `post-sign-in-url` (`force-post-sign-in-url`: see below), `resume-flow-id`,
62
+ `preview-state` / `preview-success-step`.
63
+ After sign-in the widget returns to the path in the page's `next` query
64
+ parameter, which every server SDK's middleware sets when it sends a signed-out
65
+ visit to a protected route to the login page, and goes to `post-sign-in-url`
66
+ when there is none or it is not a path on the page's origin. A page that uses
67
+ `next` for something of its own sets `force-post-sign-in-url`.
68
+ It emits `zitadel-flow-input`, `-step`, `-complete`, `-error`, `-redirect`
69
+ events (`-step` fires for every step including the first; `-redirect` is a
70
+ non-cancelable notification — the widget navigates to the IdP itself).
71
+
72
+ `<zitadel-logout>`: `project`, `post-sign-out-url`; supports a light-DOM
73
+ `<template>` slot for a custom menu.
74
+
75
+ Theming levers (strongest-first resolution, documented in the README):
76
+
77
+ - **Host CSS tokens** — set `--zl-*` custom properties in your own stylesheet on
78
+ `zitadel-login { ... }`. These inherit across shadow boundaries and **outrank**
79
+ both the design-system defaults and server-side tenant branding. The stable
80
+ names are listed in
81
+ [Customize the login design](https://preview.zitadel.com/docs/guides/customize-design.md).
82
+ - **`::part(...)`** — e.g. `zitadel-login::part(form)`, `::part(field-input)`,
83
+ `::part(button-root)`. Atom parts forward through the orchestrator as
84
+ `<atom>-<part>`; bare names (`zl-field::part(input)`) apply when composing
85
+ atoms directly.
86
+ - **`variant` + `--zl-page-min-height`** for sizing/placement; width-responsive
87
+ chrome keys off the widget's own width (container queries).
88
+ - Server branding payload, and a tenant Liquid template via the payload's
89
+ `liquid_template` field (a declarative `template` attribute is not yet
90
+ exposed).
91
+ - **`suppress-header`** (boolean attribute, reflected) hides the widget's own
92
+ heading — use it when the host page already shows a title and you want to
93
+ avoid a duplicate. The login moves between sign-in and sign-up by itself, so
94
+ that title must follow its `zitadel-flow-step` event: `step.purpose` is
95
+ `login` or `register`.
96
+ Otherwise control chrome through `variant` and tokens.
97
+ - **`suppress-password-toggle`** (boolean attribute) hides the show/hide button
98
+ the login shows on password fields by default. It applies to custom Liquid
99
+ templates too; templates have nothing to wire.
100
+
101
+ Automation hooks (stable `data-testid`s the default template emits): host atoms
102
+ `zitadel-field-email` / `zitadel-field-password` / `zitadel-action-submit`;
103
+ native shadow controls `zitadel-input-email` / `zitadel-input-password` /
104
+ `zitadel-action-submit-button`. Hooks are method-named even when the flow engine
105
+ names a credential field `x-auth-methods#<method>` — the `name` attribute keeps
106
+ the raw wire key; only the hook is normalised (`hookName` in
107
+ `src/internal/hook-name.ts`). For sign-out, the button inside `<zitadel-session>`
108
+ (the signed-in card setup's profile pages render) carries
109
+ `zitadel-session-logout`, and its native button `zitadel-session-logout-button`.
110
+ `<zitadel-logout>` has no hook: open its avatar menu ("Open user menu"), then
111
+ pierce to `.signout-btn`. Given a `<template>` child, it shows no menu, and the
112
+ element marked `data-action="logout"` signs out.
113
+
114
+ ## Read these, don't guess
115
+
116
+ The component surface moves faster than any list here, so treat these as
117
+ authoritative and discover the current shape first:
118
+
119
+ - `node_modules/@zitadel/components/README.md` — the element API tables, the
120
+ customisation tiers, and the **canonical hook list** live here.
121
+ - The published TypeScript types (`./dist/index.d.mts` and the `./atoms`,
122
+ `./orchestrator`, `./tokens`, `./jsx` subpaths) — exact prop/event/manifest
123
+ shapes.
124
+ - [Customize the login design](https://preview.zitadel.com/docs/guides/customize-design.md)
125
+ — the stable `--zl-*` tokens and the `::part()` names the login forwards.
126
+ - Per-atom manifests (`@zitadel/components/manifests`) — the allowed
127
+ attributes/parts/events for each atom.
128
+
129
+ Read the pattern, then confirm the specific attribute/part/token/event against
130
+ these before writing it — don't reproduce a catalogue from memory.
131
+
132
+ ## Gotchas
133
+
134
+ - **Server rendering.** If your app renders on the server, mount the widget on the client only, after hydration, or give a raw `<zitadel-login>` element `project-id` and `proxy-path` as attributes, because a widget that starts before its configuration arrives shows an error instead of the flow.
135
+ - **Open shadow roots, nested.** The automation hooks sit inside nested shadow
136
+ roots, so a flat `document.querySelector('[data-testid="zitadel-input-email"]')`
137
+ misses them. Use a shadow-aware locator (Playwright) or recurse through
138
+ `el.shadowRoot`. Enter submits the step only for key events carrying
139
+ `key: "Enter"`; CDP wrappers that omit it should click `zitadel-action-submit`.
140
+ - **Same-origin proxy.** The element calls the typed `@zitadel/api` fetch client
141
+ directly with `credentials: "include"`; the stateless server's `_zflow`
142
+ HttpOnly cookie is the source of truth between requests. There is no transport
143
+ to swap — mock at the network layer (`@zitadel/api-mock` MSW handlers) for
144
+ offline/test.
145
+ - **Theming through shadow DOM.** `--zl-*` custom properties inherit across
146
+ shadow boundaries, so host-page token overrides reach the atoms' own shadow
147
+ roots and win over `:host` defaults and server branding — deliberate, so an
148
+ embedding app matches its design system without a server round-trip. Internals
149
+ you can't reach with a token need `::part(...)`, not a descendant selector.
150
+ - **`project` is a property, not an attribute** — it's a typed object, so set it
151
+ via JS/`ref` (or configure globally and let the element read it), never as
152
+ markup.
@@ -0,0 +1,149 @@
1
+ ---
2
+ name: zitadel-sdk-angular
3
+ description: >-
4
+ Integrate Zitadel authentication into an Angular app with
5
+ @zitadel/sdk-angular — the client-side SPA components for login,
6
+ registration, logout, and the signed-in session card. Use when adding or
7
+ wiring Zitadel auth, a login or registration screen, or session handling into
8
+ an Angular app, and especially when the app already exists and the CLI's
9
+ setup stops on it (then wire it in by hand with this skill).
10
+ ---
11
+
12
+ # @zitadel/sdk-angular
13
+
14
+ Angular wrapper components for the Zitadel auth UI in a client-side SPA.
15
+
16
+ ## When to use
17
+
18
+ You are adding Zitadel login, registration, logout, or session handling to an
19
+ Angular app and wiring the auth UI in by hand.
20
+
21
+ The `zitadel` CLI scaffolds a *fresh* Angular app
22
+ (`npx @zitadel/cli@latest setup --framework angular`) with this SDK already wired in. In an
23
+ **existing** Angular app it writes the root component to `src/app/app.ts`, and
24
+ stops with `E_CONFLICT` when the app has one there, or when `src/main.ts`
25
+ bootstraps a root from another file (Angular 17–19's
26
+ `src/app/app.component.ts`). When setup stops, follow this skill to install the
27
+ package and wire the components in manually.
28
+
29
+ ## Install
30
+
31
+ ```sh
32
+ npm install @zitadel/sdk-angular@latest
33
+ ```
34
+
35
+ Peer dependencies: `@angular/core` and `@angular/common`, both `>=17`. Requires TypeScript ≥ 5.0 (the published types use `export type *`).
36
+
37
+ ## Integrate
38
+
39
+ The components are **standalone** — import them directly into a component's
40
+ `imports`. There is no NgModule, provider, or route guard to register.
41
+
42
+ 1. Build a project handle once with `configureZitadel(...)` and pass it to the
43
+ components via the `[project]` input:
44
+
45
+ ```ts
46
+ import { Component } from '@angular/core';
47
+ import { ZitadelLoginComponent, configureZitadel } from '@zitadel/sdk-angular';
48
+
49
+ const project = configureZitadel({ projectId: '…', proxyPath: '/__nextgen' });
50
+
51
+ @Component({
52
+ standalone: true,
53
+ imports: [ZitadelLoginComponent],
54
+ template: `<zitadel-auth-login
55
+ [project]="project"
56
+ purpose="login"
57
+ postSignInUrl="/"
58
+ />`,
59
+ })
60
+ export class LoginPage {
61
+ project = project;
62
+ }
63
+ ```
64
+
65
+ 2. Logout and the signed-in session card follow the same shape:
66
+
67
+ ```html
68
+ <zitadel-auth-logout [project]="project" postSignOutUrl="/login" />
69
+ <zitadel-auth-session [project]="project" postSignOutUrl="/login" />
70
+ ```
71
+
72
+ 3. Wire them into your routes as ordinary standalone components, e.g. a
73
+ `/login` route rendering `LoginPage` and a `/logout` route rendering a
74
+ component that imports `ZitadelLogoutComponent`. `purpose="login"` vs
75
+ `purpose="register"` on `<zitadel-auth-login>` selects sign-in vs
76
+ registration; `postSignInUrl` / `postSignOutUrl` are where the widget sends
77
+ the user afterwards.
78
+
79
+ 4. The widgets call `${proxyPath}/…` **same-origin** (default `/__nextgen`). For
80
+ local development you can instead point `proxyPath` straight at the backend
81
+ (cross-origin), e.g. `proxyPath: 'http://localhost:8080'` (the `zitadel start`
82
+ local runtime default port).
83
+
84
+ ## Widget props and events
85
+
86
+ `ZitadelLogin`, `ZitadelLogout` and `ZitadelSession` take the props below, spelled the same in every SPA SDK. An unset prop keeps the widget's own default.
87
+
88
+ | Prop | Type | Default | Widgets | Description |
89
+ | --- | --- | --- | --- | --- |
90
+ | `project` | `ZitadelProject` | unset | Login, Logout, Session | The handle from `configureZitadel(...)`. Wins over `projectId` and `proxyPath`. |
91
+ | `projectId` | `string` | unset | Login, Logout, Session | Project id, read when no `project` handle is passed. |
92
+ | `proxyPath` | `string` | `"/__nextgen"` | Login, Logout, Session | Same-origin path the widget calls. |
93
+ | `purpose` | `CreateFlowBodyPurpose` | `"login"` | Login | Flow purpose. |
94
+ | `flowName` | `string` | unset | Login | Runs the flow definition with this `name`; unset runs the project's default flow for the purpose. |
95
+ | `postSignInUrl` | `string` | unset | Login | Where to navigate after a completed sign-in. When the page's `next` query parameter names a path on the same origin, as the server SDKs' middleware sets it for a signed-out visit to a protected route, the widget returns there instead. |
96
+ | `forcePostSignInUrl` | `boolean` | `false` | Login | Navigates to `postSignInUrl` even when the page's `next` query parameter names a path, for a page that uses `next` for something of its own. |
97
+ | `postSignOutUrl` | `string` | unset | Logout, Session | Where to navigate after sign-out. |
98
+ | `resumeFlowId` | `string` or `null` | unset | Login | An existing flow's id to resume instead of starting a new flow; unset, `null` or `""` falls back to the `flow` query parameter, and an id that no longer resolves starts a new flow. |
99
+ | `variant` | `"widget"` or `"page"` | `"widget"` | Login, Session | `widget` is content-sized and paints no page chrome; `page` claims the viewport, paints the surface background and loads the brand font, and on Login focuses the first field. |
100
+ | `theme` | `"light"`, `"dark"` or `"auto"` | unset | Login, Logout, Session | Colour mode. Unset follows the tenant's branding theme on Login, then the variant default (`dark` for `page`, `auto` for `widget`); Logout defaults to `auto`. `auto` follows `prefers-color-scheme`. |
101
+ | `suppressHeader` | `boolean` | `false` | Login, Session | Visually hides the widget's own heading block while keeping it in the accessibility tree, for pages that already carry the heading. |
102
+ | `suppressPasswordToggle` | `boolean` | `false` | Login | Hides the show/hide button on password fields. |
103
+ | `heading` | `string` | `"Signed in as"` | Session | Heading text override. |
104
+ | `logoutLabel` | `string` | `"Sign out"` | Session | Logout action label override. |
105
+ | `locales` | `Record<string, Partial<Record<string, string>>>` | unset | Login | Copy overrides merged over the builtin dictionaries, keyed by primary language subtag, for example `{ en: { "identifier.title": "Welcome back" } }`. |
106
+ | `lang` | `string` | browser language | Login | BCP-47 language override. |
107
+
108
+ Each widget event reaches the callback in the last column, which receives the event's `detail` payload.
109
+
110
+ | Widget event | Fires when | Detail | Widgets | Callback |
111
+ | --- | --- | --- | --- | --- |
112
+ | `zitadel-flow-step` | Login shows a step, the first one included. | `{ step }` | Login | `(flowStep)` |
113
+ | `zitadel-flow-input` | The visitor edits a field. | `{ name, value }` | Login | `(flowInput)` |
114
+ | `zitadel-flow-complete` | The flow reaches a terminal step. | `{ behavior, redirect_uri?, handoff_token?, handoff_token_expires_at? }` | Login | `(flowComplete)` |
115
+ | `zitadel-flow-redirect` | The widget hands the browser to an identity provider; nobody is signed in yet. | `{ redirect_url, step }` | Login | `(flowRedirect)` |
116
+ | `zitadel-flow-error` | The flow reports an error. | `{ message }` | Login | `(flowError)` |
117
+ | `zitadel-signout` | The user signs out. | `{ display, identifier }` | Logout, Session | `(signout)` |
118
+
119
+ ```html
120
+ <zitadel-auth-login [project]="project" theme="light" (flowComplete)="track($event)" />
121
+ ```
122
+
123
+ The `zitadel-flow-complete`, `zitadel-flow-redirect` and `zitadel-signout` details carry a `waitUntil(promise)`; call it from your handler to make the widget wait for your async work before it navigates.
124
+
125
+ ## Read these, don't guess
126
+
127
+ This skill shows the *pattern*; it is not an API catalog and will drift. For
128
+ exact names, inputs, and props, treat the installed package as authoritative:
129
+
130
+ - `node_modules/@zitadel/sdk-angular/README.md` — the install, usage, and
131
+ proxy/deployment notes.
132
+ - The TypeScript types / exports (`node_modules/@zitadel/sdk-angular/dist/types/public-api.d.ts`)
133
+ — the exported symbols (`ZitadelLoginComponent`, `ZitadelLogoutComponent`,
134
+ `ZitadelSessionComponent`, `configureZitadel`, `ZitadelConfig`,
135
+ `ZitadelProject`, `businessLocales`) and every component `@Input()` /
136
+ `@Output()`. Read them before using an input you are unsure of.
137
+
138
+ ## Gotchas
139
+
140
+ - **Server rendering.** If your app renders on the server, mount the widget on the client only, after hydration (the `zitadel-auth-login` wrapper sets `projectId` and `proxyPath` as element properties, so adding them to the wrapper does not put them in server markup), or use a raw `<zitadel-login>` element with `project-id` and `proxy-path` as attributes, because a widget that starts before its configuration arrives shows an error instead of the flow.
141
+ - **Selector is `zitadel-auth-login`, not `zitadel-login`.** The wrapper's
142
+ selector is `zitadel-auth-login` (and `zitadel-auth-logout`,
143
+ `zitadel-auth-session`); `<zitadel-login>` is the underlying custom element
144
+ the wrapper renders internally — don't use it directly.
145
+ - **Production SPA deployment is not yet supported.** The same-origin
146
+ `${proxyPath}` must be provided by your hosting platform, and the CLI cannot
147
+ yet scaffold those configs. The CLI dev proxy covers local development only.
148
+ - **Session card export name.** `<zitadel-auth-session>` is exported as
149
+ `ZitadelSessionComponent`.
@@ -0,0 +1,174 @@
1
+ ---
2
+ name: zitadel-sdk-next
3
+ description: >-
4
+ Integrate Zitadel authentication into a Next.js App Router app with
5
+ @zitadel/sdk-next. Use whenever the user wants to add Zitadel login,
6
+ registration, logout, or session handling to a Next.js project — wiring the
7
+ auth middleware/proxy, reading sessions in Server or Client Components,
8
+ rendering the `<zitadel-login>` UI, or customizing an existing Zitadel + Next
9
+ integration.
10
+ ---
11
+
12
+ # @zitadel/sdk-next
13
+
14
+ Next.js middleware and helpers that wire the Zitadel auth UI and session
15
+ verification into an App Router app.
16
+
17
+ ## When to use
18
+
19
+ Use this when adding or customizing Zitadel auth in an **existing or custom**
20
+ Next.js app, or when you need to understand how the pieces fit together.
21
+
22
+ For a brand-new app, prefer the CLI: `npx @zitadel/cli@latest setup --framework next` scaffolds
23
+ the whole integration (middleware, login page, env) and wires it for you. Reach
24
+ for this skill when that scaffold already exists and you are editing it, or when
25
+ the app is bespoke and you are integrating by hand.
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ pnpm add @zitadel/sdk-next@latest
31
+ ```
32
+
33
+ Peer deps the package expects: `next >=15.2.3`, `react >=18`, `react-dom >=18`.
34
+ TypeScript `>=5.0` (the SDK consumer floor; the package is built against a
35
+ newer TypeScript, but that does not raise the floor for your project).
36
+
37
+ ## Integrate
38
+
39
+ The package has per-runtime entry points — import from the one that matches
40
+ where the code runs, not the root, from client modules:
41
+
42
+ | Import | Runs in | Provides |
43
+ | ------------------------------ | --------------------------------- | ---------------------------------- |
44
+ | `@zitadel/sdk-next/middleware` | Edge middleware | `nextgenMiddleware`, `createProxy` |
45
+ | `@zitadel/sdk-next/server` | Server Components, Route Handlers | `auth()`, `NextgenProvider` |
46
+ | `@zitadel/sdk-next/react` | Client Components | `useAuth()`, `AuthContextProvider` |
47
+ | `@zitadel/sdk-next/session` | Client Components | `getSession()` |
48
+ | `@zitadel/sdk-next/client` | Client boundary | `configureZitadel()`, web components |
49
+ | `@zitadel/sdk-next/components` | Client Components | `ZitadelLogin`, `ZitadelLogout`, `ZitadelSession` |
50
+
51
+ **1. Request-boundary proxy.** It proxies `/__nextgen/*` to the auth backend
52
+ (same-origin), verifies the session JWT via JWKS, and redirects unauthenticated
53
+ users on protected routes. **The filename and exported function depend on the
54
+ Next.js major version** — `proxy.ts`/`proxy` is Next 16+, while Next 15 uses the
55
+ request middleware:
56
+
57
+ - **Next 16+:** create `src/proxy.ts` exporting `proxy`.
58
+ - **Next 15:** create `src/middleware.ts` exporting `middleware`.
59
+
60
+ The body is the same either way (only the file and function name change):
61
+
62
+ ```ts
63
+ // Next 16+: src/proxy.ts, exporting `proxy`.
64
+ // Next 15: src/middleware.ts, exporting `middleware` (rename the function).
65
+ import { nextgenMiddleware } from '@zitadel/sdk-next/middleware';
66
+ import type { NextRequest } from 'next/server';
67
+
68
+ export function proxy(req: NextRequest) {
69
+ return nextgenMiddleware(req, {
70
+ url: process.env.ZITADEL_URL,
71
+ protectedRoutes: ['/admin', '/dashboard*'],
72
+ loginPath: '/login',
73
+ });
74
+ }
75
+
76
+ // Every route but Next.js's build files: protectedRoutes alone decides what
77
+ // needs a sign-in. A protected route a narrower matcher leaves out stays public.
78
+ export const config = { matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'] };
79
+ ```
80
+
81
+ When unsure which the installed Next.js uses, check the CLI's own output — its
82
+ patcher emits `middleware.ts` for Next 15 and `proxy.ts` for Next 16+.
83
+
84
+ **2. Read the session server-side** with `auth()` (it re-verifies the tunnelled
85
+ token — the header alone is never trusted):
86
+
87
+ ```tsx
88
+ import { auth } from '@zitadel/sdk-next/server';
89
+
90
+ // Render <Greeting /> inside <Suspense>: with Cache Components on (the
91
+ // create-next-app default), Next.js refuses request data outside it.
92
+ async function Greeting() {
93
+ const session = await auth();
94
+ if (!session.isAuthenticated) return <p>Not signed in</p>;
95
+ return <p>Hello {session.session.display ?? session.session.userId}</p>;
96
+ }
97
+ ```
98
+
99
+ **3. Seed client components** by rendering `NextgenProvider` (server-only — it
100
+ strips the raw token before the server→client boundary) in your root layout,
101
+ then read with `useAuth()` in any client component. For chrome on pages the
102
+ matcher does not cover, read client-side with `getSession()` from
103
+ `@zitadel/sdk-next/session` instead — it fetches same-origin
104
+ `{proxyPath}/api/v1/sessions/me`.
105
+
106
+ **4. Login page.** Render `ZitadelLogin` from `@zitadel/sdk-next/components`
107
+ client-side only (via `next/dynamic` with `ssr: false`); call
108
+ `configureZitadel({ projectId, proxyPath })` from the same entry inside the
109
+ dynamic import, and set the `postSignInUrl` prop. `ZitadelLogout` and
110
+ `ZitadelSession` exist for logout and profile UI. Sign-in/out navigate to `post-sign-in-url` /
111
+ `post-sign-out-url`, so chrome re-reads on the next page load.
112
+
113
+ ## Widget props and events
114
+
115
+ `ZitadelLogin`, `ZitadelLogout` and `ZitadelSession` from `@zitadel/sdk-next/components` are the `@zitadel/sdk-react` wrappers, so they take the same camelCase props, which the `@zitadel/sdk-react` skill lists. They set object props such as `project` and `locales` as element properties on React 18 and 19, through server-rendered hydration too. The table below lists the events and their callbacks.
116
+
117
+ Each widget event reaches the callback in the last column, which receives the event's `detail` payload.
118
+
119
+ | Widget event | Fires when | Detail | Widgets | Callback |
120
+ | --- | --- | --- | --- | --- |
121
+ | `zitadel-flow-step` | Login shows a step, the first one included. | `{ step }` | Login | `onFlowStep` |
122
+ | `zitadel-flow-input` | The visitor edits a field. | `{ name, value }` | Login | `onFlowInput` |
123
+ | `zitadel-flow-complete` | The flow reaches a terminal step. | `{ behavior, redirect_uri?, handoff_token?, handoff_token_expires_at? }` | Login | `onFlowComplete` |
124
+ | `zitadel-flow-redirect` | The widget hands the browser to an identity provider; nobody is signed in yet. | `{ redirect_url, step }` | Login | `onFlowRedirect` |
125
+ | `zitadel-flow-error` | The flow reports an error. | `{ message }` | Login | `onFlowError` |
126
+ | `zitadel-signout` | The user signs out. | `{ display, identifier }` | Logout, Session | `onSignout` |
127
+
128
+ ```tsx
129
+ <ZitadelLogin project={project} theme="light" onFlowComplete={(detail) => track(detail)} />
130
+ ```
131
+
132
+ ## Read these, don't guess
133
+
134
+ This skill is the **pattern**, not an API catalog — it deliberately omits exact
135
+ prop/option names so it does not drift with the package version. For the
136
+ authoritative, version-correct specifics:
137
+
138
+ - `node_modules/@zitadel/sdk-next/README.md` — the full setup walkthrough with
139
+ every code sample.
140
+ - The package's TypeScript types (`.d.ts` per entry point) — exact exported
141
+ names, component props, `configureZitadel()` config, and the full middleware
142
+ options table (`proxyPath`, `ignoredRoutes`, `audience`, `allowedAlgorithms`,
143
+ timeouts, …).
144
+
145
+ Confirm names against those before writing code.
146
+
147
+ ## Gotchas
148
+
149
+ - **Never import the package root from a `"use client"` module.** The root
150
+ re-exports the server-only `auth()`; pulling it into a client graph fails the
151
+ build. Client code imports from `/react` or `/session` only.
152
+ - **`NextgenProvider` is server-only** and must not be re-exported through a
153
+ `"use client"` `providers.tsx` wrapper — that would leak the unstripped token
154
+ into the RSC flight payload. To seed from client state, use
155
+ `AuthContextProvider` from `/react` (it only takes the token-less shape).
156
+ - **Same-origin proxy is required.** The widgets and `getSession()` hit
157
+ `{proxyPath}/api/v1/sessions/me` on the same origin; the middleware `matcher` must
158
+ cover `{proxyPath}/:path*` or the proxy breaks.
159
+ - **A protected route must be in the matcher too.** Next.js runs the
160
+ middleware only on routes the matcher covers, so a `protectedRoutes` entry a
161
+ narrower matcher misses is never redirected. Keep the every-route matcher.
162
+ - **`auth()` only sees a session on routes the matcher covers.** A header on an
163
+ unmatched public page always looks signed out — use `getSession()` there.
164
+ - **A rejected `getSession()` means *unknown*, not signed out** (broken proxy /
165
+ network / 5xx). Render a neutral or error state, never the signed-out CTA.
166
+ - **Config handles:** `ZITADEL_URL` (backend, server-side),
167
+ `ZITADEL_PROJECT_ID` (server-side) and `NEXT_PUBLIC_ZITADEL_PROJECT_ID`
168
+ (passed to `configureZitadel`). If the middleware runs with custom
169
+ verification options, pass the same ones to `auth()` so both layers accept
170
+ the same tokens.
171
+ - **Sessions count only in the app's own project:** `projectId`, else
172
+ `ZITADEL_PROJECT_ID`, else `NEXT_PUBLIC_ZITADEL_PROJECT_ID`. With none set,
173
+ nobody is signed in. A session of another project on the same host reads as
174
+ signed out.