ZITADEL Cheat Sheet
Overview
ZITADEL is an open-source, cloud-native identity management platform built in Go. It provides authentication, authorization, and user management with built-in multi-tenancy, supporting OAuth 2.0, OpenID Connect, SAML, and LDAP. ZITADEL is designed for B2B and B2C use cases with organizations, projects, and fine-grained role-based access control.
ZITADEL features event sourcing architecture, a powerful actions system for custom logic, branded login pages, machine-to-machine authentication, and comprehensive audit logs. It can be self-hosted or used as a managed cloud service, with SDKs available for multiple languages and frameworks.
Installation
# Docker (quickest start)
docker run -d \
--name zitadel \
-p 8080:8080 \
ghcr.io/zitadel/zitadel:latest start-from-init \
--masterkey "MasterkeyNeedsToHave32Characters" \
--tlsMode disabled
# Docker Compose
curl -fsSL https://raw.githubusercontent.com/zitadel/zitadel/main/docs/docs/self-hosting/deploy/docker-compose.yaml -o docker-compose.yaml
docker compose up -d
# Linux binary
curl -fsSL https://github.com/zitadel/zitadel/releases/latest/download/zitadel-linux-amd64.tar.gz | tar xz
./zitadel start-from-init --masterkey "MasterkeyNeedsToHave32Characters"
# Kubernetes via Helm
helm repo add zitadel https://charts.zitadel.com
helm install zitadel zitadel/zitadel
# Access console at http://localhost:8080/ui/console
ZITADEL CLI
# Initialize ZITADEL
./zitadel init --config defaults.yaml
# Start from init (first run)
./zitadel start-from-init \
--masterkey "MasterkeyNeedsToHave32Characters" \
--config defaults.yaml
# Start (subsequent runs)
./zitadel start --masterkey "MasterkeyNeedsToHave32Characters"
# Setup only (migration/initialization)
./zitadel setup --masterkey "MasterkeyNeedsToHave32Characters"
Management API
# Get a PAT (Personal Access Token) from the console first
# List users
curl -X POST https://your-instance.zitadel.cloud/v2/users \
-H "Authorization: Bearer $PAT" \
-H "Content-Type: application/json" \
-d '{"queries": []}'
# Create a human user
curl -X POST https://your-instance.zitadel.cloud/v2/users/human \
-H "Authorization: Bearer $PAT" \
-H "Content-Type: application/json" \
-d '{
"username": "john.doe",
"profile": {
"givenName": "John",
"familyName": "Doe",
"displayName": "John Doe"
},
"email": {
"email": "john@example.com",
"isVerified": true
},
"password": {
"password": "SecureP@ss123!",
"changeRequired": false
}
}'
# Create a machine user (service account)
curl -X POST https://your-instance.zitadel.cloud/v2/users/machine \
-H "Authorization: Bearer $PAT" \
-H "Content-Type: application/json" \
-d '{
"username": "api-service",
"name": "API Service Account",
"description": "Service account for API access",
"accessTokenType": "ACCESS_TOKEN_TYPE_JWT"
}'
# Create an organization
curl -X POST https://your-instance.zitadel.cloud/management/v1/orgs \
-H "Authorization: Bearer $PAT" \
-H "Content-Type: application/json" \
-d '{"name": "Acme Corporation"}'
OAuth 2.0 / OIDC Integration
// Node.js with openid-client
const { Issuer } = require("openid-client");
const issuer = await Issuer.discover("https://your-instance.zitadel.cloud");
const client = new issuer.Client({
client_id: "YOUR_CLIENT_ID",
client_secret: "YOUR_CLIENT_SECRET",
redirect_uris: ["http://localhost:3000/callback"],
response_types: ["code"],
});
// Authorization URL
const authUrl = client.authorizationUrl({
scope: "openid email profile",
resource: "https://api.example.com",
});
// Exchange code for tokens
const tokenSet = await client.callback(
"http://localhost:3000/callback",
{ code: req.query.code },
);
const userinfo = await client.userinfo(tokenSet.access_token);
React Integration
import { createBrowserRouter } from "react-router-dom";
import { UserManager } from "oidc-client-ts";
const userManager = new UserManager({
authority: "https://your-instance.zitadel.cloud",
client_id: "YOUR_CLIENT_ID",
redirect_uri: "http://localhost:3000/callback",
scope: "openid email profile",
response_type: "code",
});
// Login
await userManager.signinRedirect();
// Callback
const user = await userManager.signinRedirectCallback();
console.log(user.profile);
// Logout
await userManager.signoutRedirect();
Projects and Applications
# Create a project
curl -X POST https://your-instance.zitadel.cloud/management/v1/projects \
-H "Authorization: Bearer $PAT" \
-H "Content-Type: application/json" \
-d '{"name": "My Web Platform", "projectRoleAssertion": true}'
# Create an OIDC application
curl -X POST https://your-instance.zitadel.cloud/management/v1/projects/PROJECT_ID/apps/oidc \
-H "Authorization: Bearer $PAT" \
-H "Content-Type: application/json" \
-d '{
"name": "Web App",
"redirectUris": ["http://localhost:3000/callback"],
"postLogoutRedirectUris": ["http://localhost:3000"],
"responseTypes": ["OIDC_RESPONSE_TYPE_CODE"],
"grantTypes": ["OIDC_GRANT_TYPE_AUTHORIZATION_CODE"],
"appType": "OIDC_APP_TYPE_WEB",
"authMethodType": "OIDC_AUTH_METHOD_TYPE_BASIC"
}'
# Add project roles
curl -X POST https://your-instance.zitadel.cloud/management/v1/projects/PROJECT_ID/roles/_bulk \
-H "Authorization: Bearer $PAT" \
-H "Content-Type: application/json" \
-d '{
"roles": [
{"key": "admin", "displayName": "Administrator"},
{"key": "editor", "displayName": "Editor"},
{"key": "viewer", "displayName": "Viewer"}
]
}'
Actions (Custom Logic)
// Action: Add custom claims to tokens
function addCustomClaims(ctx, api) {
if (ctx.v1.user.grants) {
var roles = [];
ctx.v1.user.grants.grants.forEach(function(grant) {
grant.roles.forEach(function(role) {
roles.push(role);
});
});
api.v1.claims.setClaim("custom:roles", roles);
}
api.v1.claims.setClaim("custom:org", ctx.v1.user.org.name);
}
// Action: Block specific email domains
function blockDomain(ctx, api) {
var dominated = ["disposable.com", "tempmail.com"];
var email = ctx.v1.user.human.email;
dominated.forEach(function(domain) {
if (email.endsWith("@" + domain)) {
api.v1.deny("Registration from this email domain is not allowed");
}
});
}
Advanced Usage
Machine-to-Machine (JWT Profile)
const { createRemoteJWKSet, jwtVerify } = require("jose");
// Verify ZITADEL access tokens
const JWKS = createRemoteJWKSet(
new URL("https://your-instance.zitadel.cloud/oauth/v2/keys")
);
const { payload } = await jwtVerify(token, JWKS, {
issuer: "https://your-instance.zitadel.cloud",
audience: "YOUR_PROJECT_ID",
});
SAML Identity Provider
curl -X POST https://your-instance.zitadel.cloud/admin/v1/idps/saml \
-H "Authorization: Bearer $PAT" \
-H "Content-Type: application/json" \
-d '{
"name": "Corporate SSO",
"metadataXml": "<EntityDescriptor>...</EntityDescriptor>",
"binding": "SAML_BINDING_POST",
"nameIdFormat": "SAML_NAME_ID_FORMAT_PERSISTENT"
}'
Custom Login Branding
curl -X POST https://your-instance.zitadel.cloud/management/v1/orgs/me/policies/label \
-H "Authorization: Bearer $PAT" \
-H "Content-Type: application/json" \
-d '{
"primaryColor": "#5469d4",
"backgroundColor": "#fafafa",
"fontColor": "#1a1a1a",
"hideLoginNameSuffix": true,
"disableWatermark": true
}'
Configuration
# defaults.yaml
Log:
Level: info
Formatter:
Format: text
Port: 8080
ExternalDomain: localhost
ExternalPort: 8080
ExternalSecure: false
Database:
postgres:
Host: localhost
Port: 5432
Database: zitadel
User:
Username: zitadel
Password: zitadel
Admin:
Username: postgres
Password: postgres
# Environment variables
ZITADEL_MASTERKEY="MasterkeyNeedsToHave32Characters"
ZITADEL_EXTERNALDOMAIN="your-instance.example.com"
ZITADEL_EXTERNALSECURE=true
ZITADEL_DATABASE_POSTGRES_HOST=db.example.com
ZITADEL_DATABASE_POSTGRES_PORT=5432
Troubleshooting
| Issue | Solution |
|---|---|
| Console not loading | Check ExternalDomain and ExternalPort configuration |
| Master key error | Key must be exactly 32 characters long |
| OIDC discovery fails | Verify ExternalDomain matches the URL used for discovery |
| Token verification fails | Check audience matches project ID, not client ID |
| Actions not executing | Verify action is assigned to the correct flow and trigger |
| CORS errors | Configure allowed origins in application settings |
| Database migration fails | Ensure admin database credentials have CREATE permissions |
| Login branding not showing | Activate the label policy after configuration |