Explanation¶
How Zitadel works
Zitadel is a single stateless Go binary (APIs, Management Console, Login V1, background projections) plus, since v4, a separate Next.js Login V2 app. All state lives in PostgreSQL as an append-only event log (eventstore.events2) from which read models are projected (CQRS). This page explains the components, the event-sourcing pipeline, the authentication and authorization flows, extensibility through Actions, multi-tenancy, deployment topologies, and the security model. Look-up tables (paths, roles, config keys, versions) are in Reference; tasks are in How-to Guides.
Component Topology¶
The public entry point is a reverse proxy that must speak HTTP/2 to the API container; /ui/v2/login goes to the Login V2 container, everything else to the Zitadel binary.
flowchart TB
subgraph Clients["Clients"]
Browser["Browser"]
Apps["Your apps<br/>(OIDC / SAML RPs)"]
M2M["Service accounts<br/>(JWT profile, PAT)"]
end
Proxy["Reverse proxy / Ingress<br/>(Traefik, NGINX, Gateway API)<br/>h2c to backend"]
subgraph LoginApp["zitadel-login container (Next.js, MIT)"]
LoginV2["Login V2<br/>/ui/v2/login"]
end
subgraph Binary["zitadel container (Go, AGPL-3.0)"]
direction TB
subgraph Serve["Service layer"]
API["APIs: gRPC, Connect, REST<br/>v2 resource APIs + legacy v1"]
OIDC["OIDC / OAuth / SAML<br/>endpoints"]
Console["Management Console<br/>(Angular) /ui/console"]
LoginV1["Login V1 (legacy)<br/>/ui/login"]
end
subgraph Core["Core (CQRS)"]
Cmd["Command handlers<br/>+ validation"]
Query["Query handlers"]
Proj["Projections<br/>(pub-sub + spooler)"]
Exec["Actions V2 executions"]
Notif["Notification workers"]
end
end
subgraph Data["PostgreSQL 14-18"]
ES["eventstore.events2"]
RM["projections.* read models"]
Cache["cache schema (optional)"]
end
Redis["Redis standalone<br/>(optional cache)"]
Targets["Action targets<br/>(your HTTP endpoints)"]
IdPs["External IdPs<br/>(Entra ID, Okta, Google, LDAP)"]
SMTP["SMTP / SMS / webhook<br/>notification providers"]
OTel["OTel collector /<br/>Prometheus"]
Browser --> Proxy
Apps --> Proxy
M2M --> Proxy
Proxy -->|"/ui/v2/login"| LoginV2
Proxy -->|"everything else"| Serve
LoginV2 -->|"Session, OIDC, SAML v2 APIs<br/>(IAM_LOGIN_CLIENT PAT)"| API
API --> Cmd
API --> Query
OIDC --> Cmd
Cmd --> ES
ES --> Proj --> RM
Query --> RM
Query -.-> Cache
Query -.-> Redis
Cmd --> Exec --> Targets
Proj --> Notif --> SMTP
OIDC --> IdPs
Binary --> OTel
Monolithic Binary, Modular Internals¶
Each Zitadel binary contains everything needed to serve traffic: APIs, GUIs, background event processing, and scheduled jobs. The docs call this the "All in One" approach: one artifact to deploy and scale, with leader election for background work so any replica can run it. The main packages under internal/ are:
| Package | Purpose |
|---|---|
internal/eventstore/ |
Event store abstraction; v3 pushes events through the eventstore.push SQL function |
internal/command/ |
Write side: command handlers and business-rule validation |
internal/query/ |
Read side: query handlers and projections |
internal/api/ |
gRPC, Connect, REST gateways, plus api/oidc, api/saml, api/ui (Login V1, Console) |
internal/authz/ |
Permission checks against memberships and roles |
internal/actions/ |
Actions V1: embedded goja JavaScript engine |
internal/execution/ |
Actions V2: calls to external targets |
internal/crypto/ |
Encryption, hashing, key handling |
internal/webauthn/ |
FIDO2/WebAuthn |
internal/notification/ |
Email, SMS, and webhook delivery |
internal/idp/ |
External identity provider integrations |
internal/cache/ |
Cache connectors (memory, PostgreSQL, Redis) |
internal/queue/ |
Background job queue |
internal/serviceping/ |
Service Ping usage reports |
internal/user/, internal/org/, internal/project/ |
Domain aggregates (legacy repositories) |
Login V2: A Separate Next.js App¶
Login V2 is a Next.js application (apps/login/ in the monorepo, MIT-licensed, image ghcr.io/zitadel/zitadel-login). It holds no data of its own. It authenticates to the Zitadel API with a service user that has the IAM_LOGIN_CLIENT role; setup writes that user's PAT to a file (ZITADEL_FIRSTINSTANCE_LOGINCLIENTPATPATH) which the login container reads. It then drives the same public v2 APIs any custom login UI can use: SessionService, OIDCService, and SAMLService.
Since v4.0.0 (2025-07-31) Login V2 is the default for new instances. Existing instances keep Login V1 after an upgrade until an administrator switches. Login V1 still exists inside the binary, and several 2026 advisories affect V1 and V2 differently (see Reference).
The OIDC authorization code flow with Login V2 looks like this:
sequenceDiagram
participant App as Relying party
participant B as Browser
participant Z as Zitadel API
participant L as Login V2 (Next.js)
participant ES as eventstore.events2
App->>B: Redirect to /oauth/v2/authorize (PKCE)
B->>Z: GET /oauth/v2/authorize
Z->>ES: Append auth request added
Z-->>B: 302 to /ui/v2/login/login?authRequest=ID
B->>L: Load login page
L->>Z: OIDCService.GetAuthRequest (service user PAT)
B->>L: Submit login name, password or passkey
L->>Z: SessionService.CreateSession / SetSession with checks
Z->>ES: Append session events
L->>Z: OIDCService.CreateCallback(session id and token)
Z-->>L: Callback URL with authorization code
L-->>B: 302 to app redirect_uri?code=...
B->>App: Deliver code
App->>Z: POST /oauth/v2/token (code + code_verifier)
Z-->>App: ID token, access token, refresh token
API Surface¶
Every resource is defined in Protocol Buffers under proto/zitadel/ (Apache-2.0) and generated into gRPC, Connect RPC, and (where the proto has HTTP annotations) REST. Two generations coexist:
- v1 (legacy, context-based): the scope comes from the service you call:
AuthService(the caller's own user),ManagementService(the caller's organization),AdminService(the instance),SystemService(all instances, self-hosted system users). Fully supported, no longer extended. Many v1 endpoints were deprecated in v4.0.0. - v2 (resource-based): one service per resource (users, sessions, organizations, projects, applications, instances, IdPs, groups, settings, features, authorizations, actions, web keys, OIDC, SAML). Scope comes from the resource and the caller's permissions. From v4, new v2 services are Connect/gRPC-first and do not add OpenAPI 2.0 REST mappings, so some (for example
ProjectService,ApplicationService) are called asPOST /zitadel.project.v2.ProjectService/CreateProjectwith a JSON body rather than a REST path.
The full path and service list is in Reference.
Data Model¶
Hierarchical Resource Model¶
The resource hierarchy is Instance > Organization > Project > Application, with roles defined on projects and granted to users or to other organizations.
erDiagram
INSTANCE ||--o{ ORGANIZATION : contains
ORGANIZATION ||--o{ USER : manages
ORGANIZATION ||--o{ PROJECT : owns
ORGANIZATION ||--o{ IDP_CONFIG : configures
ORGANIZATION ||--o{ POLICY : enforces
PROJECT ||--o{ APPLICATION : registers
PROJECT ||--o{ ROLE : defines
PROJECT ||--o{ PROJECT_GRANT : shares
USER ||--o{ USER_GRANT : receives
USER ||--o{ SESSION : creates
USER ||--o{ IDP_LINK : links
PROJECT_GRANT }o--|| ORGANIZATION : "granted to"
USER_GRANT }o--|| PROJECT : "scoped to"
ROLE }o--|| USER_GRANT : "included in"
INSTANCE {
string instance_id PK
string domain
}
ORGANIZATION {
string org_id PK
string name
string state
}
USER {
string user_id PK
string org_id FK
string type
string state
}
PROJECT {
string project_id PK
string org_id FK
string name
}
APPLICATION {
string app_id PK
string project_id FK
string type
}
ROLE {
string role_key PK
string project_id FK
string group
}
PROJECT_GRANT {
string grant_id PK
string project_id FK
string granted_org_id FK
}
USER_GRANT {
string grant_id PK
string user_id FK
string project_id FK
}
Users are either human (profile, email, phone, password, passkeys, IdP links) or machine (service accounts with keys, secrets, PATs). IDs are Sonyflake IDs generated by Zitadel.
Eventstore Schema¶
The event log is the table eventstore.events2, introduced by setup migration 14_events_push to replace the older eventstore.events table. Its primary key is (instance_id, aggregate_type, aggregate_id, sequence), so ordering is guaranteed per aggregate, and a global position plus in_tx_order orders events across aggregates. Other schemas: projections (read models, current_sequences, failed_events), system (assets, encryption keys), and older auth, adminapi, notification projection schemas that are being folded into projections. See Reference for the column list.
Event Sourcing Pipeline¶
Zitadel combines event sourcing (the event log is the single source of truth) with CQRS (separate write and read models). The docs classify the command side as "consistent and available" and the query side as "available and performant", which makes Zitadel eventually consistent by design.
flowchart LR
subgraph Write["Command side"]
API["API request"]
Cmd["Command handler<br/>+ validation"]
Push["eventstore.push()<br/>advisory lock per instance"]
ES[("eventstore.events2")]
end
subgraph Project["Projection side"]
PubSub["In-memory pub-sub"]
Spooler["Spooler<br/>(leader-elected per projection)"]
RM[("projections.* tables")]
Failed[("failed_events")]
end
subgraph Read["Query side"]
Query["Query handler"]
Trigger["Trigger projection<br/>for id lookups"]
end
API --> Cmd --> Push --> ES
ES --> PubSub --> RM
ES --> Spooler --> RM
Spooler -.->|"after max retries"| Failed
Query --> RM
Query --> Trigger --> ES
Write Path: Command Processing¶
- The API layer authenticates the caller (Bearer token) and checks the permission declared on the RPC (for example
project.create). - The command handler loads the relevant aggregate state and validates business rules (state preconditions, policies).
- It produces events such as
user.human.addedoruser.human.password.changed. Pushtakes a shared transaction-scoped advisory lock (pg_advisory_xact_lock_sharedoneventstore.events2, keyed by instance), checks and updates unique constraints (for example unique usernames), and inserts the events through theeventstore.pushfunction in one transaction.
Event Structure¶
| Field | Purpose |
|---|---|
instance_id |
Tenant (instance) the event belongs to |
aggregate_type, aggregate_id |
The resource, for example user + Sonyflake ID |
event_type |
The change, for example user.human.added |
sequence |
Per-aggregate, monotonically increasing |
revision |
Aggregate schema version used to interpret the payload |
position, in_tx_order |
Global ordering across aggregates |
created_at |
Timestamp |
creator |
User or system component that caused the event |
owner |
Resource owner (organization or instance) |
payload |
JSON event data |
Read Path: Projections¶
- After a push, events go to an in-memory pub-sub that feeds subscribed projections in near real time. It gives no delivery guarantee, on purpose: the event store is the record, so anything missed can be replayed. The docs note this was chosen over an external message queue to keep operations simple.
- A spooler per projection (leader-elected across replicas) periodically catches up on events the pub-sub missed.
- Each projection records its last processed position in
current_sequences. An event that keeps failing is written tofailed_eventsafter the retry limit so the projection is not blocked. - Query handlers read projection tables. For lookups by ID, the query side can trigger the projection first so the response includes the latest events for that ID. List queries can be slightly stale.
- Caches (optional) hold instance, organization, and milestone objects in memory, in PostgreSQL unlogged tables, or in Redis.
Projection Rebuilding¶
Because read models are derived data, they can be rebuilt by replaying events. On a version upgrade, zitadel setup --init-projections=true builds new projections before the new pods take traffic; otherwise zitadel start serves immediately and projections catch up in the background, which can take a long time on large event logs. The same property makes restores simple: restore PostgreSQL, and projections resume from their recorded positions.
Authentication Session Lifecycle¶
OIDC Authorization Code Flow (with PKCE)¶
With Login V2 the flow is shown in the Login V2 section. With Login V1 the same endpoints are used, but the login pages are rendered by the Zitadel binary at /ui/login/. Either way the app sees standard OIDC: /oauth/v2/authorize, /oauth/v2/token, /oidc/v1/userinfo, JWKS at /oauth/v2/keys.
Session Model (V2 API)¶
The Session API is what makes custom and hosted logins possible without redirect-only flows:
- Create (
POST /v2/sessions): start a session with an optional user and initial checks (password, passkey, IdP intent, TOTP, OTP). - Set (
PATCH /v2/sessions/{id}): add further checks such as a second factor, or request a WebAuthn challenge. - Session token: returned on create and set; required for later updates and to finish an OIDC or SAML auth request (
CreateCallbackorCreateResponse). - Delete (
DELETE /v2/sessions/{id}): terminate the session.
Passkey Authentication Flow¶
With the Session API, a passkey login is a challenge on the session followed by a verified assertion.
sequenceDiagram
participant Browser as Browser / Login UI
participant ZA as Zitadel SessionService
participant Auth as Authenticator
Browser->>ZA: POST /v2/sessions (user check + WebAuthn challenge request)
ZA->>ZA: Generate WebAuthn challenge
ZA-->>Browser: Session id, session token, challenge options
Browser->>Auth: navigator.credentials.get(options)
Auth->>Auth: User verification (biometric or PIN)
Auth-->>Browser: Signed assertion
Browser->>ZA: PATCH /v2/sessions/{id} (webAuthN check with assertion)
ZA->>ZA: Verify signature against stored public key
ZA->>ZA: Append session WebAuthn-checked event
ZA-->>Browser: New session token
Authorization Resolution¶
When an API request arrives, Zitadel resolves permissions as follows:
- Token verification: resolve the user from the Bearer token (JWT or opaque via introspection).
- Membership lookup: find the caller's administrator memberships at system, instance (IAM), organization, project, and project-grant level.
- Role expansion: map each membership's roles (for example
ORG_OWNER) to permissions (for exampleproject.write) using theInternalAuthZrole-permission mapping. - Context check: an organization or project role only grants a permission for resources inside that organization or project.
flowchart TB
Req["Incoming API request"]
Token["Verify token<br/>(JWT or introspection)"]
Member["Load memberships<br/>(IAM, org, project, grant)"]
Roles["Expand roles to permissions<br/>(InternalAuthZ)"]
Check["Match permission and<br/>resource context"]
Allow{"Allowed?"}
Req --> Token --> Member --> Roles --> Check --> Allow
Allow -->|"Yes"| Handler["Command or query handler"]
Allow -->|"No"| Deny["PermissionDenied"]
Two authorization layers must not be confused: administrator roles (built in, control access to the Zitadel APIs) and project roles (defined by you, asserted into tokens for your apps via urn:zitadel:iam:org:project:roles claims when role assertion is enabled).
Actions & Webhooks Pipeline¶
Zitadel has two extension generations.
| Aspect | Actions V1 | Actions V2 |
|---|---|---|
| Model | JavaScript run inside Zitadel (goja engine) | HTTP calls to your own endpoints ("targets") |
| Triggers | Flows (pre/post authentication, pre/post creation, complement token, ...) | Executions on request, response, function, or event |
| Isolation | Shares the Zitadel process | Fully external, any language or serverless platform |
| Status | Deprecated; no new features; APIs to be removed in the next major | GA since v4.0.0 |
Actions V2 has three parts:
- Target: the endpoint plus call type.
restWebhook(fire and check status),restCall(response can change the payload), orrestAsync(fire and forget). Each has a timeout and an interrupt-on-error flag. Payloads are JSON, or signed JWT or encrypted JWE. Every call carries aZITADEL-SignatureHMAC header computed with the target's signing key. - Execution: binds a condition to targets. Conditions are a request or response of a gRPC method or service (or all), a function such as
preuserinfo,preaccesstoken, orpresamlresponse(used to add claims), or an event or event group. - Endpoint: your code. Request and response executions can reject or modify calls; function executions can add claims or metadata; event executions react after the fact (for example, notify when a user is locked).
Currently configured V1 actions still run alongside V2 executions, so migrations can be incremental.
Multi-Tenancy Hierarchy¶
Zitadel isolates tenants at two levels: instances (fully separate identity systems sharing one deployment and database; Zitadel Cloud tenants are instances) and organizations inside an instance (B2B customers or business units).
flowchart TB
Instance["Instance<br/>(self-hosted system or Cloud tenant)"]
Org1["Organization A<br/>(SaaS vendor)"]
Org2["Organization B<br/>(customer)"]
Proj1A["Project: Billing API"]
Proj1B["Project: User Portal"]
App1["App: Web client (OIDC)"]
App2["App: Mobile app (OIDC native)"]
App3["App: Legacy app (SAML)"]
Roles1["Roles: admin, editor, viewer"]
Grant1["Project grant<br/>to Org B"]
OrgBUsers["Org B users<br/>(managed by Org B admins)"]
Instance --> Org1
Instance --> Org2
Org1 --> Proj1A
Org1 --> Proj1B
Proj1A --> App1
Proj1A --> App2
Proj1B --> App3
Proj1A --> Roles1
Proj1A -.->|"delegates admin, editor"| Grant1
Grant1 -.-> Org2
Org2 --> OrgBUsers
Project grants are the B2B primitive: Organization A defines roles on a project and grants a subset to Organization B, whose administrators (PROJECT_GRANT_OWNER) assign those roles to their own users. The owner can deactivate or delete the grant at any time. Per-organization login policies, branding, IdPs, and domain discovery let each customer get its own sign-in experience on one instance.
Deployment Topologies¶
Single Host (Development and Homelab)¶
The official compose pack runs Traefik in front of the API and Login containers with PostgreSQL behind them.
flowchart LR
subgraph Host["Docker host (compose project zitadel)"]
T["traefik<br/>:8080 to :80"]
ZA["zitadel-api<br/>start-from-init :8080 (h2c)"]
ZL["zitadel-login<br/>:3000"]
PG[("postgres 17")]
R[("redis<br/>profile: cache")]
end
T -->|"/ui/v2/login"| ZL
T -->|"all other paths"| ZA
ZL --> ZA
ZA --> PG
ZA -.-> R
Kubernetes Production (HA)¶
The Helm chart runs zitadel init and zitadel setup as Jobs before the Deployments roll, so API pods only run zitadel start.
flowchart TB
subgraph Edge["Ingress or Gateway API"]
ING["Ingress controller<br/>(TLS termination, h2c upstream)"]
end
subgraph K8s["Kubernetes 1.30+"]
Init["Job: zitadel-init<br/>(roles, database, schemas)"]
Setup["Job: zitadel-setup<br/>(migrations, projections)"]
subgraph ZA["Deployment: zitadel (HPA, PDB)"]
Z1["zitadel pod"]
Z2["zitadel pod"]
Z3["zitadel pod"]
end
subgraph ZLD["Deployment: zitadel-login"]
L1["login pod"]
L2["login pod"]
end
Redis["Redis (optional)"]
end
PGHA[("PostgreSQL HA<br/>(managed or operator)")]
ING -->|"/ui/v2/login"| ZLD
ING -->|"/"| ZA
Init --> PGHA
Setup --> PGHA
L1 --> ZA
L2 --> ZA
Z1 --> PGHA
Z2 --> PGHA
Z3 --> PGHA
ZA -.-> Redis
The reference design from the docs is three application nodes and one storage node per cluster, spread across availability zones. Zero-downtime updates work by running the new version's setup, starting new pods alongside the old ones, and shifting traffic when /debug/ready reports ready.
Multi-Region and Zitadel Cloud¶
For multiple regions the docs recommend at least three independent clusters, kept in sync with PostgreSQL read replicas, with traffic steered by path, hostname, or IP. Zitadel Cloud runs the same codebase (no SaaS-only fork) in US, EU, AU, and CH regions.
flowchart TB
subgraph Cloud["Zitadel Cloud (region chosen per instance)"]
ZC["Managed Zitadel instance"]
PG_C[("Managed PostgreSQL")]
end
subgraph Remote1["Your cluster 1"]
App1["Application A"]
end
subgraph Remote2["Your cluster 2"]
App2["Application B"]
end
App1 -->|"OIDC / SAML"| ZC
App2 -->|"OIDC / SAML"| ZC
ZC --> PG_C
Security Model¶
Authentication Mechanisms¶
| Protocol | Status | Use case |
|---|---|---|
| OpenID Connect | Certified OpenID Provider | Web and mobile SSO, API access tokens |
| OAuth 2.0 | Full, plus token exchange (impersonation) and dynamic client registration | API authorization, M2M |
| SAML 2.0 | IdP | Enterprise and legacy apps |
| WebAuthn / FIDO2 | First-class | Passkeys, U2F second factor |
| LDAP | As an external IdP | Active Directory and LDAP directories |
| SCIM 2.0 | Server | Inbound provisioning from Entra ID, Okta, and others |
Grant types include authorization code with PKCE, refresh token, JWT profile, client credentials, token exchange, and device code. The resource owner password grant is not supported, which removes a common credential-phishing pattern.
Multi-Factor Authentication¶
| Method | Type |
|---|---|
| TOTP | Authenticator app (RFC 6238) |
| U2F | Hardware security keys through WebAuthn |
| Email OTP | One-time code to a verified email |
| SMS OTP | One-time code via SMS provider |
Login policies (instance or organization) decide which factors are allowed and whether MFA is forced for local users or all users.
Passkeys and Passwordless¶
Passkeys are a primary login method, not only a second factor: platform authenticators (Touch ID, Windows Hello, Android), roaming keys (YubiKey, Titan), and synced passkeys (iCloud Keychain, Google Password Manager). The OIDC app config can publish Android Digital Asset Links for native passkey trust.
External Identity Providers¶
Zitadel acts as an identity broker. Templates exist for Google, GitHub, GitLab, Apple, Microsoft Entra ID, Okta, generic OIDC, generic OAuth, SAML, and LDAP. IdPs can be configured per instance or per organization, and domain discovery routes users to their organization's IdP by email domain.
Machine-to-Machine Authentication¶
| Method | Mechanism | Guidance |
|---|---|---|
| JWT profile | Service account signs a JWT with its private key | Preferred for production |
| Client credentials | Client ID + secret | Simple, but a shared secret |
| Personal access token | Static Bearer token with expiry | CI and scripts; set an expiration |
Authorization Model¶
Administrator access follows a three-level RBAC (instance, organization, project, plus project grants), with dozens of built-in roles such as IAM_OWNER, ORG_OWNER, ORG_USER_MANAGER, and PROJECT_OWNER (full list in Reference). Application authorization uses your own project roles, assigned to users directly (authorizations, formerly "user grants") or delegated through project grants.
Encryption and Key Management¶
| Layer | Mechanism |
|---|---|
| Stored secrets (IdP client secrets, SMTP passwords, keys) | AES-256 with the masterkey (exactly 32 characters, passed via --masterkey or ZITADEL_MASTERKEY). It cannot be changed after initialization without losing access to encrypted data |
| Passwords and client secrets | One-way hashes (bcrypt by default, cost 14; argon2id, scrypt, pbkdf2, sha2 configurable). Imported legacy hashes (md5 variants, phpass, drupal7) are verified and re-hashed on login |
| Public keys (FIDO2, U2F, JWT profile, signing) | Stored in the event log, protected against tampering by its append-only history |
| In transit | TLS at the proxy or on the Zitadel listener (--tlsMode), sslmode=verify-full to PostgreSQL |
| At rest in the database | Your PostgreSQL disk or TDE encryption |
OIDC tokens are signed with web keys managed through WebKeyService (/v2/web_keys) and published at /oauth/v2/keys. Web keys became mandatory in v4; installations that skipped them on v3 can invalidate existing JWTs when upgrading (technical advisory A-10017).
Audit Trail¶
Because every mutation is an event, the audit trail is a by-product of the storage model rather than a separate log:
| Capability | Detail |
|---|---|
| Completeness | Every create, update, and delete is an event with creator and timestamp |
| Time travel | State at any point can be reconstructed by replaying events |
| Access | Events API for programmatic export; Actions V2 event executions to stream to a SIEM |
| Retention | AuditLogRetention (default unlimited) |
Threat Model¶
| Threat | Mitigation in Zitadel |
|---|---|
| Credential phishing | Passkeys and U2F are origin-bound; MFA enforcement in login policies |
| Token replay | Short-lived access tokens, refresh token rotation, revocation and introspection |
| Privilege escalation | Context-scoped administrator roles; permission annotations on every RPC |
| Tenant data leakage | Instance and organization scoping in every query; per-org administrators |
| Database dump theft | Secrets encrypted with the masterkey, passwords hashed |
| Brute force | Lockout policies, password complexity; rate limiting must be added in front (production guide) |
| Compromised extension code | Actions V2 runs outside the process; V1 JavaScript runs inside it (a 2026 advisory concerned V1's goja loader according to third-party trackers) |
| Login UI flaws | The main recent risk area: multiple 2026 critical and high advisories hit Login V1 and V2 flows (account takeover, MFA bypass). Patch promptly |
| Insider threat | Immutable event history, separation of duties by role |
Security Policies¶
| Policy | Scope | Controls |
|---|---|---|
| Login policy | Instance / org | Allowed methods, MFA requirement, IdP allowlist, registration |
| Password complexity and lockout | Instance / org | Length, character classes, max attempts |
| Privacy policy | Instance / org | Terms of service, privacy links, support email |
| Branding | Instance / org | Logo, colors, fonts for the login |
| Domain settings | Instance / org | Login name must match org domain, domain verification |
SCIM 2.0 User Provisioning¶
The built-in SCIM 2.0 server lets an enterprise directory create, update, and deactivate users in a Zitadel organization at /scim/v2/{orgId}/Users. Calls are authorized with Zitadel service accounts and mapped to the normal user permissions, so provisioning shows up in the same event history.
Design Trade-offs¶
| Decision | Benefit | Cost |
|---|---|---|
| Event sourcing + CQRS | Complete audit trail, rebuildable read models, replay after incidents | Eventual consistency for list queries, event log grows forever, projection catch-up after upgrades |
| In-process pub-sub instead of a message queue | No extra infrastructure | Relies on the spooler to repair missed deliveries |
| PostgreSQL only (since v3) | Simpler testing and tuning; indexes tuned on Zitadel Cloud | No distributed SQL option for multi-region writes; CockroachDB users had to migrate |
| AGPL-3.0 core (since v3) | Protects against closed SaaS forks, funds development | Legal review needed for modified, network-served forks |
| Login split into a Next.js app (v4) | Login is replaceable and built on public APIs | Two containers, a service-user PAT to manage, and a new attack surface |
| Webhook Actions V2 | Isolation and any language | Network latency and availability of your endpoints in the login path |