Skip to content

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 as POST /zitadel.project.v2.ProjectService/CreateProject with 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

  1. The API layer authenticates the caller (Bearer token) and checks the permission declared on the RPC (for example project.create).
  2. The command handler loads the relevant aggregate state and validates business rules (state preconditions, policies).
  3. It produces events such as user.human.added or user.human.password.changed.
  4. Push takes a shared transaction-scoped advisory lock (pg_advisory_xact_lock_shared on eventstore.events2, keyed by instance), checks and updates unique constraints (for example unique usernames), and inserts the events through the eventstore.push function 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

  1. 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.
  2. A spooler per projection (leader-elected across replicas) periodically catches up on events the pub-sub missed.
  3. Each projection records its last processed position in current_sequences. An event that keeps failing is written to failed_events after the retry limit so the projection is not blocked.
  4. 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.
  5. 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:

  1. Create (POST /v2/sessions): start a session with an optional user and initial checks (password, passkey, IdP intent, TOTP, OTP).
  2. Set (PATCH /v2/sessions/{id}): add further checks such as a second factor, or request a WebAuthn challenge.
  3. Session token: returned on create and set; required for later updates and to finish an OIDC or SAML auth request (CreateCallback or CreateResponse).
  4. 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:

  1. Token verification: resolve the user from the Bearer token (JWT or opaque via introspection).
  2. Membership lookup: find the caller's administrator memberships at system, instance (IAM), organization, project, and project-grant level.
  3. Role expansion: map each membership's roles (for example ORG_OWNER) to permissions (for example project.write) using the InternalAuthZ role-permission mapping.
  4. 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), or restAsync (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 a ZITADEL-Signature HMAC 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, or presamlresponse (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

Sources