docs: introduce versioned docs and migrate to fuma (#11166)

## Todos for release

- [x] Configure Env in docs project on vercel
- [x] Configure Root Path in the docs project on vercel
- [ ] Remove old CSP https://github.com/zitadel/website/pull/1592

## What we did

This pull request migrates the project documentation from the old
`docs/` directory to the new `apps/docs/` directory, introduces a new
documentation system built with Next.js and Fumadocs, and updates all
relevant references, configuration files, and documentation to reflect
this change. It also adds new configuration and ignore files for the new
documentation app, updates CI and linting to exclude the new docs from
certain checks, and revises the contributing guidelines accordingly.

**Documentation System Migration and New Docs App**

* Migrated all documentation from `docs/` to `apps/docs/`, and updated
all references in `README.md`, `CONTRIBUTING.md`, and other files to
point to the new location.
[[1]](diffhunk://#diff-eca12c0a30e25b4b46522ebf89465a03ba72a03f540796c979137931d8f92055L585-L640)
[[2]](diffhunk://#diff-eca12c0a30e25b4b46522ebf89465a03ba72a03f540796c979137931d8f92055L660-R607)
[[3]](diffhunk://#diff-eca12c0a30e25b4b46522ebf89465a03ba72a03f540796c979137931d8f92055L740-R687)
[[4]](diffhunk://#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5L2-R3)
[[5]](diffhunk://#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5L30-R30)
* Added a new Next.js/Fumadocs-based documentation app under
`apps/docs/`, including core app files, layouts, routing, search API,
and a comprehensive `README.md` with development and contribution
instructions.
[[1]](diffhunk://#diff-5a1b07344a2c1b4d3f37b23ff1388b62cd9f57dea3c1cd23d8a0412b7602b132R1-R74)
[[2]](diffhunk://#diff-462b9ad1eabbb7d1c29bb9c36e4931eb180fd7190900e6d2babf8f4d66ad1c28R1-R39)
[[3]](diffhunk://#diff-e16ae25660ded787b10ac35dea96d5ecaacf895dae0afc8a9bd4382dc79a8c87R1-R7)
[apps/docs/app/[[...slug]]/layout.tsxR1-R81](diffhunk://#diff-59e08acde4e805b7aeccef1dcf98f1d71dfc550777e6b9402085cee0e9fa4e0aR1-R81),
[apps/docs/app/[[...slug]]/page.tsxR1-R79](diffhunk://#diff-e5df3f80d0fa01e12d63d81f29c57a9d14e846c78fd3beabb2ef768e38fd9580R1-R79),
[[4]](diffhunk://#diff-389b34918e040cacaa87cd7201ffa462cc2b0b716736f537e3d3c660ac69353bR1-R7)
[[5]](diffhunk://#diff-d3b03416d1c457b19f1c27b26f2db741412df841b875c26ccadc36e4522247f4R1-R29)
[[6]](diffhunk://#diff-c8fb8339570a5305809be7c618e14705fd86390dc278e6ce8ba224a7bc8b0c3cR1-R25)

**Configuration and Tooling Updates**

* Updated `.github/workflows/codeql.yml`, `.golangci.yaml`, and
`.github/dependabot.yml` to properly handle the new docs app: excluded
`apps/docs` from certain checks, added npm dependency updates for the
docs app, and excluded generated content.
[[1]](diffhunk://#diff-12783128521e452af0cfac94b99b8d250413c516ec71fe6d97dbea666ff7ba27L8-R14)
[[2]](diffhunk://#diff-9917ddc9f1c3304218f7269265b746d997c5c0615478177b5fceecd33ef47cb5R5-R6)
[[3]](diffhunk://#diff-9917ddc9f1c3304218f7269265b746d997c5c0615478177b5fceecd33ef47cb5R126-R129)
[[4]](diffhunk://#diff-dd4fbda47e51f1e35defb9275a9cd9c212ecde0b870cba89ddaaae65c5f3cd28R89-R106)
* Updated `.devcontainer/devcontainer.json` to use the latest Go 1.25.3
version for consistency.

**Licensing and Miscellaneous**

* Added `apps/docs/` to the list of licensed directories in
`LICENSING.md`.

These changes ensure the documentation is now maintained in a modern,
scalable system and all project tooling is updated to support the new
structure.

---------

Co-authored-by: Federico Coppede <[email protected]>
This commit is contained in:
Florian Forster
2026-01-29 08:38:36 -08:00
committed by GitHub
co-authored by Federico Coppede
parent 7b6395c348
commit d8ea041c56
2826 changed files with 78739 additions and 23571 deletions
+39
View File
@@ -0,0 +1,39 @@
# deps
/node_modules
# generated content
.source
scanned-urls.json
# test & build
/coverage
/.next/
/out/
/build
*.tsbuildinfo
# misc
.DS_Store
*.pem
/.pnp
.pnp.js
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# others
.env*.local
.vercel
next-env.d.ts
# generated api docs
/content/reference/api
/content/reference/api-v1
/openapi/
# multi-version artifacts
/content/versions.json
/content/v*/
/public/v*
/.temp/
/.source
+83
View File
@@ -0,0 +1,83 @@
# @zitadel/docs
Documentation for Zitadel, built with [Fumadocs](https://fumadocs.dev) and [Next.js](https://nextjs.org).
## Getting Started
Ensure you have followed the [root quick start](../../CONTRIBUTING.md#quick-start) to set up dependencies.
### Local Development
Start the development server:
```bash
pnpm nx run @zitadel/docs:dev
```
The site will be available at http://localhost:3000.
### Scripts
Key scripts for documentation workflows:
| Script | Description |
| :--- | :--- |
| `dev` | Starts the development server. |
| `build` | Builds the production application. |
| `fetch:remote-content` | Fetches remote tags and referenced content. |
| `generate` | Runs all generation steps (`fetch:remote-content`, `generate:proto-docs`, `generate:api-reference`, `generate:index-pages`). |
| `check:links` | Validates content integrity (broken links, missing frontmatter, schema errors). |
| `check-types` | Validates typescript types. |
| `test` | Runs all validation steps (`check-types`, `check:links`). |
| `lint` | checks for code style and syntax errors (ESLint). |
| `clean` | Cleans the build output and generated files. |
### Validation
* **Code Quality**: Run `pnpm lint` to check for syntax and style issues in JS/TS/MDX files.
* **Content Integrity**: Run `pnpm check:links` to validate content structure, including:
* Broken internal links
* Missing required frontmatter (e.g., `title`)
* Image references
## Contributing
### Build Process
The docs build process automatically handling the following steps via `generate`:
1. Downloads required protoc plugins.
2. Generates gRPC documentation from proto files.
3. Generates API documentation from OpenAPI specs.
4. Generates index files for directory structures.
### Style Guide
- **Variables**: Use environment variables in code snippets where possible.
- **Embedded Content**: Use `_filename.mdx` for content embedded in other pages (not indexed individually).
- **Code Embedding**: Use the `file` property in code blocks to embed code from the repo.
- **Voice**: Use active voice and sentence case for titles.
Refer to the [Google Developer Style Guide](https://developers.google.com/style) for general guidelines.
### Adding Content
All documentation content is located in the `content` directory. Note that the system strictly accepts **only `.mdx` files**.
To add a new page:
1. Create a `.mdx` file in the appropriate subdirectory of [`content`](./content).
2. Register the new page in the sidebar configuration at [`lib/sidebar-data.ts`](./lib/sidebar-data.ts) to make it accessible in the navigation.
### Pull Requests
Use `docs(<scope>): <short summary>` for PR titles.
Pass quality checks before submitting:
```bash
pnpm nx run @zitadel/docs:build
pnpm nx run @zitadel/docs:check:links
```
You can also run specific steps individually:
* `pnpm fetch:remote-content`
* `pnpm generate:api-reference`
+7
View File
@@ -0,0 +1,7 @@
import { HomeLayout } from 'fumadocs-ui/layouts/home';
import { baseOptions } from '@/lib/layout.shared';
import { ReactNode } from 'react';
export default function Layout({ children }: { children: ReactNode }) {
return <HomeLayout {...baseOptions()}>{children}</HomeLayout>;
}
+81
View File
@@ -0,0 +1,81 @@
import { source, versionSource } from '@/lib/source';
import { ReactNode } from 'react';
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
import { baseOptions } from '@/lib/layout.shared';
import { buildCustomTree } from '@/lib/custom-tree';
import rawVersions from '@/content/versions.json';
const versions = rawVersions as any[];
import { VersionSelector } from '@/components/version-selector';
export default async function Layout(props: { children: ReactNode; params: Promise<{ slug?: string[] }> }) {
const params = await props.params;
const slug = params.slug || [];
// Determine current version and tail path
// If first slug segment matches a version param (e.g. v4.10), that's the version.
// Otherwise it's latest.
const versionParam = slug[0] && versions.find(v => v.param === slug[0]) ? slug[0] : undefined;
const currentVersion = versionParam || 'latest';
const allPages = [...source.getPages(), ...versionSource.getPages()];
const labelsMap = new Map<string, string>();
allPages.forEach(p => {
if (p.data.sidebar_label) {
labelsMap.set(p.url, p.data.sidebar_label);
}
});
let tree = source.pageTree;
if (currentVersion !== 'latest') {
// Hoist the version folder to root to flatten sidebar
// versionSource.pageTree -> [ v4.10 folder, v4.9 folder ... ]
// We want the children of the specific version folder.
const children = (versionSource.pageTree as any).children || [];
const versionFolder = children.find((node: any) => {
if (node.type !== 'folder') return false;
// Check if the folder's index page belongs to this version
if (node.index?.url && node.index.url.includes(`/${currentVersion}`)) return true;
// Fallback: check first child if index missing
if (node.children?.[0]?.url?.includes(`/${currentVersion}`)) return true;
return false;
});
if (versionFolder) {
// Hoist children
const hoistedTree = {
name: versionFolder.name,
children: versionFolder.children,
} as any;
// Apply custom sidebar structure (from local sidebar-data.ts)
// We strip the prefix '/v4.10' so the lookup matches generic keys 'guides/...'
// The logs showed the URLs start with /[version] not /docs/[version]
const prefix = currentVersion === 'latest' ? '/docs' : `/${currentVersion}`;
tree = buildCustomTree(hoistedTree, {
stripPrefix: prefix,
labels: labelsMap
});
} else {
tree = buildCustomTree(versionSource.pageTree, { labels: labelsMap });
}
} else {
tree = buildCustomTree(source.pageTree, { labels: labelsMap });
}
return (
<DocsLayout
tree={tree}
{...baseOptions()}
sidebar={{
banner: (
<VersionSelector />
),
}}
>
{props.children}
</DocsLayout>
);
}
+79
View File
@@ -0,0 +1,79 @@
import { getPageImage, getPage, source } from '@/lib/source';
import {
DocsBody,
DocsDescription,
DocsPage,
DocsTitle,
} from 'fumadocs-ui/layouts/docs/page';
import { notFound } from 'next/navigation';
import { getMDXComponents } from '@/mdx-components';
import type { Metadata } from 'next';
import { createRelativeLink } from 'fumadocs-ui/mdx';
import { Callout } from 'fumadocs-ui/components/callout';
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
import { Feedback } from '@/components/feedback';
export default async function Page(props: any) {
const params = await props.params;
const { page, source: pageSource } = getPage(params.slug);
if (!page) notFound();
const MDX = page.data.body;
return (
<DocsPage toc={page.data.toc} full={page.data.full}>
<DocsTitle>{page.data.title}</DocsTitle>
<DocsDescription>{page.data.description}</DocsDescription>
<DocsBody>
<MDX
components={getMDXComponents({
Callout,
Tab,
Tabs,
// this allows you to link to other pages with relative file paths
a: createRelativeLink(pageSource, page),
})}
/>
</DocsBody>
<Feedback />
</DocsPage>
);
}
export const dynamicParams = true;
export const revalidate = 3600;
export async function generateStaticParams() {
return source.generateParams();
}
export async function generateMetadata(
props: any,
): Promise<Metadata> {
const params = await props.params;
const { page } = getPage(params.slug);
if (!page) notFound();
const baseUrl = 'https://zitadel.com/docs';
const url = params.slug ? `${baseUrl}/${params.slug.join('/')}` : baseUrl;
let canonicalUrl = url;
if (params.slug?.[0]?.startsWith('v')) {
const unversionedSlug = params.slug.slice(1);
const unversionedPage = source.getPage(unversionedSlug);
if (unversionedPage) {
canonicalUrl = `${baseUrl}${unversionedPage.url === '/' ? '' : unversionedPage.url}`;
}
}
return {
title: page.data.title,
description: page.data.description,
alternates: {
canonical: canonicalUrl,
},
openGraph: {
images: getPageImage(page).url,
},
};
}
+7
View File
@@ -0,0 +1,7 @@
import { source } from '@/lib/source';
import { createFromSource } from 'fumadocs-core/search/server';
export const { GET } = createFromSource(source, {
// https://docs.orama.com/docs/orama-js/supported-languages
language: 'english',
});
+408
View File
@@ -0,0 +1,408 @@
@import 'tailwindcss';
@import 'fumadocs-ui/css/neutral.css';
@import 'fumadocs-ui/css/preset.css';
@import 'fumadocs-openapi/css/preset.css';
@theme {
/* 1. REGISTER STANDARD UTILITIES */
--color-background: hsl(var(--background));
--color-foreground: hsl(var(--foreground));
--color-card: hsl(var(--card));
--color-card-foreground: hsl(var(--card-foreground));
--color-popover: hsl(var(--popover));
--color-popover-foreground: hsl(var(--popover-foreground));
--color-primary: hsl(var(--primary));
--color-primary-foreground: hsl(var(--primary-foreground));
--color-secondary: hsl(var(--secondary));
--color-secondary-foreground: hsl(var(--secondary-foreground));
--color-muted: hsl(var(--muted));
--color-muted-foreground: hsl(var(--muted-foreground));
--color-accent: hsl(var(--accent));
--color-accent-foreground: hsl(var(--accent-foreground));
--color-destructive: hsl(var(--destructive));
--color-destructive-foreground: hsl(var(--destructive-foreground));
--color-border: hsl(var(--border));
--color-input: hsl(var(--input));
--color-ring: hsl(var(--ring));
/* 2. MAP FUMADOCS VARIABLES */
--color-fd-primary: var(--color-primary);
--color-fd-primary-foreground: var(--color-primary-foreground);
--color-fd-background: var(--color-background);
--color-fd-foreground: var(--color-foreground);
--color-fd-card: var(--color-card);
--color-fd-card-foreground: var(--color-card-foreground);
--color-fd-popover: var(--color-popover);
--color-fd-popover-foreground: var(--color-popover-foreground);
--color-fd-muted: var(--color-muted);
--color-fd-muted-foreground: var(--color-muted-foreground);
--color-fd-accent: var(--color-accent);
--color-fd-accent-foreground: var(--color-accent-foreground);
--color-fd-border: var(--color-border);
--color-fd-ring: var(--color-ring);
--color-zitadelpink: #ff2069;
--font-sans: var(--font-lato), "Lato", "ui-sans-serif", "system-ui";
}
/* --- LIGHT MODE VARIABLES --- */
:root {
--primary: 230 59% 58%;
--primary-foreground: 0 0% 100%;
--background: 0 0% 98%;
--foreground: 0 0% 10%;
--card: 0 0% 93%;
--card-foreground: 0 0% 10%;
--muted: 0 0% 96%;
--muted-foreground: 0 0% 45%;
--accent: 0 0% 92%;
--accent-foreground: 230 59% 58%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 90%;
--input: 0 0% 90%;
--ring: 230 59% 58%;
--radius: 0.5rem;
--overlaycolor: #6c90b420;
}
/* --- DARK MODE VARIABLES --- */
:root[class~="dark"], [data-theme="dark"] {
--primary: 340 100% 56%;
--primary-foreground: 0 0% 100%;
--background: 235 45% 14%;
--foreground: 0 0% 98%;
--card: 220 40% 25%;
--card-foreground: 0 0% 98%;
--muted: 230 43% 15%;
--muted-foreground: 230 20% 70%;
--accent: 230 43% 20%;
--accent-foreground: 340 100% 65%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 230 43% 25%;
--input: 230 43% 25%;
--ring: 340 100% 56%;
--overlaycolor: #ffffff15;
/* FORCE OVERRIDE for Fumadocs Variables */
--color-fd-background: hsl(var(--background));
--color-fd-foreground: hsl(var(--foreground));
--color-fd-primary: hsl(var(--primary));
--color-fd-primary-foreground: hsl(var(--primary-foreground));
--color-fd-card: hsl(var(--card));
--color-fd-card-foreground: hsl(var(--card-foreground));
--color-fd-popover: hsl(var(--card));
--color-fd-popover-foreground: hsl(var(--foreground));
--color-fd-muted: hsl(var(--muted));
--color-fd-muted-foreground: hsl(var(--muted-foreground));
--color-fd-accent: hsl(var(--accent));
--color-fd-accent-foreground: hsl(var(--accent-foreground));
--color-fd-border: hsl(var(--border));
--color-fd-ring: hsl(var(--ring));
}
@layer components {
/* 1. VISIBILITY HELPERS */
.hideondark { display: block !important; }
.hideonlight { display: none !important; }
:root[class~="dark"] .hideondark, [data-theme="dark"] .hideondark {
display: none !important;
}
:root[class~="dark"] .hideonlight, [data-theme="dark"] .hideonlight {
display: block !important;
}
/* 2. MAIN CARD LAYOUT (Tile Wrapper) */
.tile-wrapper {
@apply flex flex-row flex-wrap gap-6 my-6;
}
.tile-wrapper > * {
@apply flex flex-col items-center text-center rounded-lg border relative;
background-color: hsl(var(--card)) !important;
border-color: hsl(var(--border));
justify-content: flex-start;
min-width: 160px;
min-height: 180px;
padding: 2rem;
padding-bottom: 3rem;
}
.tile-wrapper > * > :last-child {
margin-top: auto;
}
.tile-wrapper > * > h3,
.tile-wrapper > * > h4,
.tile-wrapper > * > strong {
@apply mt-0;
}
/* 3. ICON STYLING */
.custom-rounded {
@apply flex items-center justify-center p-2 rounded-full w-14 h-14 shrink-0 m-0;
}
.custom-rounded svg {
@apply fill-white h-7 w-7;
}
.custom-rounded i {
@apply text-white;
}
/* Gradients */
.custom-rounded-start { background: linear-gradient(40deg, #059669 30%, #047857); }
.custom-rounded-password { background: linear-gradient(40deg, #f59e0b 30%, #b45309); }
.custom-rounded-login, .custom-rounded-register { background: linear-gradient(40deg, #059669 30%, #047857); }
.custom-rounded-privatelabel, .custom-rounded-phone, .custom-rounded-email, .custom-rounded-storage, .custom-rounded-service { background: linear-gradient(40deg, #ff2069 30%, #cc1a54); }
.custom-rounded-split { background: linear-gradient(40deg, #ff2069, #cc1a54); }
.custom-rounded-texts, .custom-rounded-help, .custom-rounded-architecture { background: linear-gradient(40deg, #dc2626 30%, #db2777); }
.custom-rounded-system, .custom-rounded-apis, .custom-rounded-policy, .custom-rounded-instance { background: linear-gradient(40deg, #1f2937, #111827); }
/* 4. B2B / DIAGRAM LAYOUTS (Fixes the B2B Component) */
.flexrowbetween {
@apply flex flex-row items-center justify-around gap-4 my-8 w-full;
}
.arrowright {
@apply w-8 h-8 shrink-0 text-muted-foreground;
}
.b2borg, .b2bproject, .b2bapp, .b2buser {
@apply rounded-xl p-4 border text-left;
background: var(--overlaycolor);
border-color: hsl(var(--border));
}
.b2buser { @apply mt-4; }
.b2borg span, .b2bproject span, .b2bapp span, .b2buser span {
@apply ml-4 mb-2 block font-semibold;
}
/* Specific styling for the role text */
.b2bprojectrole {
@apply mt-2 ml-4 block text-sm opacity-80 font-normal;
}
/* 5. FOOTER / PAGINATION CARDS */
a.\@max-lg\:col-span-full {
@apply flex flex-col p-6 rounded-xl border transition-all no-underline;
background-color: hsl(var(--card)) !important;
border-color: hsl(var(--border)) !important;
}
a.\@max-lg\:col-span-full:hover {
@apply shadow-sm;
border-color: hsl(var(--primary)) !important;
background-color: hsl(var(--card)) !important;
}
a.\@max-lg\:col-span-full > div:first-child {
@apply flex items-center gap-3;
}
a.\@max-lg\:col-span-full svg {
@apply w-5 h-5 m-0;
}
a.\@max-lg\:col-span-full p {
margin-bottom: 0 !important;
margin-top: 0 !important;
}
a.\@max-lg\:col-span-full p.text-fd-muted-foreground {
@apply mt-2 text-sm opacity-80;
}
/* 6. VIDEO PLAYER WRAPPER */
/* Fixes aspect ratio for ReactPlayer embeds */
.player-wrapper {
@apply relative mb-8;
padding-top: 56.25%; /* 16:9 Aspect Ratio */
}
.react-player {
@apply absolute top-0 left-0;
}
/* 7. GET STARTED BUTTON */
.get-started {
@apply inline-flex items-center justify-center px-6 py-2 rounded-full font-bold border-2 transition-all;
border-color: #ff2069;
background-color: #ff2069;
color: white;
}
.get-started:hover {
@apply opacity-90;
color: white !important;
}
/* Target any div that uses the callout color variable */
div[style*="--callout-color"] {
@apply rounded-lg shadow-sm mb-4;
/* Force Docusaurus Layout: Left Border Only */
border: 1px solid rgba(0,0,0,0.05) !important;
border-left-width: 4px !important;
/* Hide the default Fumadocs vertical colored bar inside */
& > div[role="none"] {
display: none !important;
}
}
/* --- INFO (Blue) --- */
/* We match the variable name in the style string */
div[style*="--color-fd-info"] {
background-color: #eff6ff !important;
border-left-color: #3b82f6 !important;
color: #1e3a8a !important;
}
div[style*="--color-fd-info"] svg {
color: #3b82f6 !important;
fill: transparent !important; /* Remove fill to look cleaner */
}
/* Dark Mode Info */
:is(.dark div[style*="--color-fd-info"]) {
background-color: rgba(59, 130, 246, 0.1) !important;
color: #dbeafe !important;
border-top-color: rgba(255,255,255,0.05) !important;
border-right-color: rgba(255,255,255,0.05) !important;
border-bottom-color: rgba(255,255,255,0.05) !important;
}
/* --- WARNING (Amber) --- */
div[style*="--color-fd-warn"] {
background-color: #fffbeb !important;
border-left-color: #f59e0b !important;
color: #78350f !important;
}
div[style*="--color-fd-warn"] svg {
color: #f59e0b !important;
fill: transparent !important;
}
/* Dark Mode Warning */
:is(.dark div[style*="--color-fd-warn"]) {
background-color: rgba(245, 158, 11, 0.1) !important;
color: #fef3c7 !important;
border-top-color: rgba(255,255,255,0.05) !important;
border-right-color: rgba(255,255,255,0.05) !important;
border-bottom-color: rgba(255,255,255,0.05) !important;
}
/* --- ERROR (Red) --- */
div[style*="--color-fd-error"] {
background-color: #fef2f2 !important;
border-left-color: #ef4444 !important;
color: #7f1d1d !important;
}
div[style*="--color-fd-error"] svg {
color: #ef4444 !important;
fill: transparent !important;
}
/* Dark Mode Error */
:is(.dark div[style*="--color-fd-error"]) {
background-color: rgba(239, 68, 68, 0.12) !important;
color: #fee2e2 !important;
border-top-color: rgba(255,255,255,0.05) !important;
border-right-color: rgba(255,255,255,0.05) !important;
border-bottom-color: rgba(255,255,255,0.05) !important;
}
/* --- TITLE BOLD --- */
div[style*="--callout-color"] strong,
div[style*="--callout-color"] p:first-child strong {
font-weight: 700 !important;
}
}
@layer base {
/* HEADINGS */
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-sans);
color: hsl(var(--foreground)) !important;
margin-bottom: 1rem;
letter-spacing: -0.025em;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1 a, h2 a, h3 a, h4 a, h5 a, h6 a {
color: inherit !important;
text-decoration: none !important;
}
h1 {
font-size: 3.5rem !important;
font-weight: 700 !important;
line-height: 1.1 !important;
margin-bottom: 1.5rem !important;
}
h2 {
font-size: 2.25rem !important;
font-weight: 600 !important;
margin-top: 3rem !important;
margin-bottom: 1rem !important;
padding-bottom: 0.5rem !important;
border-bottom: 1px solid hsl(var(--border)) !important;
}
h3 {
font-size: 2rem !important;
font-weight: 600 !important;
margin-top: 2.5rem !important;
margin-bottom: 1rem !important;
}
h4 {
font-size: 1.5rem !important;
font-weight: 600 !important;
margin-top: 2.5rem !important;
margin-bottom: 1rem !important;
}
p {
font-size: 1rem;
line-height: 1.7;
margin-bottom: 1.5rem;
}
html, body {
background-color: hsl(var(--background));
color: hsl(var(--foreground));
}
a {
color: hsl(var(--primary));
text-decoration: none;
transition: opacity 0.2s;
}
a:hover {
opacity: 0.8;
}
}
+29
View File
@@ -0,0 +1,29 @@
import { type BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
import { BookOpen, Code2, Server } from 'lucide-react';
export const baseOptions: BaseLayoutProps = {
nav: {
title: 'ZITADEL Docs',
},
links: [
{
text: 'Guides',
url: '/',
active: 'nested-url',
icon: <BookOpen />,
},
{
text: 'APIs',
url: '/apis/introduction',
active: 'nested-url',
icon: <Code2 />,
},
{
text: 'Legal',
url: '/legal/terms-of-service',
active: 'nested-url',
icon: <Server />, // Using Server icon as placeholder or pick another one
},
],
githubUrl: 'https://github.com/zitadel/zitadel',
};
+25
View File
@@ -0,0 +1,25 @@
import './global.css';
import type { Metadata } from 'next';
import { Providers } from './providers';
export const metadata: Metadata = {
metadataBase: new URL(process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'),
icons: {
other: [
{
rel: 'stylesheet',
url: '/docs/img/icons/line-awesome/css/line-awesome.min.css',
},
],
},
};
export default function Layout({ children }: any) {
return (
<html lang="en" suppressHydrationWarning>
<body className="flex flex-col min-h-screen font-sans bg-fd-background text-fd-foreground">
<Providers>{children}</Providers>
</body>
</html>
);
}
+10
View File
@@ -0,0 +1,10 @@
import { getLLMText, source } from '@/lib/source';
export const revalidate = false;
export async function GET() {
const scan = source.getPages().map(getLLMText);
const scanned = await Promise.all(scan);
return new Response(scanned.join('\n\n'));
}
+28
View File
@@ -0,0 +1,28 @@
import { ImageResponse } from 'next/og';
import { notFound } from 'next/navigation';
import { source } from '@/lib/source';
import { generate as DefaultImage } from 'fumadocs-ui/og';
export const revalidate = false;
export async function GET(request: Request, context: any) {
const { slug } = context.params;
const page = source.getPage(slug.slice(0, -1));
if (!page) notFound();
return new ImageResponse(
<DefaultImage
title={page.data.title}
description={page.data.description}
site="My App"
/>,
{
width: 1200,
height: 630,
},
);
}
export function generateStaticParams() {
return [];
}
+26
View File
@@ -0,0 +1,26 @@
'use client';
import { RootProvider } from 'fumadocs-ui/provider/next';
import dynamic from "next/dynamic";
import { ThemeProvider } from 'next-themes';
import MixpanelProvider from '@/components/mixpanel-provider';
import PlausibleProvider from '@/components/plausible-provider';
const SearchDialog = dynamic(() => import("@/components/inkeep-search"));
export function Providers({ children }: { children: React.ReactNode }) {
return (
<RootProvider
search={{
enabled: true,
SearchDialog: SearchDialog as any,
}}
>
<PlausibleProvider />
<MixpanelProvider>
{children}
</MixpanelProvider>
</RootProvider>
);
}
+27
View File
@@ -0,0 +1,27 @@
import { MetadataRoute } from 'next';
import { source } from '@/lib/source';
export default function sitemap(): MetadataRoute.Sitemap {
const baseUrl = 'https://zitadel.com/docs';
const docsPages = source.getPages().map((page) => ({
url: `${baseUrl}${page.url === '/' ? '' : page.url}`,
lastModified: new Date(),
changeFrequency: 'weekly' as const,
priority: 0.8,
}));
return [
{
url: baseUrl,
lastModified: new Date(),
changeFrequency: 'daily' as const,
priority: 1,
},
...docsPages,
].filter((item, index, self) =>
index === self.findIndex((t) => t.url === item.url)
);
}
+6
View File
@@ -0,0 +1,6 @@
version: v2
plugins:
- remote: buf.build/community/sudorandom-connect-openapi:v0.19.1
out: .
opt:
- format=json
@@ -0,0 +1,111 @@
import React, { useMemo } from 'react';
import yaml from 'js-yaml';
import { Check, X } from 'lucide-react';
import { cn } from '@/utils/cn'; // Assuming utils/cn exists or I'll use clsx/tailwind-merge directly if not sure
// Fallback utility if generic cn doesn't exist, checking imports first is safer but I'll inline a simple merge if needed
// Checking package.json I saw clsx and tailwind-merge are dependencies.
function classNames(...classes) {
return classes.filter(Boolean).join(' ');
}
export default function PermissionTable({ data }) {
const { roles, permissions, matrix } = useMemo(() => {
if (!data) return { roles: [], permissions: [], matrix: {} };
let parsedData = data;
if (typeof data === 'string') {
try {
parsedData = yaml.load(data);
} catch (e) {
console.error('Failed to parse YAML data:', e);
return { roles: [], permissions: [], matrix: {} };
}
}
// 1. Identify all sources.
const sources = [
parsedData?.InternalAuthZ?.RolePermissionMappings,
parsedData?.SystemAuthZ?.RolePermissionMappings,
].filter(Boolean).flat();
// 2. Aggregate Data
const roleMap = new Map();
const allPermissions = new Set();
sources.forEach((mapping) => {
const roleName = mapping.Role;
const rolePerms = mapping.Permissions || [];
if (!roleMap.has(roleName)) {
roleMap.set(roleName, new Set());
}
rolePerms.forEach((perm) => {
roleMap.get(roleName).add(perm);
allPermissions.add(perm);
});
});
// 3. Sort for display
const sortedRoles = Array.from(roleMap.keys()).sort();
const sortedPermissions = Array.from(allPermissions).sort();
return {
roles: sortedRoles,
permissions: sortedPermissions,
matrix: roleMap,
};
}, [data]);
if (!roles.length) {
return <div className="p-4 text-sm text-fd-muted-foreground bg-fd-muted rounded-md border border-fd-border">No permission data available.</div>;
}
return (
<div className="overflow-x-auto my-6 rounded-lg border border-fd-border bg-fd-card shadow-sm">
<table className="w-full text-sm text-left">
<thead className="text-xs font-semibold text-fd-muted-foreground uppercase bg-fd-muted/50 border-b border-fd-border">
<tr>
<th scope="col" className="px-4 py-3 sticky left-0 z-10 bg-fd-muted/95 backdrop-blur shadow-[1px_0_0_0_rgba(0,0,0,0.1)] dark:shadow-[1px_0_0_0_rgba(255,255,255,0.1)]">
Permission
</th>
{roles.map((role) => (
<th key={role} scope="col" className="px-4 py-3 whitespace-nowrap text-center min-w-[100px]">
{role.replace(/_/g, ' ')}
</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-fd-border">
{permissions.map((perm) => (
<tr key={perm} className="hover:bg-fd-accent/50 transition-colors">
<th scope="row" className="px-4 py-2 font-medium text-fd-foreground whitespace-nowrap sticky left-0 z-10 bg-fd-card shadow-[1px_0_0_0_rgba(0,0,0,0.1)] dark:shadow-[1px_0_0_0_rgba(255,255,255,0.1)]">
{perm}
</th>
{roles.map((role) => {
const hasPermission = matrix.get(role).has(perm);
return (
<td
key={`${role}-${perm}`}
className={classNames(
"px-4 py-2 text-center",
hasPermission ? "bg-green-50/50 dark:bg-green-900/10" : ""
)}
>
{hasPermission ? (
<Check className="w-4 h-4 text-green-600 dark:text-green-400 mx-auto" strokeWidth={3} />
) : (
<span className="sr-only">No</span>
)}
</td>
);
})}
</tr>
))}
</tbody>
</table>
</div>
);
}
+4
View File
@@ -0,0 +1,4 @@
'use client';
import { defineClientConfig } from 'fumadocs-openapi/ui/client';
export default defineClientConfig();
+27
View File
@@ -0,0 +1,27 @@
import { createOpenAPI } from 'fumadocs-openapi/server';
import { createAPIPage } from 'fumadocs-openapi/ui';
import path from 'path';
import client from './api-page.client';
export function APIPage({ document, operations }: { document: string; operations: any[] }) {
if (!path.isAbsolute(document) && !document.startsWith('openapi/')) {
throw new Error(`APIPage document path MUST start with 'openapi/'. Received: ${document}`);
}
const absoluteDocument = path.isAbsolute(document)
? document
: path.join(process.cwd(), 'openapi', document.slice('openapi/'.length));
const openapi = createOpenAPI({
input: [absoluteDocument],
});
const InnerAPIPage = createAPIPage(openapi, {
client,
});
return <InnerAPIPage document={absoluteDocument} operations={operations} />;
}
+32
View File
@@ -0,0 +1,32 @@
import React from "react";
import styles from "@/css/apicard.module.css";
export function ApiCard({ type, children }) {
let classes = "";
switch (type) {
case "AUTH":
classes = "bg-green-500/10 dark:bg-green-500/20";
break;
case "MGMT":
classes = "bg-blue-500/10 dark:bg-blue-500/20";
break;
case "ADMIN":
classes = "bg-red-500/10 dark:bg-red-500/20";
break;
case "SYSTEM":
classes = "bg-yellow-500/10 dark:bg-yellow-500/20";
break;
case "ASSET":
classes = "bg-black/10 dark:bg-black/20";
break;
}
return (
<div
className={`${styles.apicard} flex mb-4 flex-row p-4 rounded-lg ${classes} `}
>
{children}
</div>
);
}
+47
View File
@@ -0,0 +1,47 @@
import React from 'react';
export function B2B() {
return (
<div className="flexrowbetween">
<div className="b2borg">
<span>Octagon <small>(owner)</small></span>
<div className="b2bproject">
<span>Portal Project</span>
<div className="b2bapp">
<strong>WEBAPP</strong>
</div>
<span className="b2bprojectrole">reader, writer, admin</span>
</div>
<div className="b2buser">
Bill <small>(admin)</small>
</div>
</div>
<svg className="arrowright" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M14 5l7 7m0 0l-7 7m7-7H3"></path></svg>
<div className="b2borg">
<span>Pentagon</span>
<div className="b2bproject">
<span>Portal Project <small>(grant)</small></span>
<div className="b2bapp">
<strong>WEBAPP</strong>
</div>
<span className="b2bprojectrole">reader, writer</span>
</div>
<div className="b2buser">
Dimitri <small>(writer)</small>
</div>
<div className="b2buser">
Michael <small>(reader)</small>
</div>
</div>
</div>
);
}
+78
View File
@@ -0,0 +1,78 @@
"use client";
import React from "react";
import Chart from "react-google-charts";
export function BenchmarkChart({ testResults = [], height = '500px' } = {}) {
if (!Array.isArray(testResults)) {
console.error("BenchmarkChart: testResults is not an array. Received:", testResults);
return <p>Error: Benchmark data is not available or in the wrong format.</p>;
}
if (testResults.length === 0) {
return <p>No benchmark data to display.</p>;
}
const dataPerMetric = new Map();
let maxVValue = 0;
testResults.forEach((result) => {
if (!result || typeof result.metric_name === 'undefined') {
console.warn("BenchmarkChart: Skipping invalid result item:", result);
return;
}
if (!dataPerMetric.has(result.metric_name)) {
dataPerMetric.set(result.metric_name, [
[
{type:"datetime", label: "timestamp"},
{type:"number", label: "p50"},
{type:"number", label: "p95"},
{type:"number", label: "p99"},
],
]);
}
if (result.p99 !== undefined && result.p99 > maxVValue) {
maxVValue = result.p99;
}
dataPerMetric.get(result.metric_name).push([
result.timestamp ? new Date(result.timestamp) : null,
result.p50,
result.p95,
result.p99,
]);
});
const options = {
legend: { position: 'bottom' },
focusTarget: 'category',
hAxis: {
title: 'timestamp',
},
vAxis: {
title: 'latency (ms)',
maxValue: maxVValue > 0 ? maxVValue : undefined,
},
title: ''
};
const charts = [];
dataPerMetric.forEach((data, metric) => {
const opt = { ...options };
opt.title = metric;
charts.push(
<Chart
key={metric}
chartType="LineChart"
width="100%"
height={height}
options={opt}
data={data}
legendToggle
/>
);
});
if (charts.length === 0) {
return <p>No chart data could be generated.</p>;
}
return <>{charts}</>;
}
+7
View File
@@ -0,0 +1,7 @@
import React from "react";
export default function Column({ children }) {
return (
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">{children}</div>
);
}
@@ -0,0 +1,19 @@
import { Callout } from 'fumadocs-ui/components/callout';
export default function Admonition({ type, title, children, icon }: any) {
let calloutType: 'info' | 'warn' | 'error' = 'info';
switch (type) {
case 'note': calloutType = 'info'; break;
case 'tip': calloutType = 'info'; break;
case 'info': calloutType = 'info'; break;
case 'warning': calloutType = 'warn'; break;
case 'danger': calloutType = 'error'; break;
case 'caution': calloutType = 'warn'; break;
}
return (
<Callout title={title} type={calloutType} icon={icon}>
{children}
</Callout>
);
}
@@ -0,0 +1,12 @@
"use client";
import { useEffect, useState } from 'react';
export default function BrowserOnly({ children, fallback }: any) {
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
if (!mounted) return fallback || null;
return children();
}
@@ -0,0 +1,14 @@
import React from 'react';
export default function CodeBlock({ children, language, title, className, showLineNumbers: _showLineNumbers }: any) {
return (
<div className="my-4">
{title && <div className="bg-secondary text-secondary-foreground px-4 py-2 text-sm font-medium rounded-t-md">{title}</div>}
<pre className={`p-4 overflow-x-auto bg-secondary/50 rounded-b-md ${title ? 'rounded-t-none' : 'rounded-md'} ${className || ''}`}>
<code className={language ? `language-${language}` : ''}>
{children}
</code>
</pre>
</div>
);
}
@@ -0,0 +1,30 @@
import React from 'react';
import { Cards, Card } from 'fumadocs-ui/components/card';
export default function DocCardList({ items }: { items?: any[] }) {
// If items are provided (manual list), render them
if (items) {
return (
<Cards>
{items.map((item, index) => (
<Card key={index} href={item.href} title={item.label} />
))}
</Cards>
);
}
// If no items, it's likely an auto-generated list for the current sidebar category.
// Fumadocs doesn't have a direct equivalent to "current sidebar category items" in a component easily accessible here without context.
// For now, we'll render a placeholder or nothing to avoid build errors.
// In a real migration, we'd use `useTreeContext` or similar if available, or rely on Fumadocs' index page generation.
return (
<div className="admonition admonition-info alert alert--info">
<div className="admonition-heading">
<h5>DocCardList Placeholder</h5>
</div>
<div className="admonition-content">
<p>Auto-generated card lists are not yet fully supported in the migration.</p>
</div>
</div>
);
}
+58
View File
@@ -0,0 +1,58 @@
import { Tabs as FumadocsTabs, Tab as FumadocsTab } from 'fumadocs-ui/components/tabs';
import React, { isValidElement } from 'react';
export function TabItem({ value, label: _label, children, default: _isDefault, className, attributes }: any) {
return <FumadocsTab value={value} className={className} {...attributes}>{children}</FumadocsTab>;
}
export default function Tabs({ children, defaultValue, values, groupId, className }: any) {
let items: string[] = [];
const childrenArray = React.Children.toArray(children);
if (values) {
items = values.map((v: any) => v.label);
} else {
childrenArray.forEach((child) => {
if (isValidElement(child)) {
const props = child.props as any;
const label = props.label || props.value;
items.push(label);
}
});
}
const newChildren = childrenArray.map((child, index) => {
if (isValidElement(child)) {
const props = child.props as any;
let label = props.label;
if (!label && values) {
const valObj = values.find((v: any) => v.value === props.value);
if (valObj) label = valObj.label;
}
if (!label) label = props.value;
return <FumadocsTab key={index} value={label} {...props}>{props.children}</FumadocsTab>;
}
return child;
});
let newDefaultValue = defaultValue;
if (defaultValue && values) {
const valObj = values.find((v: any) => v.value === defaultValue);
if (valObj) newDefaultValue = valObj.label;
} else if (defaultValue) {
const child = childrenArray.find((c: any) => (c as any).props.value === defaultValue);
if (child) {
newDefaultValue = (child as any).props.label || (child as any).props.value;
}
}
return (
<FumadocsTabs items={items} defaultValue={newDefaultValue} groupId={groupId} className={className}>
{newChildren}
</FumadocsTabs>
);
}
@@ -0,0 +1,17 @@
"use client";
import Image from 'next/image';
import { useTheme } from 'next-themes';
import { useEffect, useState } from 'react';
export default function ThemedImage({ sources, alt, ...props }: any) {
const { resolvedTheme } = useTheme();
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
if (!mounted) return <Image src={sources.light} alt={alt} {...props} />;
const src = resolvedTheme === 'dark' ? sources.dark : sources.light;
return <Image src={src} alt={alt} {...props} />;
}
+56
View File
@@ -0,0 +1,56 @@
'use client';
import { useState } from 'react';
import { mixpanelClient } from '@/utils/mixpanel';
import { usePathname } from 'next/navigation';
import { ThumbsUp, ThumbsDown } from 'lucide-react';
import { cn } from '@/utils/cn';
export function Feedback() {
const pathname = usePathname();
const [voted, setVoted] = useState<boolean | null>(null);
const handleVote = (useful: boolean) => {
setVoted(useful);
mixpanelClient.track('helpfulness_vote', {
path: pathname,
useful: useful,
});
};
if (voted !== null) {
return (
<div className="flex flex-col gap-2 border-t pt-8 mt-8">
<p className="text-sm text-fd-muted-foreground">
Thanks for your feedback!
</p>
</div>
);
}
return (
<div className="flex flex-col gap-2 border-t pt-8 mt-8">
<p className="text-sm text-fd-muted-foreground">Was this page helpful?</p>
<div className="flex gap-2">
<button
onClick={() => handleVote(true)}
className={cn(
"p-2 rounded-md transition-colors border hover:bg-fd-accent hover:text-fd-accent-foreground border-fd-border text-fd-muted-foreground"
)}
aria-label="Helpful"
>
<ThumbsUp className="size-4" />
</button>
<button
onClick={() => handleVote(false)}
className={cn(
"p-2 rounded-md transition-colors border hover:bg-fd-accent hover:text-fd-accent-foreground border-fd-border text-fd-muted-foreground"
)}
aria-label="Not helpful"
>
<ThumbsDown className="size-4" />
</button>
</div>
</div>
);
}
+24
View File
@@ -0,0 +1,24 @@
import React from "react";
import { Tile } from "./tile";
import frameworks from "../frameworks.json";
export function Frameworks({ filter }) {
const filteredFrameworks = frameworks.filter((framework) => {
return filter ? filter(framework) : true;
});
return (
<div className="tile-wrapper">
{filteredFrameworks.map((framework) => (
<Tile
key={framework.id || framework.title}
title={framework.title}
imageSource={framework.imgSrcDark}
imageSourceLight={framework.imgSrcLight}
link={framework.docsLink}
external={framework.external}
></Tile>
))}
</div>
);
}
+71
View File
@@ -0,0 +1,71 @@
"use client";
import type { SharedProps } from "fumadocs-ui/components/dialog/search";
import {
InkeepModalSearchAndChat,
type InkeepModalSearchAndChatProps,
} from "@inkeep/cxkit-react";
import { useEffect, useState } from "react";
export default function CustomDialog(props: SharedProps) {
const [syncTarget, setSyncTarget] = useState<HTMLElement | null>(null);
const { open, onOpenChange } = props;
// We do this because document is not available in the server
useEffect(() => {
setSyncTarget(document.documentElement);
}, []);
const config: InkeepModalSearchAndChatProps = {
baseSettings: {
apiKey: process.env.NEXT_PUBLIC_INKEEP_API_KEY,
primaryBrandColor: '#425b76',
organizationDisplayName: "Zitadel",
colorMode: {
sync: {
target: syncTarget,
attributes: ["class"],
isDarkMode: (attributes) => !!attributes.class?.includes("dark"),
},
},
},
modalSettings: {
isOpen: open,
onOpenChange,
// optional settings
},
searchSettings: {
// optional settings
},
aiChatSettings: {
aiAssistantName: 'Zitadel AI Assistant',
chatSubjectName: 'Zitadel',
placeholder: 'How can I enable MFA?',
introMessage: 'Hey! I’m the Zitadel AI assistant 🤖 — throw your questions my way!',
aiAssistantAvatar: '/icons/favicon-32x32.png',
userAvatar: '/icons/user.png',
exampleQuestionsLabel: 'Example Questions:',
exampleQuestions: [
'What is an Instance?',
'What is an Organization?',
'How to create an Action?',
'How to integrate Zitadel to my React app?',
],
isFirstExampleQuestionHighlighted: true,
shouldOpenLinksInNewTab: true,
isCopyChatButtonVisible: true,
disclaimerSettings: {
isEnabled: true,
label: 'AI Assistant',
tooltip: 'Responses are AI-generated and may require verification.',
},
toolbarButtonLabels: {
clear: 'Start Over',
share: 'Share Chat',
getHelp: 'Get Help',
stop: 'Stop',
copyChat: 'Copy Chat',
},
},
};
return <InkeepModalSearchAndChat {...config} />;
}
@@ -0,0 +1,51 @@
'use client';
import { initMixpanel, mixpanelClient, optInTracking, optOutTracking } from '@/utils/mixpanel';
import React, { useEffect } from 'react';
export default function MixpanelProvider({ children }: { children: React.ReactNode }) {
useEffect(() => {
const setup = async () => {
await initMixpanel();
const CookieConsent = await import('vanilla-cookieconsent');
const cookie = CookieConsent.getCookie();
// @ts-ignore
if (!cookie || !cookie.services || !cookie.services.analytics) {
// We only send data when people opt-in, not just if they have not opted out.
return;
}
// @ts-ignore
const hasMixpanelConsent = cookie?.services?.analytics?.indexOf('mixpanel') !== -1;
if (hasMixpanelConsent) {
optInTracking();
} else {
optOutTracking();
}
};
setup().catch((error) => {
console.error('Error setting up Mixpanel:', error);
});
const handleConsentChange = async () => {
const CookieConsent = await import('vanilla-cookieconsent');
const cookie = CookieConsent.getCookie();
// @ts-ignore
const hasMixpanelConsent = cookie?.services?.analytics?.indexOf('mixpanel') !== -1;
if (hasMixpanelConsent) {
optInTracking();
} else {
optOutTracking();
}
};
window.addEventListener('cc:onChange:mixpanel', handleConsentChange);
return () => {
window.removeEventListener('cc:onChange:mixpanel', handleConsentChange);
};
}, []);
return <>{children}</>;
}
+114
View File
@@ -0,0 +1,114 @@
import React from "react";
export function PiiTable() {
const pii = [
{
type: "Basic data",
examples: [
'Names',
'Email addresses',
'User names'
],
subjects: "All users as uploaded by Customer."
},
{
type: "Login data",
examples: [
'Randomly generated ID',
'Passwords',
'Public keys / certificates ("FIDO2", "U2F", "x509", ...)',
'User names or identifiers of external login providers',
'Phone numbers',
],
subjects: "All users as uploaded and feature use by Customer."
},
{
type: "Profile data",
examples: [
'Profile pictures',
'Gender',
'Languages',
'Nicknames or Display names',
'Phone numbers',
'Metadata'
],
subjects: "All users as uploaded by Customer"
},
{
type: "Communication data",
examples: [
'Emails',
'Chats',
'Call metadata',
'Call recording and transcripts',
'Form submissions',
],
subjects: "Customers and users who communicate with us directly (e.g. support, chat)."
},
{
type: "Payment data",
examples: [
'Billing address',
'Payment information',
'Customer number',
'Support Customer history',
'Credit rating information',
],
subjects: "Customers who use services that require payment. Credit rating information: Only customers who pay by invoice."
},
{
type: "Analytics data",
examples: [
'Usage metrics',
'User behavior',
'User journeys (eg, Milestones)',
'Telemetry data',
'Client-side anonymized session replay',
],
subjects: "Customers who use our services."
},
{
type: "Usage metadata",
examples: [
'User agent',
'IP addresses',
'Operating system',
'Time and date',
'URL',
'Referrer URL',
'Accepted Language',
],
subjects: "All users"
},
]
return (
<table className="text-xs">
<thead>
<tr>
<th>Type of personal data</th>
<th>Examples</th>
<th>Affected data subjects</th>
</tr>
</thead>
<tbody>
{
pii.map((row) => (
<tr key={row.type}>
<td>{row.type}</td>
<td>
<ul>
{row.examples.map((example) => (
<li key={example}>{example}</li>
))}
</ul>
</td>
<td>{row.subjects}</td>
</tr>
))
}
</tbody>
</table>
);
}
@@ -0,0 +1,17 @@
'use client';
import Script from 'next/script';
export default function PlausibleProvider() {
const domain = process.env.NEXT_PUBLIC_PLAUSIBLE_DOMAIN || 'zitadel.com';
return (
<Script
defer
data-domain={domain}
data-api="/docs/pl/api/event"
src="/docs/pl/js/script.js"
strategy="afterInteractive"
/>
);
}
+37
View File
@@ -0,0 +1,37 @@
"use client";
import React from "react";
export function ResponsivePlayer({ url }) {
const getYouTubeId = (url) => {
if (!url) return null;
const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/;
const match = url.match(regExp);
return (match && match[2].length === 11) ? match[2] : null;
};
const videoId = getYouTubeId(url);
if (videoId) {
return (
/* 1. Removed 'player-wrapper' class to avoid global.css conflict (double height).
2. Reduced max-width to 'max-w-2xl' so it fits better in the text flow.
*/
<div className="relative w-full max-w-2xl mx-auto h-0 pb-[56.25%] mb-8 rounded-lg overflow-hidden border border-zinc-200 dark:border-zinc-800 bg-black shadow-md">
<iframe
src={`https://www.youtube.com/embed/${videoId}`}
title="YouTube video player"
className="absolute top-0 left-0 w-full h-full"
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
/>
</div>
);
}
return (
<div className="p-4 mb-8 border border-red-200 bg-red-50 text-red-600 rounded-lg text-sm">
Video source not supported: {url}
</div>
);
}
+35
View File
@@ -0,0 +1,35 @@
import React from "react";
import styles from '@/css/tile.module.css';
export function Tile({ title, imageSource, imageSourceLight, link, external }) {
return (
<a
href={link}
className={styles.tile}
target={external ? "_blank" : "_self"}
>
<h3>{title}</h3>
<img
className={imageSourceLight ? "hideonlight" : ""}
src={imageSource}
alt={title}
width={70}
height={70}
/>
{imageSourceLight && (
<img
className={imageSourceLight ? "hideondark" : ""}
src={imageSourceLight}
alt={title}
width={70}
height={70}
/>
)}
{external && (
<div className={styles.external}>
<i className="las la-external-link-alt"></i>
</div>
)}
</a>
);
}
+93
View File
@@ -0,0 +1,93 @@
'use client';
import * as React from 'react';
import { usePathname, useRouter } from 'next/navigation';
interface DocVersion {
param: string;
label: string;
url: string;
ref: string;
refType: string;
type?: 'external';
}
import rawVersions from '../content/versions.json';
const versions = rawVersions as DocVersion[];
export function VersionSelector() {
const pathname = usePathname();
const router = useRouter();
// Handle basePath issues: usePathname() typically returns path relative to basePath if configured?
// But purely relying on that is tricky. Let's normalize.
// If pathname starts with /docs, assume it's the full path and we strip it for logic,
// then let router handle re-adding it if needed (or we check behaviors).
// Actually, Next.js router.push() expects path relative to basePath usually?
// Let's implement robust segment swapping.
const normalizePath = (p: string) => {
if (p.startsWith('/docs')) return p.slice(5) || '/';
return p;
};
const normalizedPath = normalizePath(pathname);
const currentVersionObj = versions.find((v) => {
if (!v.param || v.param === 'latest') return false;
return normalizedPath.startsWith(`/${v.param}`);
});
const currentVersion = currentVersionObj ? currentVersionObj.param : 'latest';
const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const value = e.target.value;
const version = versions.find((v) => (v.param || v.label) === value);
if (!version) return;
if (!version.param) return;
// Calculate the 'tail' of the path (content path)
let tail = normalizedPath;
if (currentVersion !== 'latest') {
// Strip current version prefix
// e.g. /v4.10/foo -> /foo
tail = normalizedPath.replace(new RegExp(`^/${currentVersion}`), '') || '/';
}
// Construct new path
let newPath = tail;
if (version.param !== 'latest') {
// Add new version prefix
// e.g. /foo -> /v4.9/foo
// Handle root slash
if (tail === '/') {
newPath = `/${version.param}`;
} else {
newPath = `/${version.param}${tail}`;
}
}
// Next.js router handles basePath automatically if configured correctly.
// If we push '/v4.10', it becomes '/docs/v4.10'.
router.push(newPath);
};
return (
<select
value={currentVersion}
onChange={handleChange}
className="p-2 bg-transparent text-sm font-medium rounded-md border border-fd-border hover:bg-fd-accent/50 focus:outline-none focus:ring-2 focus:ring-fd-ring text-fd-foreground w-full mb-4"
>
{versions.map((v) => (
<option
key={v.param || v.label}
value={v.param || v.label}
className="bg-fd-background text-fd-foreground"
>
{v.label}
</option>
))}
</select>
);
}
+203
View File
@@ -0,0 +1,203 @@
{
"root": true,
"pages": [
"introduction",
{
"title": "Core Resources",
"pages": [
{
"title": "V2",
"pages": [
{
"title": "User",
"pages": [
"../reference/api/user"
]
},
{
"title": "Session",
"pages": [
"../reference/api/session"
]
},
{
"title": "OIDC",
"pages": [
"../reference/api/oidc"
]
},
{
"title": "SAML",
"pages": [
"../reference/api/saml"
]
},
{
"title": "Settings",
"pages": [
"../reference/api/settings"
]
},
{
"title": "Feature",
"pages": [
"../reference/api/feature"
]
},
{
"title": "Organization",
"pages": [
"../reference/api/org"
]
},
{
"title": "Identity Provider",
"pages": [
"../reference/api/idp"
]
},
{
"title": "Web Key",
"pages": [
"../reference/api/webkey"
]
},
{
"title": "Action",
"pages": [
"../reference/api/action"
]
},
{
"title": "Instance",
"pages": [
"../reference/api/instance"
]
},
{
"title": "Project",
"pages": [
"../reference/api/project"
]
},
{
"title": "Application",
"pages": [
"../reference/api/application"
]
},
{
"title": "Authorizations",
"pages": [
"../reference/api/authorization"
]
},
{
"title": "Internal Permissions",
"pages": [
"../reference/api/internal_permission"
]
}
]
},
{
"title": "V1",
"pages": [
{
"title": "Authenticated User",
"pages": [
"../reference/api-v1/auth"
]
},
{
"title": "Organization Objects",
"pages": [
"../reference/api-v1/management"
]
},
{
"title": "Instance Objects",
"pages": [
"../reference/api-v1/admin"
]
},
{
"title": "Instance Lifecycle",
"pages": [
"../reference/api-v1/system"
]
},
"migration_v1_to_v2"
]
},
{
"title": "Assets",
"pages": [
"assets/assets"
]
}
]
},
{
"title": "Observability",
"pages": [
"observability/metrics",
"observability/health"
]
},
{
"title": "Provision Users",
"pages": [
"scim2"
]
},
{
"title": "gRPC Status Codes",
"url": "statuscodes"
},
{
"title": "Rate Limits (Cloud)",
"url": "/legal/policies/rate-limit-policy"
},
{
"title": "Benchmarks",
"pages": [
{
"title": "v2.65.0",
"pages": [
"benchmarks/v2.65.0/machine_jwt_profile_grant/index"
]
},
{
"title": "v2.66.0",
"pages": [
"benchmarks/v2.66.0/machine_jwt_profile_grant/index"
]
},
{
"title": "v2.70.0",
"pages": [
"benchmarks/v2.70.0/machine_jwt_profile_grant/index",
"benchmarks/v2.70.0/oidc_session/index"
]
},
{
"title": "v4",
"pages": [
"benchmarks/v4/add_session/index",
"benchmarks/v4/human_password_login/index",
"benchmarks/v4/introspect/index",
"benchmarks/v4/machine_client_credentials_login/index",
"benchmarks/v4/machine_jwt_profile_grant/index",
"benchmarks/v4/machine_pat_login/index",
"benchmarks/v4/manipulate_user/index",
"benchmarks/v4/oidc_session/index",
"benchmarks/v4/otp_session/index",
"benchmarks/v4/password_session/index",
"benchmarks/v4/user_info/index"
]
}
]
}
]
}
@@ -0,0 +1,347 @@
---
title: Code examples
---
Actions are a powerful tool to extend ZITADEL and you might wonder what use cases actions can be used for.
This page provides a non-exhaustive list of possibilities which is provided by [examples](https://github.com/zitadel/actions/tree/main/examples). If a use case is missing feel free to contribute an issue or pull request to the repository, thanks in advance 🤗.
## Customize OIDC response
Append claims returned on OIDC requests.
### Triggers
- Complement token
- [Pre Userinfo creation](./complement-token#pre-userinfo-creation-id-token-userinfo-introspection-endpoint)
- [Pre access token creation](./complement-token#pre-access-token-creation)
### Set hardcoded claim
Extend the claims by a hardcoded value.
<details open="">
<summary>Code example</summary>
```js reference
https://github.com/zitadel/actions/blob/main/examples/add_claim.js
```
</details>
### Set dynamic claim from user metadata
Extend the claims by dynamically read metadata from a user and sets the picture-claim if idpPicture-metadata value is present.
<details open="">
<summary>{props.summary ? props.summary : 'Code example'}</summary>
```js reference
https://github.com/zitadel/actions/blob/main/examples/add_picture_claim_from_idp_metadata.js
```
</details>
### Set dynamic claim from organization metadata
Extend the claims by dynamically read metadata from an organization and sets the present metadata.
<details open="">
<summary>Code example</summary>
```js reference
https://github.com/zitadel/actions/blob/main/examples/org_metadata_claim.js
```
</details>
### Custom role mapping in claims
Some products require specific role mapping from ZITADEL, no worries we got you covered 😉
<details open="">
<summary>Code example</summary>
```js reference
https://github.com/zitadel/actions/blob/main/examples/custom_roles.js
```
</details>
### Custom role mapping including org metadata in claims
There's even a possibility to use the metadata of organizations the user is granted to
<details open="">
<summary>Code example</summary>
```js reference
https://github.com/zitadel/actions/blob/main/examples/custom_roles_org_metadata.js
```
</details>
## Customize SAML response
Append attributes returned on SAML requests.
### Triggers
- Complement SAMLResponse
- [Pre SAMLResponse creation](./customize-samlresponse#pre-saml-response-creation)
### Custom role mapping in attributes
Some products require specific role mapping from ZITADEL, no worries we got you covered 😉
<details open="">
<summary>Code example</summary>
```js reference
https://github.com/zitadel/actions/blob/main/examples/set_custom_attribute.js
```
</details>
### Set dynamic attribute from organization metadata
Extend the attributes by dynamically read metadata from an organization and sets the present metadata.
<details open="">
<summary>Code example</summary>
```js reference
https://github.com/zitadel/actions/blob/main/examples/org_metadata_attribute.js
```
</details>
## Manipulate user
You can automate manual tasks like setting default grants during user creation.
### Set email always verified
Useful if you trust the provided information or don't want the users to verify their e-mail addresses.
#### Triggers
- Internal Authentication
- [Pre Creation](/apis/actions/internal-authentication#pre-creation)
- External Authentication
- [Pre Creation](/apis/actions/external-authentication#pre-creation)
<details open="">
<summary>Code example</summary>
```js reference
https://github.com/zitadel/actions/blob/main/examples/verify_email.js
```
</details>
### Add grants to users
Allows you to add default user grants to a user after it was created or federated.
#### Triggers
- Internal Authentication
- [Post Creation](/apis/actions/internal-authentication#post-creation)
- External Authentication
- [Post Creation](/apis/actions/external-authentication#post-creation)
<details open="">
<summary>Code example</summary>
```js reference
https://github.com/zitadel/actions/blob/main/examples/add_user_grant.js
```
</details>
### Add metadata to users
Adding metadata to users allows you to set default metadata on users.
#### Triggers
- Internal Authentication
- [Pre Creation](/apis/actions/internal-authentication#pre-creation)
- [Post Authentication](/apis/actions/internal-authentication#post-authentication)
- External Authentication
- [Pre Creation](/apis/actions/internal-authentication#pre-creation)
- [Post Authentication](/apis/actions/internal-authentication#post-authentication)
<details open="">
<summary>Code example</summary>
```js reference
https://github.com/zitadel/actions/blob/main/examples/add_metadata.js
```
</details>
## Use provided fields of identity providers
If you want to ensure that the data of a user are always up-to-date, you can automatically update user fields during authentication and save time of your customers and your team.
### Trigger
- External Authentication
- [Post Authentication](./external-authentication#post-authentication)
### Fields provided by Okta as OIDC IdP
If you use [Okta as an identity provider](/guides/integrate/identity-providers/okta-oidc) you can improve the onboarding experience of new users by prefilling some basic information during authentication.
<details open="">
<summary>Code example</summary>
```js reference
https://github.com/zitadel/actions/blob/main/examples/okta_identity_provider.js
```
</details>
### Fields provided by Gitlab
If you use [Gitlab as an identity provider](/guides/integrate/identity-providers/gitlab) you can improve the onboarding experience of new users by prefilling some basic information during authentication.
<details open="">
<summary>Code example</summary>
```js reference
https://github.com/zitadel/actions/blob/main/examples/gitlab_identity_provider.js
```
</details>
### Fields provided by Github
If you use [Github as an identity provider](/guides/integrate/identity-providers/github) you can improve the onboarding experience of new users by prefilling some basic information during authentication.
<details open="">
<summary>Code example</summary>
```js reference
https://github.com/zitadel/actions/blob/main/examples/github_identity_provider.js
```
</details>
### Claims provided by a generic OIDC identity provider
If you use a [generic OIDC identity provider](/guides/integrate/identity-providers/migrate#migrate-generic-oidc-provider) you can improve the onboarding experience of new users by prefilling some basic information during authentication.
<details open="">
<summary>Code example</summary>
```js reference
https://github.com/zitadel/actions/blob/main/examples/set_idp_picture_metadata.js
```
</details>
### Attributes provided by Okta as SAML IDP
If you use [Okta as an identity provider](/guides/integrate/identity-providers/okta_saml#add-attribute-statements) you can improve the onboarding experience of new users by prefilling some basic information during authentication.
<details open="">
<summary>Code example</summary>
```js reference
https://github.com/zitadel/actions/blob/main/examples/okta_saml_prefil_register_form.js
```
</details>
### Attributes provided by Microsoft Entra as SAML IDP
If you use [Microsoft Entra as SAML identity provider](/guides/integrate/identity-providers/azure-ad-saml) you can improve the onboarding experience of new users by prefilling some basic information during authentication.
<details open="">
<summary>Code example</summary>
```js reference
https://github.com/zitadel/actions/blob/main/examples/entra_id_saml_prefil_register_form.js
```
</details>
### Attributes provided by a generic SAML identity provider
If you use a [SAML identity provider like mocksaml](/guides/integrate/identity-providers/mocksaml) you can improve the onboarding experience of new users by prefilling some basic information during authentication.
<details open="">
<summary>Code example</summary>
```js reference
https://github.com/zitadel/actions/blob/main/examples/post_auth_saml.js
```
</details>
## Context aware execution
Based on the context the execution path of an action can change. ZITADEL allows complex execution paths of course. 😎
### Based on auth request information
Execution paths might change based on the application initiating the authentication.
#### Triggers
- Internal Authentication
- [Pre Creation](/apis/actions/internal-authentication#pre-creation)
- [Post Creation](/apis/actions/internal-authentication#post-creation)
- [Post Authentication](/apis/actions/internal-authentication#post-authentication)
- External Authentication
- [Pre Creation](/apis/actions/external-authentication#pre-creation)
- [Post Creation](/apis/actions/external-authentication#post-creation)
- [Post Authentication](/apis/actions/external-authentication#post-authentication)
<details open="">
<summary>Code example</summary>
```js reference
https://github.com/zitadel/actions/blob/main/examples/execute_action_on_specific_app.js
```
</details>
This example uses [zitadel's log module](/apis/actions/modules#log)
### Check authentication error
Your action can also check for errors during the login process.
#### Triggers
- Internal Authentication
- [Post Authentication](/apis/actions/internal-authentication#post-authentication)
- External Authentication
- [Post Authentication](/apis/actions/external-authentication#post-authentication)
<details open="">
<summary>Code example</summary>
```js reference
https://github.com/zitadel/actions/blob/main/examples/post_auth_log.js
```
</details>
This example uses [zitadel's log module](/apis/actions/modules#log)
### Throw an error
Allows you to limit the user interaction. The error thrown will be shown to the user if the action is not [allowed to fail](/concepts/features/actions#how-it-works).
<details open="">
<summary>Code example</summary>
```js reference
https://github.com/zitadel/actions/blob/main/examples/throw_error.js
```
</details>
@@ -0,0 +1,72 @@
---
title: Complement Token Flow
---
This flow is executed during the creation of tokens and token introspection.
The flow is represented by the following Ids in the API: `2`
## Pre Userinfo creation (id_token / userinfo / introspection endpoint)
This trigger is called before userinfo are set in the id_token or userinfo and introspection endpoint response.
The trigger is represented by the following Ids in the API: `4`
### Parameters of Pre Userinfo creation
- `ctx`
The first parameter contains the following fields:
- `v1`
- `claims` [*Claims*](./objects#claims)
- `getUser()` [*User*](./objects#user)
- `user`
- `getMetadata()` [*metadataResult*](./objects#metadata-result)
- `grants` [*UserGrantList*](./objects#user-grant-list)
- `org`
- `getMetadata()` [*metadataResult*](./objects#metadata-result)
- `api`
The second parameter contains the following fields:
- `v1`
- `userinfo`
This function is deprecated, please use `api.v1.claims`
- `setClaim(string, Any)`
Sets any value if the key is not already present. If it's already present there is a message added to `urn:zitadel:iam:action:${action.name}:log`
Note that keys with prefix `urn:zitadel:iam` will be ignored.
- `claims`
- `setClaim(string, Any)`
Sets any value if the key is not already present. If it's already present there is a message added to `urn:zitadel:iam:action:${action.name}:log`
Note that keys with prefix `urn:zitadel:iam` will be ignored.
- `user`
- `setMetadata(string, Any)`
Key of the metadata and any value
## Pre access token creation
This trigger is called before the claims are set in the access token and the token type is `jwt`.
The trigger is represented by the following Ids in the API: `5`
### Parameters of Pre access token creation
- `ctx`
The first parameter contains the following fields:
- `v1`
- `claims` [*Claims*](./objects#claims)
- `getUser()` [*User*](./objects#user)
- `user`
- `getMetadata()` [*metadataResult*](./objects#metadata-result)
- `grants` [*UserGrantList*](./objects#user-grant-list)
- `org`
- `getMetadata()` [*metadataResult*](./objects#metadata-result)
- `api`
The second parameter contains the following fields:
- `v1`
- `claims`
- `setClaim(string, Any)`
Sets any value if the key is not already present. If it's already present there is a message added to `urn:zitadel:iam:action:${action.name}:log`
Note that keys with prefix `urn:zitadel:iam` will be ignored.
- `appendLogIntoClaims(string)`
Appends the entry into the claim `urn:zitadel:action:{action.name}:log` the value of the claim is an Array of *string*
- `user`
- `setMetadata(string, Any)`
Key of the metadata and any value
@@ -0,0 +1,34 @@
---
title: Complement SAMLResponse
---
This flow is executed before the return of the SAML Response.
The flow is represented by the following Ids in the API: `4`
## Pre SAMLResponse creation
This trigger is called before attributes are set in the SAMLResponse.
The trigger is represented by the following Ids in the API: `6`.
### Parameters of Pre SAMLResponse creation
- `ctx`
The first parameter contains the following fields:
- `v1`
- `getUser()` [*User*](./objects#user)
- `user`
- `getMetadata()` [*metadataResult*](./objects#metadata-result)
- `grants` [*UserGrantList*](./objects#user-grant-list)
- `org`
- `getMetadata()` [*metadataResult*](./objects#metadata-result)
- `api`
The second parameter contains the following fields:
- `v1`
- `attributes`
- `setCustomAttribute(string, string, ...string)`
Sets any value as attribute in addition to the default attributes, if the key is not already present. The parameters represent the key, nameFormat and the attributeValue(s).
- `user`
- `setMetadata(string, Any)`
Key of the metadata and any value
@@ -0,0 +1,137 @@
---
title: External Authentication Flow
---
This flow is executed if the user logs in using an [identity provider](/guides/integrate/identity-providers/introduction).
The flow is represented by the following Ids in the API: `FLOW_TYPE_EXTERNAL_AUTHENTICATION` and `1`
## Post Authentication
A user has authenticated externally. ZITADEL retrieved and mapped the external information.
The trigger is represented by the following Ids in the API: `TRIGGER_TYPE_POST_AUTHENTICATION` or `1`.
### Parameters of Post Authentication Action
- `ctx`
The first parameter contains the following fields
- `accessToken` *string*
The access token returned by the identity provider. This can be an opaque token or a JWT
- `refreshToken` *string*
The refresh token returned by the identity provider if there is one. This is most likely to be an opaque token.
- `claimsJSON()` [*idTokenClaims*](../openidoauth/claims)
Returns all claims of the id token
- `getClaim(key)` *Any*
Returns the requested [id token claim](../openidoauth/claims)
- `idToken` *string*
The id token provided by the identity provider.
- `v1`
- `externalUser` [*externalUser*](./objects#external-user)
- `authError` *string*
This is a verification errors string representation. If the verification succeeds, this is "none"
- `authRequest` [*auth request*](/apis/actions/objects#auth-request)
- `httpRequest` [*http request*](/apis/actions/objects#http-request)
- `providerInfo` *Any*
Returns the response of the provider. In case the provider is a Generic OAuth Provider, the information is accessible through:
- `rawInfo` *Any*
- `org`
- `getMetadata()` [*metadataResult*](./objects#metadata-result)
- `api`
The second parameter contains the following fields
- `v1`
- `user`
- `appendMetadata(string, Any)`
The first parameter represents the key and the second a value which will be stored
- `setFirstName(string)`
Sets the first name
- `setLastName(string)`
Sets the last name
- `setNickName(string)`
Sets the nickname
- `setDisplayName(string)`
Sets the display name
- `setPreferredLanguage(string)`
Sets the preferred language. Please use the format defined in [RFC 5646](https://www.rfc-editor.org/rfc/rfc5646)
- `setPreferredUsername(string)`
Sets the preferred username
- `setEmail(string)`
Sets the email address of the user
- `setEmailVerified(boolean)`
Sets the email address verified or unverified
- `setPhone(string)`
Sets the phone number of the user
- `setPhoneVerified(boolean)`
Sets the phone number verified or unverified
- `metadata`
Array of [*metadata*](./objects#metadata-with-value-as-bytes). This function is deprecated, please use `api.v1.user.appendMetadata`
## Pre Creation
A user selected **Register** on the overview page after external authentication. ZITADEL did not create the user yet.
The trigger is represented by the following Ids in the API: `TRIGGER_TYPE_PRE_CREATION` or `2`.
### Parameters of Pre Creation
- `ctx`
The first parameter contains the following fields
- `v1`
- `user` [*human*](./objects#human-user)
- `authRequest` [*auth request*](/apis/actions/objects#auth-request)
- `httpRequest` [*http request*](/apis/actions/objects#http-request)
- `org`
- `getMetadata()` [*metadataResult*](./objects#metadata-result)
- `api`
The second parameter contains the following fields
- `metadata`
Array of [*metadata*](./objects#metadata-with-value-as-bytes). This function is deprecated, please use `api.v1.user.appendMetadata`
- `setFirstName(string)`
Sets the first name
- `setLastName(string)`
Sets the last name
- `setNickName(string)`
Sets the nick name
- `setDisplayName(string)`
Sets the display name
- `setPreferredLanguage(string)`
Sets the preferred language, the string has to be a valid language tag as defined in [RFC 5646](https://www.rfc-editor.org/rfc/rfc5646)
- `setGender(int)`
Sets the gender.
<ul><li>0: unspecified</li><li>1: female</li><li>2: male</li><li>3: diverse</li></ul>
- `setUsername(string)`
Sets the username
- `setEmail(string)`
Sets the email
- `setEmailVerified(bool)`
If true the email set is verified without user interaction
- `setPhone(string)`
Sets the phone number
- `setPhoneVerified(bool)`
If true the phone number set is verified without user interaction
- `v1`
- `user`
- `appendMetadata(string, Any)`
The first parameter represents the key and the second a value which will be stored
## Post Creation
A user selected **Register** on the overview page after external authentication and ZITADEL successfully created the user.
The trigger is represented by the following Ids in the API: `TRIGGER_TYPE_POST_CREATION` or `3`.
### Parameters of Post Creation
- `ctx`
The first parameter contains the following fields
- `v1`
- `getUser()` [*user*](./objects#user)
- `authRequest` [*auth request*](/apis/actions/objects#auth-request)
- `httpRequest` [*http request*](/apis/actions/objects#http-request)
- `org`
- `getMetadata()` [*metadataResult*](./objects#metadata-result)
- `api`
The second parameter contains the following fields
- `userGrants` Array of [*userGrant*](./objects#user-grant)'s
- `v1`
- `appendUserGrant(`[`userGrant`](./objects#user-grant)`)`
@@ -0,0 +1,103 @@
---
title: Internal Authentication Flow
---
This flow is executed if the user logs in using the login UI hosted by ZITADEL.
The flow is represented by the following Ids in the API: `3`
## Post Authentication
A user has authenticated directly at ZITADEL.
ZITADEL validated the users inputs for password, one-time password or passkey.
Each validation step triggers the action.
The trigger is represented by the following Ids in the API: `TRIGGER_TYPE_POST_AUTHENTICATION` or `1`.
### Parameters of Post Authentication Action
- `ctx`
The first parameter contains the following fields
- `v1`
- `authMethod` *string*
This is one of "password", "OTP", "U2F" or "passwordless"
- `authError` *string*
This is a verification errors string representation. If the verification succeeds, this is "none"
- `authRequest` [*auth request*](/apis/actions/objects#auth-request)
- `httpRequest` [*http request*](/apis/actions/objects#http-request)
- `api`
The second parameter contains the following fields
- `metadata`
Array of [*metadata*](./objects#metadata-with-value-as-bytes). This function is deprecated, please use `api.v1.user.appendMetadata`
- `v1`
- `user`
- `appendMetadata(string, Any)`
The first parameter represents the key and the second a value which will be stored
## Pre Creation
A user registers directly at ZITADEL.
ZITADEL did not create the user yet.
The trigger is represented by the following Ids in the API: `TRIGGER_TYPE_PRE_CREATION` or `2`.
### Parameters of Pre Creation
- `ctx`
The first parameter contains the following fields
- `v1`
- `user` [*human*](./objects#human-user)
- `authRequest` [*auth request*](/apis/actions/objects#auth-request)
- `httpRequest` [*http request*](/apis/actions/objects#http-request)
- `api`
The second parameter contains the following fields
- `metadata`
Array of [*metadata*](./objects#metadata-with-value-as-bytes). This function is deprecated, please use `api.v1.user.appendMetadata`
- `setFirstName(string)`
Sets the first name
- `setLastName(string)`
Sets the last name
- `setNickName(string)`
Sets the nick name
- `setDisplayName(string)`
Sets the display name
- `setPreferredLanguage(string)`
Sets the preferred language, the string has to be a valid language tag as defined in [RFC 5646](https://www.rfc-editor.org/rfc/rfc5646)
- `setGender(int)`
Sets the gender.
<ul><li>0: unspecified</li><li>1: female</li><li>2: male</li><li>3: diverse</li></ul>
- `setUsername(string)`
Sets the username
- `setEmail(string)`
Sets the email
- `setEmailVerified(bool)`
If true the email set is verified without user interaction
- `setPhone(string)`
Sets the phone number
- `setPhoneVerified(bool)`
If true the phone number set is verified without user interaction
- `v1`
- `user`
- `appendMetadata(string, Any)`
The first parameter represents the key and the second a value which will be stored
## Post Creation
A user registers directly at ZITADEL.
ZITADEL successfully created the user.
The trigger is represented by the following Ids in the API: `TRIGGER_TYPE_POST_CREATION` or `3`.
### Parameters of Post Creation
- `ctx`
The first parameter contains the following fields
- `v1`
- `getUser()` [*user*](./objects#user)
- `authRequest` [*auth request*](/apis/actions/objects#auth-request)
- `httpRequest` [*http request*](/apis/actions/objects#http-request)
- `api`
The second parameter contains the following fields
- `userGrants` Array of [*userGrant*](./objects#user-grant)'s
- `v1`
- `appendUserGrant(`[`userGrant`](./objects#user-grant)`)`
@@ -0,0 +1,58 @@
---
title: Actions
---
This page describes the options you have when writing ZITADEL actions scripts.
## Language
ZITADEL interprets the scripts as JavaScript.
Make sure your scripts are ECMAScript 5.1(+) compliant.
Go to the [goja GitHub page](https://github.com/dop251/goja) for detailed reference about the underlying library features and limitations.
Stuck customizing ZITADEL actions? Find samples for setting OIDC claims, SAML attributes, extending JIT provisioning data, calling external APIs, and more in [this repository](https://github.com/zitadel/actions).
Actions are a key feature to extend the functionality of ZITADEL and continuously improve the feature and expand the use cases. Check out our [roadmap](https://zitadel.com/roadmap) for more details.
## Action
The action object describes the logic defined by the user. Actions can be linked to multiple [triggers](#flows).
The name of the action must corelate with the javascript function in the code section. This function will be called by ZITADEL.
For reading and mutating state, the runtime executes the function that has the same name as the action.
The function receives the JavaScript objects `ctx` and `api`.
The object `ctx` provides readable information as object properties and by callable functions.
The object `api` provides mutable properties and state mutating functions.
The script of an action called **doSomething** should have a function called `doSomething` and look something like this:
```js
function doSomething(ctx, api){
// read from ctx and manipulate with api
}
```
## Flows
Flows are the links between an [action](#action) and a specific point during a user interaction with ZITADEL. These specific point are called [Trigger Types](#trigger-types).
## Trigger Types
Trigger types define the point during execution of request. Each trigger defines which readable information (`ctx`) and mutual properties (`api`) are passed into the called function as well as which libraries are available for the function. Each trigger type is described in the [available flow types section](#available-flow-types)
## Available Flow Types
Currently ZITADEL provides the following flows:
- [Internal Authentication](./internal-authentication)
- [External Authentication](./external-authentication)
- [Complement Token](./complement-token)
- [Complement SAML Response](./customize-samlresponse)
## Available Modules inside Javascript
- [HTTP module](./modules#http) to call API's
- [Logging module](./modules#log) logs information to stdout
- [UUID module](./modules#uuid) generates uuids
+148
View File
@@ -0,0 +1,148 @@
---
title: Modules
---
ZITADEL provides the following modules.
## HTTP
This module provides functionality to call REST APIs.
### Import
```js
let http = require('zitadel/http')
```
### `fetch()` function
This function allows to call HTTP servers. The function does NOT fulfil the [Fetch API specification](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API).
#### Parameters
- `url` *string*
- `options`
**Optional**, containing custom settings that you want to apply to the request.
- `headers`
Overwrites the default headers. One of the following types
- *map[string] string*
The value is split into separate values after each comma `,`.
- *map[string] Array of string*
The value is a string array
- default:
- `Content-Type`: `application/json`
- `Accept`: `application/json`
- `method`
The request method. Allowed values are `GET`, `POST`, `PUT`, `DELETE`
- `body` *Object*
JSON representation
#### Response
If the request was invalid, an error will be thrown, otherwise a Response object will be returned.
The object has the following fields and methods:
- `status` *number*
Status code of response
- `body` *string*
Return value
- `json()` *Object*
Returns the body as JSON object, or throws an error if the body is not a json object.
- `text()` *string*
Returns the body
### Example
```js reference
https://github.com/zitadel/actions/blob/main/examples/make_api_call.js#L10-L20
```
## Log
The log module provides you with the functionality to log to stdout.
### Import
```js
let logger = require("zitadel/log")
```
### `log()`, `warn()`, `error()` function
The logger offers three distinct log levels (info, warn, and error) to effectively communicate messages based on their severity, enhancing debugging and troubleshooting efficiency.
Use the function that reflects your log level.
- log()
- warn()
- error()
### Example
```js
logger.log("This is an info log.")
logger.warn("This is a warn log.")
logger.error("This is an error log.")
```
#### Parameters
- `msg` *string*
The message you want to print out.
## UUID
This module provides functionality to generate a UUID
### Import
```js
let uuid = require("zitadel/uuid")
```
### `uuid.vX()` function
This function generates a UUID using [google/uuid](https://github.com/google/uuid). `vX` allows to define the UUID version:
- `uuid.v1()` *string*
Generates a UUID version 1, based on date-time and MAC address
- `uuid.v3(namespace, data)` *string*
Generates a UUID version 3, based on the provided namespace using MD5
- `uuid.v4()` *string*
Generates a UUID version 4, which is randomly generated
- `uuid.v5(namespace, data)` *string*
Generates a UUID version 5, based on the provided namespace using SHA1
#### Parameters
- `namespace` *UUID*/*string*
Namespace to be used in the hashing function. Either provide one of defined [namespaces](#namespaces) or a string representing a UUID.
- `data` *[]byte*/*string*
data to be used in the hashing function. Possible types are []byte or string.
### Namespaces
The following predefined namespaces can be used for `uuid.v3` and `uuid.v5`:
- `uuid.namespaceDNS` *UUID*
6ba7b810-9dad-11d1-80b4-00c04fd430c8
- `uuid.namespaceURL` *UUID*
6ba7b811-9dad-11d1-80b4-00c04fd430c8
- `uuid.namespaceOID` *UUID*
6ba7b812-9dad-11d1-80b4-00c04fd430c8
- `uuid.namespaceX500` *UUID*
6ba7b814-9dad-11d1-80b4-00c04fd430c8
### Example
```js
let uuid = require("zitadel/uuid")
function setUUID(ctx, api) {
if (api.metadata === undefined) {
return;
}
api.v1.user.appendMetadata('custom-id', uuid.v4());
}
```
+214
View File
@@ -0,0 +1,214 @@
---
title: Objects
---
## External User
- `externalId` *string*
User id from the identity provider
- `externalIdpId` *string*
Id of the identity provider
- `human`
- `firstName` *string*
- `lastName` *string*
- `nickName` *string*
- `displayName` *string*
- `preferredLanguage` *string*
In [RFC 5646](https://www.rfc-editor.org/rfc/rfc5646) format
- `email` *string*
- `isEmailVerified` *boolean*
- `phone` *string*
- `isPhoneVerified` *boolean*
## metadata with value as bytes
- `key` *string*
- `value` Array of *byte*
## metadata result
- `count` *number*
- `sequence` *number*
- `timestamp` *Date*
- `metadata` Array of [*metadata*](#metadata)
## metadata
- `creationDate` *Date*
- `changeDate` *Date*
- `resourceOwner` *string*
- `sequence` *number*
- `key` *string*
- `value` `Any`
## user grant
- `projectID` *string*
Required. Id of the project to be granted
- `projectGrantID` *string*
Optional. If the grant is for a project grant, include projectGrantID
- `roles` Array of *string*
Containing the roles
## user
- `id` *string*
- `creationDate` *Date*
- `changeDate` *Date*
- `resourceOwner` *string*
- `sequence` *number*
Unsigned 64 bit integer
- `state` *number*
<ul><li>0: unspecified</li><li>1: active</li><li>2: inactive</li><li>3: deleted</li><li>4: locked</li><li>5: suspended</li><li>6: initial</li></ul>
- `username` *string*
- `loginNames` Array of *string*
- `preferredLoginName` *string*
- `human`
Set if the user is human
- `firstName` *string*
- `lastName` *string*
- `nickName` *string*
- `displayName` *string*
- `avatarKey` *string*
- `preferredLanguage` *string*
In [RFC 5646](https://www.rfc-editor.org/rfc/rfc5646) format
- `gender` *number*
<ul><li>0: unspecified</li><li>1: female</li><li>2: male</li><li>3: diverse</li></ul>
- `email` *string*
- `isEmailVerified` *boolean*
- `phone` *string*
- `isPhoneVerified` *boolean*
- `machine`
Set if the user is a machine
- `name` *string*
- `description` *string*
## human user
- `id` *string*
- `creationDate` *Date*
- `changeDate` *Date*
- `resourceOwner` *string*
- `sequence` *number*
- `state` *number*
<ul><li>0: unspecified</li><li>1: active</li><li>2: inactive</li><li>3: deleted</li><li>4: locked</li><li>5: suspended</li><li>6: initial</li></ul>
- `username` *string*
- `loginNames` Array of *string*
- `preferredLoginName` *string*
- `profile`
- `firstName` *string*
- `lastName` *string*
- `nickName` *string*
- `displayName` *string*
- `preferredLanguage` *string*
In [RFC 5646](https://www.rfc-editor.org/rfc/rfc5646) format
- `email`
- `email` *string*
- `isEmailVerified` *boolean*
- `phone`
- `phone` *string*
- `isPhoneVerified` *boolean*
## Auth Request
This object contains context information about the request to the [authorization endpoint](/apis/openidoauth/endpoints#authorization-endpoint).
- `id` *string*
- `agentId` *string*
- `creationDate` *Date*
- `changeDate` *Date*
- `browserInfo` *browserInfo*
- `userAgent` *string*
- `acceptLanguage` *string*
- `remoteIp` *string*
- `applicationId` *string*
- `callbackUri` *string*
- `transferState` *string*
- `prompt` Array of *Number*
<ul><li>0: not specified</li><li>1: none</li><li>2: login</li><li>3: consent</li><li>4: select_account</li><li>5: create</li></ul>
- `uiLocales` Array of *string*
- `loginHint` *string*
- `maxAuthAge` *Number*
Duration in nanoseconds
- `instanceId` *string*
- `request`
- `oidc`
- `scopes` Array of *string*
- `userId` *string*
- `userName` *string*
- `loginName` *string*
- `displayName` *string*
- `resourceOwner` *string*
- `requestedOrgId` *string*
- `requestedOrgName` *string*
- `requestedPrimaryDomain` *string*
- `requestedOrgDomain` *bool*
- `applicationResourceOwner` *string*
- `privateLabelingSetting` *Number*
<ul><li>0: Unspecified</li><li>1: Enforce project's policy</li><li>2: Allow user's organization login policy</li></ul>
- `selectedIdpConfigId` *string*
- `linkingUsers` Array of [*ExternalUser*](#external-user)
- `passwordVerified` *bool*
- `mfasVerified` Array of *Number*
<ul><li>0: OTP</li><li>1: U2F</li><li>2: U2F User verification</li></ul>
- `audience` Array of *string*
- `authTime` *Date*
## HTTP Request
This object is based on the Golang struct [http.Request](https://pkg.go.dev/net/http#Request), some attributes are removed as not all provided information is usable in this context.
- `method` *string*
- `url` *string*
- `proto` *string*
- `contentLength` *number*
- `host` *string*
- `form` Map *string* of Array of *string*
- `postForm` Map *string* of Array of *string*
- `remoteAddr` *string*
- `headers` Map *string* of Array of *string*
## Claims
This object represents [the claims](../openidoauth/claims) which will be written into the oidc token.
- `sub` *string*
- `name` *string*
- `email` *string*
- `locale` *string*
- `given_name` *string*
- `family_name` *string*
- `preferred_username` *string*
- `email_verified` *bool*
- `updated_at` *Number*
Additionally there could additional fields depending on the configuration of your [project](../../guides/manage/console/projects-overview#role-settings) and your [application](../../guides/manage/console/applications-overview#token-settings)
## user grant list
This object represents a list of user grant stored in ZITADEL.
- `count` *Number*
- `sequence` *Number*
- `timestamp` *Date*
- `grants` Array of
- `id` *string*
- `projectGrantId` *string*
The id of the [project grant](/guides/solution-scenarios/saas#project-grant)
- `state` *Number*
<ul><li>0: unspecified</li><li>1: active</li><li>2: inactive</li><li>3: removed</li></ul>
- `creationDate` *Date*
- `changeDate` *Date*
- `sequence` *Number*
- `userId` *string*
- `roles` Array of *string*
- `userResourceOwner` *string*
The id of the organization of the user
- `userGrantResourceOwner` *string*
The id of the organization, where the user was granted
- `userGrantResourceOwnerName` *string*
The name of the organization, where the user was granted
- `projectId` *string*
- `projectName` *string*
- `getOrgMetadata()` [*metadataResult*](#metadata-result)
Get the metadata of the organization where the user was granted
+12
View File
@@ -0,0 +1,12 @@
---
title: Core Resources
sidebar_label: Core Resources
---
import DocCardList from '@/components/docusaurus/doc-card-list';
ZITADEL provides multiple APIs to manage the system, instances and resources such as users, projects and more.
There are different versions and multiple services available:
<DocCardList />
+197
View File
@@ -0,0 +1,197 @@
---
title: zitadel/assets
---
## AssetsService
### UploadDefaultLabelPolicyFont()
> UploadDefaultLabelPolicyFont()
POST: /instance/policy/label/font
### GetDefaultLabelPolicyFont()
> GetDefaultLabelPolicyFont()
GET: /instance/policy/label/font
### GetPreviewDefaultLabelPolicyFont()
> GetPreviewDefaultLabelPolicyFont()
GET: /instance/policy/label/font/_preview
### UploadDefaultLabelPolicyIcon()
> UploadDefaultLabelPolicyIcon()
POST: /instance/policy/label/icon
### UploadDefaultLabelPolicyIconDark()
> UploadDefaultLabelPolicyIconDark()
POST: /instance/policy/label/icon/dark
### GetDefaultLabelPolicyIcon()
> GetDefaultLabelPolicyIcon()
GET: /instance/policy/label/icon
### GetDefaultLabelPolicyIconDark()
> GetDefaultLabelPolicyIconDark()
GET: /instance/policy/label/icon/dark
### GetPreviewDefaultLabelPolicyIcon()
> GetPreviewDefaultLabelPolicyIcon()
GET: /instance/policy/label/icon/_preview
### GetPreviewDefaultLabelPolicyIconDark()
> GetPreviewDefaultLabelPolicyIconDark()
GET: /instance/policy/label/icon/dark/_preview
### UploadDefaultLabelPolicyLogo()
> UploadDefaultLabelPolicyLogo()
POST: /instance/policy/label/logo
### UploadDefaultLabelPolicyLogoDark()
> UploadDefaultLabelPolicyLogoDark()
POST: /instance/policy/label/logo/dark
### GetDefaultLabelPolicyLogo()
> GetDefaultLabelPolicyLogo()
GET: /instance/policy/label/logo
### GetDefaultLabelPolicyLogoDark()
> GetDefaultLabelPolicyLogoDark()
GET: /instance/policy/label/logo/dark
### GetPreviewDefaultLabelPolicyLogo()
> GetPreviewDefaultLabelPolicyLogo()
GET: /instance/policy/label/logo/_preview
### GetPreviewDefaultLabelPolicyLogoDark()
> GetPreviewDefaultLabelPolicyLogoDark()
GET: /instance/policy/label/logo/dark/_preview
### UploadOrgLabelPolicyFont()
> UploadOrgLabelPolicyFont()
POST: /org/policy/label/font
### GetOrgLabelPolicyFont()
> GetOrgLabelPolicyFont()
GET: /org/policy/label/font
### GetPreviewOrgLabelPolicyFont()
> GetPreviewOrgLabelPolicyFont()
GET: /org/policy/label/font/_preview
### UploadOrgLabelPolicyIcon()
> UploadOrgLabelPolicyIcon()
POST: /org/policy/label/icon
### UploadOrgLabelPolicyIconDark()
> UploadOrgLabelPolicyIconDark()
POST: /org/policy/label/icon/dark
### GetOrgLabelPolicyIcon()
> GetOrgLabelPolicyIcon()
GET: /org/policy/label/icon
### GetOrgLabelPolicyIconDark()
> GetOrgLabelPolicyIconDark()
GET: /org/policy/label/icon/dark
### GetPreviewOrgLabelPolicyIcon()
> GetPreviewOrgLabelPolicyIcon()
GET: /org/policy/label/icon/_preview
### GetPreviewOrgLabelPolicyIconDark()
> GetPreviewOrgLabelPolicyIconDark()
GET: /org/policy/label/icon/dark/_preview
### UploadOrgLabelPolicyLogo()
> UploadOrgLabelPolicyLogo()
POST: /org/policy/label/logo
### UploadOrgLabelPolicyLogoDark()
> UploadOrgLabelPolicyLogoDark()
POST: /org/policy/label/logo/dark
### GetOrgLabelPolicyLogo()
> GetOrgLabelPolicyLogo()
GET: /org/policy/label/logo
### GetOrgLabelPolicyLogoDark()
> GetOrgLabelPolicyLogoDark()
GET: /org/policy/label/logo/dark
### GetPreviewOrgLabelPolicyLogo()
> GetPreviewOrgLabelPolicyLogo()
GET: /org/policy/label/logo/_preview
### GetPreviewOrgLabelPolicyLogoDark()
> GetPreviewOrgLabelPolicyLogoDark()
GET: /org/policy/label/logo/dark/_preview
### UploadMyUserAvatar()
> UploadMyUserAvatar()
POST: /users/me/avatar
### GetMyUserAvatar()
> GetMyUserAvatar()
GET: /users/me/avatar
@@ -0,0 +1,43 @@
import OutputSource from "./output.json";
import { BenchmarkChart } from '@/components/benchmark_chart';
TODO: describe the outcome of the test?
## Performance test results
| Metric | Value |
|-:-------------------------------------|-:-----|
| Baseline | none |
| Purpose | |
| Test start | UTC |
| Test duration | 30min |
| Executed test | |
| k6 version | |
| VUs | |
| Client location | |
| Client machine specification | vCPU: <br/> memory: Gb |
| ZITADEL location | |
| ZITADEL container specification | vCPU: <br/> Memory: Gb <br/>Container count: |
| ZITADEL Version | |
| ZITADEL Configuration | |
| ZITADEL feature flags | |
| Database | type: psql<br />version: |
| Database location | |
| Database specification | vCPU: <br/> memory: Gb |
| ZITADEL metrics during test | |
| Observed errors | |
| Top 3 most expensive database queries | |
| Database metrics during test | |
| k6 Iterations per second | |
| k6 overview | |
| k6 output | |
| flowchart outcome | |
## Endpoint latencies
<BenchmarkChart testResults={OutputSource} />
## k6 output
```bash
TODO: add summary of k6
```
+107
View File
@@ -0,0 +1,107 @@
---
title: Benchmarks
sidebar_label: Benchmarks
---
import DocCardList from '@/components/docusaurus/doc-card-list';
Benchmarks are crucial to understand if ZITADEL fulfills your expected workload and what resources it needs to do so.
This document explains the process and goals of load-testing zitadel in a cloud environment.
The results can be found on sub pages.
## Goals
The primary goal is to assess if ZITADEL can scale to required proportion. The goals might change over time and maturity of ZITADEL. At the moment the goal is to assess how the application’s performance scales. There are some concrete goals we have to meet:
1. [https://github.com/zitadel/zitadel/issues/8352](https://github.com/zitadel/zitadel/issues/8352) defines 1000 JWT profile auth/sec
2. [https://github.com/zitadel/zitadel/issues/4424](https://github.com/zitadel/zitadel/issues/4424) defines 1200 logins / sec.
## Procedure
First we determine the “target” of our load-test. The target is expressed as a make recipe in the load-test [Makefile](https://github.com/zitadel/zitadel/blob/main/load-test/Makefile). See also the load-test [readme](https://github.com/zitadel/zitadel/blob/main/load-test/README.md) on how to configure and run load-tests.
A target should be tested for longer periods of time, as it might take time for certain metrics to show up. For example, cloud SQL samples query insights. A runtime of at least **30 minutes** is advised at the moment.
After each iteration of load-test, we should consult the [After test procedure](#after-test-procedure) to conclude an outcome:
1. Scale
2. Log potential issuer and scale
3. Terminate testing and resolve issues
## Methodology
### Benchmark definition
Tests are implemented in the ecosystem of [k6](https://k6.io). The tests are publicly available in the [zitadel repository](https://github.com/zitadel/zitadel/tree/main/load-test). Custom extensions of k6 are implemented in the [xk6-modules repository](https://github.com/zitadel/xk6-modules).
The tests must at least measure the request duration for each API call. This gives an indication on how zitadel behaves over the duration of the load test.
### Metrics
The following metrics must be collected for each test iteration. The metrics are used to follow the decision path of the [After test procedure](https://drive.google.com/open?id=1WVr7aA8dGgV1zd2jUg1y1h_o37mkZF2O6M5Mhafn_NM):
| Metric | Type | Description | Unit |
| :---- | :---- | :---- | :---- |
| Baseline | Comparison | Defines the baseline the test is compared against. If not specified the baseline defined in this document is used. | Link to test result |
| Purpose | Description | Description what should been proved with this test run | text
| Test start | Setup | Timestamp when the test started. This is useful for gathering additional data like metrics or logs later | Date |
| Test duration | Setup | Duration of the test | Duration |
| Executed test | Setup | Name of the make recipe executed. Further information about specific test cases can be found [here](?tab=t.0#heading=h.xav4f3s5r2f3). | Name of the make recipe |
| k6 version | Setup | Version of the test client (k6) used | semantic version |
| VUs | Setup | Virtual Users which execute the test scenario in parallel | Number |
| Client location | Setup | Region or location of the machine which executed the test client. If not further specified the hoster is Google Cloud | Location / Region |
| Client machine specification | Setup | Definition of the client machine the test client ran on. The resources of the machine could be maxed out during tests therefore we collect this metric as well. The description must at least clarify the following metrics: vCPU Memory egress bandwidth | **vCPU**: Amount of threads ([additional info](https://cloud.google.com/compute/docs/cpu-platforms)) **memory**: GB **egress bandwidth**:Gbps |
| ZITADEL location | Setup | Region or location of the deployment of zitadel. If not further specified the hoster is Google Cloud | Location / Region |
| ZITADEL container specification | Setup | As ZITADEL is mainly run in cloud environments it should also be run as a container during the load tests. The description must at least clarify the following metrics: vCPU Memory egress bandwidth Scale | **vCPU**: Amount of threads ([additional info](https://cloud.google.com/compute/docs/cpu-platforms)) **memory**: GB **egress bandwidth**:Gbps **scale**: The amount of containers running during the test. The amount must not vary during the tests |
| ZITADEL Version | Setup | The version of zitadel deployed | Semantic version or commit |
| ZITADEL Configuration | Setup | Configuration of zitadel which deviates from the defaults and is not secret | yaml |
| ZITADEL feature flags | Setup | Changed feature flags | yaml |
| Database | Setup | Database type and version | **type**: psql **version**: semantic version |
| Database location | Setup | Region or location of the deployment of the database. If not further specified the hoster is Google Cloud SQL | Location / Region |
| Database specification | Setup | The description must at least clarify the following metrics: vCPU, Memory and egress bandwidth (Scale) | **vCPU**: Amount of threads ([additional info](https://cloud.google.com/compute/docs/cpu-platforms)) **memory**: GB **egress bandwidth**:Gbps |
| ZITADEL metrics during test | Result | This metric helps understanding the bottlenecks of the executed test. At least the following metrics must be provided: CPU usage Memory usage | **CPU usage** in percent **Memory usage** in percent |
| Observed errors | Result | Errors worth mentioning, mostly unexpected errors | description |
| Top 3 most expensive database queries | Result | The execution plan of the top 3 most expensive database queries during the test execution | database execution plan |
| Database metrics during test | Result | This metric helps understanding the bottlenecks of the executed test. At least the following metrics must be provided: CPU usage Memory usage | **CPU usage** in percent **Memory usage** in percent |
| k6 Iterations per second | Result | How many test iterations were done per second | Number |
| k6 overview | Result | Shows some basic metrics aggregated over the test run At least the following metrics must be included: duration per request (min, max, avg, p50, p95, p99) VUS For simplicity just add the whole test result printed to the terminal | terminal output |
| k6 output | Result | Trends and metrics generated during the test, this contains detailed information for each step executed during each iteration | csv |
### Test setup
#### Make recipes
Details about the tests implemented can be found in [this readme](https://github.com/zitadel/zitadel/blob/main/load-test/README.md#test).
### Test conclusion
After each iteration of load-test, we should consult the [Flowchart](#after-test-procedure) to conclude an outcome:
1. [Scale](#scale)
2. [Log potential issue and scale](#potential-issues)
3. [Terminate testing](#termination) and resolve issues
#### Scale
An outcome of scale means that the service hit some kind of resource limit, like CPU or RAM which can be increased. In such cases we increase the suggested parameter and rerun the load-test for the same target. On the next test we should analyze if the increase in scale resulted in a performance improvement proportional to the scale parameter. For example if we scale from 1 to 2 containers, it might be reasonable to expect a doubling of iterations / sec. If such an increase is not noticed, there might be another bottleneck or underlying issue, such as locking.
#### Potential issues
A potential issue has an impact on performance, but does not prevent us to scale. Such issues must be logged in GH issues and load-testing can continue. The issue can be resolved at a later time and the load-tests repeated when it is. This is primarily for issues which require big changes to ZITADEL.
#### Termination
Scaling no longer improves iterations / second, or some kind of critical error or bug is experienced. The root cause of the issue must be resolved before we can continue with increasing scale.
### After test procedure
This flowchart shows the procedure after running a test.
![](../../../public/img/benchmark/Flowchart.svg)
## Baseline
Will be established as soon as the goal described above is reached.
## Test results
This chapter provides a table linking to the detailed test results.
<DocCardList />
@@ -0,0 +1,70 @@
---
title: machine jwt profile grant benchmark of zitadel v2.65.0
sidebar_label: machine jwt profile grant
---
import OutputSource from "./output.json";
import { BenchmarkChart } from '@/components/benchmark_chart';
Tests are halted after this test run because of too many [client read events](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/wait-event.clientread.html) on the database.
## Performance test results
| Metric | Value |
| :---- | :---- |
| Baseline | none |
| Test start | 22-10-2024 16:20 UTC |
| Test duration | 30min |
| Executed test | machine\_jwt\_profile\_grant |
| k6 version | v0.54.0 |
| VUs | 50 |
| Client location | US1 |
| Client machine specification | e2-high-cpu-4 |
| Zitadel location | US1 |
| Zitadel container specification | vCPUs: 2<br/>Memory: 512 MiB<br/>Container count: 2 |
| Zitadel feature flags | none |
| Database | postgres v15 |
| Database location | US1 |
| Database specification | vCPUs: 4<br/>Memory: 16 GiB |
| Zitadel metrics during test | |
| Observed errors | Many client read events during push |
| Top 3 most expensive database queries | 1: Query events `instance_id = $1 AND aggregate_type = $2 AND aggregate_id = $3 AND event_type = ANY($4)`<br/>2: latest sequence query during push events<br/>3: writing events during push (caused lock wait events) |
| k6 iterations per second | 193 |
| k6 overview | [output](#k6-output) |
| flowchart outcome | Halt tests, must resolve an issue |
## /token endpoint latencies
<BenchmarkChart testResults={OutputSource} />
## k6 output
```bash
checks...............................: 100.00% ✓ 695739 ✗ 0
data_received........................: 479 MB 265 kB/s
data_sent............................: 276 MB 153 kB/s
http_req_blocked.....................: min=178ns avg=5µs max=119.8ms p(50)=460ns p(95)=702ns p(99)=921ns
http_req_connecting..................: min=0s avg=1.24µs max=43.45ms p(50)=0s p(95)=0s p(99)=0s
http_req_duration....................: min=18ms avg=255.3ms max=1.22s p(50)=241.56ms p(95)=479.19ms p(99)=600.92ms
{ expected_response:true }.........: min=18ms avg=255.3ms max=1.22s p(50)=241.56ms p(95)=479.19ms p(99)=600.92ms
http_req_failed......................: 0.00% ✓ 0 ✗ 347998
http_req_receiving...................: min=25.92µs avg=536.96µs max=401.94ms p(50)=89.44µs p(95)=2.39ms p(99)=11.12ms
http_req_sending.....................: min=24.01µs avg=63.86µs max=4.48ms p(50)=60.97µs p(95)=88.69µs p(99)=141.74µs
http_req_tls_handshaking.............: min=0s avg=2.8µs max=51.05ms p(50)=0s p(95)=0s p(99)=0s
http_req_waiting.....................: min=17.65ms avg=254.7ms max=1.22s p(50)=240.88ms p(95)=478.6ms p(99)=600.6ms
http_reqs............................: 347998 192.80552/s
iteration_duration...................: min=33.86ms avg=258.77ms max=1.22s p(50)=245ms p(95)=482.61ms p(99)=604.32ms
iterations...........................: 347788 192.689171/s
login_ui_enter_login_name_duration...: min=218.61ms avg=218.61ms max=218.61ms p(50)=218.61ms p(95)=218.61ms p(99)=218.61ms
login_ui_enter_password_duration.....: min=18ms avg=18ms max=18ms p(50)=18ms p(95)=18ms p(99)=18ms
login_ui_init_login_duration.........: min=90.96ms avg=90.96ms max=90.96ms p(50)=90.96ms p(95)=90.96ms p(99)=90.96ms
login_ui_token_duration..............: min=140.02ms avg=140.02ms max=140.02ms p(50)=140.02ms p(95)=140.02ms p(99)=140.02ms
oidc_token_duration..................: min=29.85ms avg=255.38ms max=1.22s p(50)=241.61ms p(95)=479.23ms p(99)=600.95ms
org_create_org_duration..............: min=64.51ms avg=64.51ms max=64.51ms p(50)=64.51ms p(95)=64.51ms p(99)=64.51ms
user_add_machine_key_duration........: min=44.93ms avg=87.89ms max=159.52ms p(50)=84.43ms p(95)=144.59ms p(99)=155.54ms
user_create_machine_duration.........: min=65.75ms avg=266.53ms max=421.58ms p(50)=276.59ms p(95)=380.84ms p(99)=414.43ms
vus..................................: 0 min=0 max=50
vus_max..............................: 50 min=50 max=50
running (30m04.9s), 00/50 VUs, 347788 complete and 0 interrupted iterations
default ✓ [======================================] 50 VUs 30m0s
```
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,98 @@
---
title: machine jwt profile grant benchmark of zitadel v2.66.0
sidebar_label: machine jwt profile grant
---
import OutputSource from "./output.json";
import { BenchmarkChart } from '@/components/benchmark_chart';
The tests showed heavy database load by time by the first two database queries. These queries need to be analyzed further.
## Performance test results
| Metric | Value |
|:--------------------------------------|:------|
| Baseline | none |
| Purpose | Test current performance |
| Test start | 15:39 UTC |
| Test duration | 30min |
| Executed test | machine\_jwt\_profile\_grant |
| k6 version | v0.54.0 |
| VUs | 150 |
| Client location | US1 |
| ZITADEL location | US1 |
| ZITADEL container specification | vCPU: 2<br/> Memory: 512 Mib <br/>Container count: 5 |
| ZITADEL Version | v2.66.0 |
| ZITADEL feature flags | webKey: true, improvedPerformance: \[\"IMPROVED\_PERFORMANCE\_ORG\_BY\_ID\", \"IMPROVED\_PERFORMANCE\_PROJECT\", \"IMPROVED\_PERFORMANCE\_USER\_GRANT\", \"IMPROVED\_PERFORMANCE\_ORG\_DOMAIN\_VERIFIED\", \"IMPROVED\_PERFORMANCE\_PROJECT\_GRANT\"\] |
| Database | type: psql<br />version: v15.8 |
| Database location | US1 |
| Database specification | vCPU: 8<br/> memory: 32Gib |
| ZITADEL metrics during test | |
| Observed errors | |
| Top 3 most expensive database queries | 1: Write events using the newly added eventstore.push function<br/>2: Query events by instance\_id, aggregate\_type, aggregate\_id, event\_types<br/>3: Query user<br/> |
| k6 Iterations per second | 439 |
| k6 output | [output](#k6-output) |
| flowchart outcome | Scale out |
## Endpoint latencies
<BenchmarkChart testResults={OutputSource} />
## k6 output
```bash
✓ openid configuration
✗ token status ok
↳ 99% — ✓ 790655 / ✗ 5
✗ access token returned
↳ 99% — ✓ 790655 / ✗ 5
█ setup
✓ user defined
✓ authorize status ok
✓ login name status ok
✓ login shows password page
✓ password status ok
✓ password callback
✓ code set
✓ token status ok
✓ access token created
✓ id token created
✓ info created
✓ org created
✓ create user is status ok
✓ generate machine key status ok
█ teardown
✓ org removed
checks...............................: 99.99% ✓ 1581773 ✗ 10
data_received........................: 1.1 GB 623 kB/s
data_sent............................: 628 MB 347 kB/s
http_req_blocked.....................: min=167ns avg=20.68µs max=493.59ms p(50)=468ns p(95)=717ns p(99)=928ns
http_req_connecting..................: min=0s avg=10.06µs max=388.27ms p(50)=0s p(95)=0s p(99)=0s
http_req_duration....................: min=17.71ms avg=337.34ms max=16.27s p(50)=249.03ms p(95)=888.75ms p(99)=1.4s
{ expected_response:true }.........: min=17.71ms avg=337.29ms max=3.56s p(50)=249.03ms p(95)=888.7ms p(99)=1.4s
http_req_failed......................: 0.00% ✓ 5 ✗ 791265
http_req_receiving...................: min=25.49µs avg=1.58ms max=539.43ms p(50)=89.69µs p(95)=7.55ms p(99)=23.46ms
http_req_sending.....................: min=22.7µs avg=69.14µs max=480.23ms p(50)=59.16µs p(95)=85.15µs p(99)=129.88µs
http_req_tls_handshaking.............: min=0s avg=9.38µs max=98.15ms p(50)=0s p(95)=0s p(99)=0s
http_req_waiting.....................: min=15.11ms avg=335.69ms max=16.26s p(50)=246.91ms p(95)=888.27ms p(99)=1.4s
http_reqs............................: 791270 437.256468/s
iteration_duration...................: min=32.28ms avg=341.46ms max=16.27s p(50)=253ms p(95)=892.49ms p(99)=1.41s
iterations...........................: 790660 436.919382/s
login_ui_enter_login_name_duration...: min=179.27ms avg=179.27ms max=179.27ms p(50)=179.27ms p(95)=179.27ms p(99)=179.27ms
login_ui_enter_password_duration.....: min=17.71ms avg=17.71ms max=17.71ms p(50)=17.71ms p(95)=17.71ms p(99)=17.71ms
login_ui_init_login_duration.........: min=77.66ms avg=77.66ms max=77.66ms p(50)=77.66ms p(95)=77.66ms p(99)=77.66ms
login_ui_token_duration..............: min=86.79ms avg=86.79ms max=86.79ms p(50)=86.79ms p(95)=86.79ms p(99)=86.79ms
oidc_token_duration..................: min=28.38ms avg=337.54ms max=16.27s p(50)=249.17ms p(95)=889.01ms p(99)=1.4s
org_create_org_duration..............: min=44.94ms avg=44.94ms max=44.94ms p(50)=44.94ms p(95)=44.94ms p(99)=44.94ms
user_add_machine_key_duration........: min=38.11ms avg=66.64ms max=160.59ms p(50)=60.28ms p(95)=104.99ms p(99)=112.5ms
user_create_machine_duration.........: min=37.12ms avg=122.76ms max=1.03s p(50)=78.25ms p(95)=266.95ms p(99)=306.94ms
vus..................................: 150 min=0 max=150
vus_max..............................: 150 min=150 max=150
running (30m09.6s), 000/150 VUs, 790660 complete and 0 interrupted iterations
default ✓ [======================================] 150 VUs 30m0s
```
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,98 @@
---
title: machine jwt profile grant benchmark of zitadel v2.70.0
sidebar_label: machine jwt profile grant
---
import OutputSource from "./output.json";
import { BenchmarkChart } from '@/components/benchmark_chart';
The performance goals of [this issue](https://github.com/zitadel/zitadel/issues/8352) are reached. Next we will test linear scalability.
## Performance test results
| Metric | Value |
|:--------------------------------------|:------|
| Baseline | none |
| Purpose | Test current performance |
| Test start | 09:48 UTC |
| Test duration | 30min |
| Executed test | machine\_jwt\_profile\_grant |
| k6 version | v0.57.0 |
| VUs | 600 |
| Client location | US1 |
| ZITADEL location | US1 |
| ZITADEL container specification | vCPU: 6<br/> Memory: 6 Gi <br/>Container min scale: 2<br/>Container max scale: 7 |
| ZITADEL Version | v2.70.0 |
| ZITADEL feature flags | webKey: true, improvedPerformance: \[\"IMPROVED\_PERFORMANCE\_ORG\_BY\_ID\", \"IMPROVED\_PERFORMANCE\_PROJECT\", \"IMPROVED\_PERFORMANCE\_USER\_GRANT\", \"IMPROVED\_PERFORMANCE\_ORG\_DOMAIN\_VERIFIED\", \"IMPROVED\_PERFORMANCE\_PROJECT\_GRANT\"\] |
| Database | type: psql<br />version: v17.2 |
| Database location | US1 |
| Database specification | vCPU: 8<br/> memory: 32Gib |
| ZITADEL metrics during test | |
| Observed errors | |
| Top 3 most expensive database queries | 1: Write events using eventstore.push function<br/>2: Query user<br/>3: Query events by instance id, aggregate type, aggregate id, event types, owner<br/> |
| k6 Iterations per second | 1806 |
| k6 output | [output](#k6-output) |
| flowchart outcome | Scale out |
## Endpoint latencies
<BenchmarkChart testResults={OutputSource} />
## k6 output
```bash
✓ openid configuration
✗ token status ok
↳ 99% — ✓ 3289559 / ✗ 5
✗ access token returned
↳ 99% — ✓ 3289559 / ✗ 5
█ setup
✓ user defined
✓ authorize status ok
✓ login name status ok
✓ login shows password page
✓ password status ok
✓ password callback
✓ code set
✓ token status ok
✓ access token created
✓ id token created
✓ info created
✓ org created
✓ create user is status ok
✓ generate machine key status ok
█ teardown
✓ org removed
checks...............................: 99.99% 6580931 out of 6580941
data_received........................: 4.8 GB 2.6 MB/s
data_sent............................: 2.8 GB 1.5 MB/s
http_req_blocked.....................: min=110ns avg=53.66µs max=937.62ms p(50)=420ns p(95)=660ns p(99)=989ns
http_req_connecting..................: min=0s avg=22.26µs max=532.7ms p(50)=0s p(95)=0s p(99)=0s
http_req_duration....................: min=16.42ms avg=323.71ms max=3.4s p(50)=209.85ms p(95)=903.86ms p(99)=1.01s
{ expected_response:true }.........: min=16.42ms avg=323.71ms max=3.4s p(50)=209.85ms p(95)=903.85ms p(99)=1.01s
http_req_failed......................: 0.00% 5 out of 3291974
http_req_receiving...................: min=18.23µs avg=1.73ms max=913.21ms p(50)=73.15µs p(95)=8.73ms p(99)=29.35ms
http_req_sending.....................: min=17.54µs avg=53.13µs max=501.66ms p(50)=46.07µs p(95)=75.92µs p(99)=120.24µs
http_req_tls_handshaking.............: min=0s avg=29.45µs max=657.29ms p(50)=0s p(95)=0s p(99)=0s
http_req_waiting.....................: min=3.84ms avg=321.92ms max=3.4s p(50)=206.45ms p(95)=901.97ms p(99)=1.01s
http_reqs............................: 3291974 1807.469453/s
iteration_duration...................: min=18.62ms avg=328.21ms max=3.41s p(50)=215.77ms p(95)=907.48ms p(99)=1.01s
iterations...........................: 3289564 1806.146234/s
login_ui_enter_login_name_duration...: min=131.65ms avg=131.65ms max=131.65ms p(50)=131.65ms p(95)=131.65ms p(99)=131.65ms
login_ui_enter_password_duration.....: min=18.55ms avg=18.55ms max=18.55ms p(50)=18.55ms p(95)=18.55ms p(99)=18.55ms
login_ui_init_login_duration.........: min=68.72ms avg=68.72ms max=68.72ms p(50)=68.72ms p(95)=68.72ms p(99)=68.72ms
login_ui_token_duration..............: min=77.56ms avg=77.56ms max=77.56ms p(50)=77.56ms p(95)=77.56ms p(99)=77.56ms
oidc_token_duration..................: min=16.42ms avg=323.79ms max=3.4s p(50)=209.87ms p(95)=903.91ms p(99)=1.01s
org_create_org_duration..............: min=38.42ms avg=38.42ms max=38.42ms p(50)=38.42ms p(95)=38.42ms p(99)=38.42ms
user_add_machine_key_duration........: min=23.47ms avg=280.32ms max=851.94ms p(50)=304.18ms p(95)=437.53ms p(99)=443.37ms
user_create_machine_duration.........: min=63.91ms avg=406.02ms max=663.96ms p(50)=437.61ms p(95)=518.85ms p(99)=521.88ms
vus..................................: 425 min=0 max=600
vus_max..............................: 600 min=600 max=600
running (30m21.3s), 000/600 VUs, 3289564 complete and 0 interrupted iterations
default ✓ [======================================] 600 VUs 30m0s
```
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,106 @@
---
title: oidc session benchmark of Zitadel v2.70.0
sidebar_label: oidc session
---
import OutputSource from "./output.json";
import { BenchmarkChart } from '@/components/benchmark_chart';
The test implements [Support for (OIDC) Standard in a Custom Login UI flow](https://zitadel.com/docs/guides/integrate/login-ui/oidc-standard).
The tests showed that querying the user takes too much time because Zitadel ensures the projection is up to date. This performance bottleneck must be resolved.
## Performance test results
| Metric | Value |
|:--------------------------------------|:------|
| Baseline | none |
| Purpose | Test current performance |
| Test start | 14:24 UTC |
| Test duration | 30min |
| Executed test | oidc\_session |
| k6 version | v0.57.0 |
| VUs | 600 |
| Client location | US1 |
| ZITADEL location | US1 |
| ZITADEL container specification | vCPU: 6<br/> Memory: 6 Gi <br/>Container min scale: 2<br/>Container max scale: 7 |
| ZITADEL Version | v2.70.0 |
| ZITADEL feature flags | webKey: true, improvedPerformance: \[\"IMPROVED\_PERFORMANCE\_ORG\_BY\_ID\", \"IMPROVED\_PERFORMANCE\_PROJECT\", \"IMPROVED\_PERFORMANCE\_USER\_GRANT\", \"IMPROVED\_PERFORMANCE\_ORG\_DOMAIN\_VERIFIED\", \"IMPROVED\_PERFORMANCE\_PROJECT\_GRANT\"\] |
| Database | type: psql<br />version: v17.2 |
| Database location | US1 |
| Database specification | vCPU: 8<br/> memory: 32Gib |
| ZITADEL metrics during test | |
| Observed errors | |
| Top 3 most expensive database queries | 1: lock current\_states table<br/>2: write events<br/>3: get events for projection<br/> |
| k6 Iterations per second | 153 |
| k6 output | [output](#k6-output) |
| flowchart outcome | Resolve locking issue |
## Endpoint latencies
<BenchmarkChart testResults={OutputSource} />
## k6 output
```bash
✓ authorize status ok
✓ auth request id returned
✓ add Session status ok
✓ finalize auth request status ok
█ setup
✓ user defined
✓ authorize status ok
✓ login name status ok
✓ login shows password page
✓ password status ok
✓ password callback
✓ code set
✓ token status ok
✓ access token created
✓ id token created
✓ info created
✓ org created
✓ create user is status ok
✓ generate machine key status ok
✓ member added successful
✓ openid configuration
✓ access token returned
█ teardown
✓ org removed
checks...............................: 100.00% 1097103 out of 1097103
data_received........................: 482 MB 267 kB/s
data_sent............................: 206 MB 114 kB/s
http_req_blocked.....................: min=150ns avg=185.63µs max=639.06ms p(50)=360ns p(95)=790ns p(99)=1.11µs
http_req_connecting..................: min=0s avg=76.84µs max=394.03ms p(50)=0s p(95)=0s p(99)=0s
http_req_duration....................: min=2.27ms avg=1.31s max=6.57s p(50)=326.44ms p(95)=3.94s p(99)=4.28s
{ expected_response:true }.........: min=2.27ms avg=1.31s max=6.57s p(50)=326.44ms p(95)=3.94s p(99)=4.28s
http_req_failed......................: 0.00% 0 out of 823429
http_req_receiving...................: min=22.92µs avg=143.73µs max=245.98ms p(50)=105.17µs p(95)=188.26µs p(99)=260.56µs
http_req_sending.....................: min=22.37µs avg=67.8µs max=41.57ms p(50)=63.65µs p(95)=104.8µs p(99)=138.46µs
http_req_tls_handshaking.............: min=0s avg=106.12µs max=580.5ms p(50)=0s p(95)=0s p(99)=0s
http_req_waiting.....................: min=2.11ms avg=1.31s max=6.57s p(50)=326.17ms p(95)=3.94s p(99)=4.28s
http_reqs............................: 823429 456.440453/s
iteration_duration...................: min=713.37ms avg=3.94s max=8.94s p(50)=3.92s p(95)=4.98s p(99)=5.44s
iterations...........................: 274271 152.032998/s
login_ui_enter_login_name_duration...: min=113.75ms avg=113.75ms max=113.75ms p(50)=113.75ms p(95)=113.75ms p(99)=113.75ms
login_ui_enter_password_duration.....: min=2.27ms avg=2.27ms max=2.27ms p(50)=2.27ms p(95)=2.27ms p(99)=2.27ms
login_ui_init_login_duration.........: min=20.48ms avg=156.67ms max=6.57s p(50)=126.64ms p(95)=280.16ms p(99)=675.36ms
login_ui_token_duration..............: min=68.53ms avg=68.53ms max=68.53ms p(50)=68.53ms p(95)=68.53ms p(99)=68.53ms
membership_iam_member................: min=34.16ms avg=34.16ms max=34.16ms p(50)=34.16ms p(95)=34.16ms p(99)=34.16ms
oidc_auth_requst_by_id_duration......: min=20.59ms avg=370.56ms max=2.87s p(50)=294.12ms p(95)=911.42ms p(99)=1.08s
oidc_session_duration................: min=713ms avg=3.94s max=8.94s p(50)=3.92s p(95)=4.98s p(99)=5.44s
oidc_token_duration..................: min=40.67ms avg=40.67ms max=40.67ms p(50)=40.67ms p(95)=40.67ms p(99)=40.67ms
org_create_org_duration..............: min=48.92ms avg=48.92ms max=48.92ms p(50)=48.92ms p(95)=48.92ms p(99)=48.92ms
session_add_session_duration.........: min=92.04ms avg=3.4s max=6.16s p(50)=3.46s p(95)=4.18s p(99)=4.48s
user_add_machine_key_duration........: min=32.08ms avg=32.08ms max=32.08ms p(50)=32.08ms p(95)=32.08ms p(99)=32.08ms
user_create_machine_duration.........: min=91.73ms avg=91.73ms max=91.73ms p(50)=91.73ms p(95)=91.73ms p(99)=91.73ms
vus..................................: 82 min=0 max=600
vus_max..............................: 600 min=600 max=600
running (30m04.0s), 000/600 VUs, 274271 complete and 0 interrupted iterations
default ✓ [======================================] 600 VUs 30m0s
```
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,97 @@
---
title: create session benchmark of zitadel v4
sidebar_label: create session
---
import OutputSource from "./output.json";
import { BenchmarkChart } from '@/components/benchmark_chart';
Benchmark results of v4 release of Zitadel.
## Performance test results
| Metric | Value |
|:--------------------------------------|:------|
| Baseline | none |
| Purpose | Test current performance |
| Test start | 11:31 UTC |
| Test duration | 30min |
| Executed test | add\_session |
| k6 version | v1.0.0-rc1 |
| VUs | 600 |
| Client location | US1 |
| ZITADEL location | US1 |
| ZITADEL container specification | vCPU: 6<br/> Memory: 6 Gi <br/>Container min scale: 2<br/>Container max scale: 7 |
| ZITADEL Version | v4.0.0-rc2 |
| ZITADEL feature flags | webKey: true, improvedPerformance: \[\"IMPROVED\_PERFORMANCE\_ORG\_BY\_ID\", \"IMPROVED\_PERFORMANCE\_PROJECT\", \"IMPROVED\_PERFORMANCE\_USER\_GRANT\", \"IMPROVED\_PERFORMANCE\_ORG\_DOMAIN\_VERIFIED\", \"IMPROVED\_PERFORMANCE\_PROJECT\_GRANT\"\] |
| Database | type: psql<br />version: v17.4 |
| Database location | US1 |
| Database specification | vCPU: 8<br/> memory: 32Gib |
| ZITADEL metrics during test | |
| Observed errors | |
| Top 3 most expensive database queries | 1: Query events by instance_id, aggregate_types, event_types, position<br/>2: write events<br/>3: list members<br/> |
| k6 Iterations per second | 607 |
| k6 output | [output](#k6-output) |
| flowchart outcome | |
## Endpoint latencies
Until :37 were containers starting because it was a cold start test.
<BenchmarkChart testResults={OutputSource} />
## k6 output
```bash
█ TOTAL RESULTS
checks_total.......................: 2244565 1213.769815/s
checks_succeeded...................: 99.99% 2244563 out of 2244565
checks_failed......................: 0.00% 2 out of 2244565
✓ user defined
✓ authorize status ok
✓ login name status ok
✓ login shows password page
✓ password status ok
✓ password callback
✓ code set
✓ token status ok
✓ access token created
✓ id token created
✓ info created
✓ org created
✓ create user is status ok
✗ add Session status ok
↳ 99% — ✓ 1121975 / ✗ 2
✓ add session is status ok
✓ org removed
CUSTOM
add_session_duration....................................................: min=236ms avg=962.51ms max=3.66s p(50)=951ms p(95)=1.33s p(99)=1.55s
login_ui_enter_login_name_duration......................................: min=184.04ms avg=184.04ms max=184.04ms p(50)=184.04ms p(95)=184.04ms p(99)=184.04ms
login_ui_enter_password_duration........................................: min=15.68ms avg=15.68ms max=15.68ms p(50)=15.68ms p(95)=15.68ms p(99)=15.68ms
login_ui_init_login_duration............................................: min=128.36ms avg=128.36ms max=128.36ms p(50)=128.36ms p(95)=128.36ms p(99)=128.36ms
login_ui_token_duration.................................................: min=87.2ms avg=87.2ms max=87.2ms p(50)=87.2ms p(95)=87.2ms p(99)=87.2ms
org_create_org_duration.................................................: min=76.73ms avg=76.73ms max=76.73ms p(50)=76.73ms p(95)=76.73ms p(99)=76.73ms
session_add_session_duration............................................: min=0s avg=961.43ms max=3.66s p(50)=950.06ms p(95)=1.33s p(99)=1.55s
user_create_human_duration..............................................: min=548.74ms avg=10.69s max=16.39s p(50)=13.39s p(95)=15.97s p(99)=16.28s
HTTP
http_req_duration.......................................................: min=0s avg=966.15ms max=18.87s p(50)=950.04ms p(95)=1.33s p(99)=1.55s
{ expected_response:true }............................................: min=15.68ms avg=966.15ms max=18.87s p(50)=950.04ms p(95)=1.33s p(99)=1.55s
http_req_failed.........................................................: 0.00% 2 out of 1123187
http_reqs...............................................................: 1123187 607.374025/s
EXECUTION
iteration_duration......................................................: min=1.63ms avg=962.61ms max=3.66s p(50)=951.17ms p(95)=1.33s p(99)=1.55s
iterations..............................................................: 1121977 606.719706/s
vus.....................................................................: 0 min=0 max=600
vus_max.................................................................: 600 min=600 max=600
NETWORK
data_received...........................................................: 617 MB 334 kB/s
data_sent...............................................................: 184 MB 100 kB/s
running (30m49.3s), 000/600 VUs, 1121977 complete and 0 interrupted iterations
default ✓ [======================================] 600 VUs 30m0s
```
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,104 @@
---
title: human password login benchmark of zitadel v4
sidebar_label: human password login
---
import OutputSource from "./output.json";
import { BenchmarkChart } from '@/components/benchmark_chart';
Benchmark results of v4 release of Zitadel.
This benchmark tests the flow of a user signing in to Zitadel using username and password which is a highly CPU intensive task for checking the password.
## Performance test results
| Metric | Value |
|:--------------------------------------|:------|
| Baseline | none |
| Purpose | Test current performance |
| Test start | 06:43 UTC |
| Test duration | 30min |
| Executed test | human\_password\_login |
| k6 version | v1.0.0-rc1 |
| VUs | 600 |
| Client location | US1 |
| ZITADEL location | US1 |
| ZITADEL container specification | vCPU: 6<br/> Memory: 6 Gi <br/>Container min scale: 2<br/>Container max scale: 7 |
| ZITADEL Version | v4.0.0-rc2 |
| ZITADEL feature flags | webKey: true, improvedPerformance: \[\"IMPROVED\_PERFORMANCE\_ORG\_BY\_ID\", \"IMPROVED\_PERFORMANCE\_PROJECT\", \"IMPROVED\_PERFORMANCE\_USER\_GRANT\", \"IMPROVED\_PERFORMANCE\_ORG\_DOMAIN\_VERIFIED\", \"IMPROVED\_PERFORMANCE\_PROJECT\_GRANT\"\] |
| Database | type: psql<br />version: v17.4 |
| Database location | US1 |
| Database specification | vCPU: 8<br/> memory: 32Gib |
| ZITADEL metrics during test | |
| Observed errors | |
| Top 3 most expensive database queries | 1: Locking projections for reduce<br/>2: Query events by aggregate type and event type<br/>3: Query events by instance id, aggregate types, event types, position<br/> |
| k6 Iterations per second | 39 |
| k6 output | [output](#k6-output) |
| flowchart outcome | |
## Endpoint latencies
Until :45 were containers starting because it was a cold start test.
<BenchmarkChart testResults={OutputSource} />
## k6 output
```bash
█ TOTAL RESULTS
checks_total.......................: 867495 464.541713/s
checks_succeeded...................: 99.88% 866490 out of 867495
checks_failed......................: 0.11% 1005 out of 867495
✓ user defined
✗ authorize status ok
↳ 99% — ✓ 72470 / ✗ 435
✗ login name status ok
↳ 99% — ✓ 72331 / ✗ 139
✓ login shows password page
✗ password status ok
↳ 99% — ✓ 72032 / ✗ 155
✗ password callback
↳ 99% — ✓ 72000 / ✗ 32
✓ code set
✗ token status ok
↳ 99% — ✓ 71866 / ✗ 134
✓ access token created
✓ id token created
✓ info created
✓ org created
✓ create user is status ok
✗ openid configuration
↳ 98% — ✓ 600 / ✗ 11
✗ userinfo status ok
↳ 99% — ✓ 71755 / ✗ 99
✓ org removed
CUSTOM
human_password_login_duration...........................................: min=984ms avg=14.73s max=58.88s p(50)=12.41s p(95)=36.8s p(99)=40.55s
login_ui_enter_login_name_duration......................................: min=98.28ms avg=10.57s max=38.72s p(50)=5.99s p(95)=32.4s p(99)=36.16s
login_ui_enter_password_duration........................................: min=1.12ms avg=119.12ms max=1m0s p(50)=2.15ms p(95)=3.96ms p(99)=3.8s
login_ui_init_login_duration............................................: min=41.73ms avg=600.61ms max=22.51s p(50)=292.69ms p(95)=1.84s p(99)=2.82s
login_ui_token_duration.................................................: min=53.03ms avg=712.39ms max=21.76s p(50)=339.01ms p(95)=2.11s p(99)=3.32s
oidc_user_info_duration.................................................: min=14.54ms avg=187.13ms max=21.51s p(50)=74.47ms p(95)=398.78ms p(99)=619.27ms
org_create_org_duration.................................................: min=50.09ms avg=50.09ms max=50.09ms p(50)=50.09ms p(95)=50.09ms p(99)=50.09ms
user_create_human_duration..............................................: min=425.26ms avg=11.83s max=16.89s p(50)=13.67s p(95)=16.07s p(99)=16.78s
HTTP
http_req_duration.......................................................: min=1.12ms avg=1.87s max=1m0s p(50)=257.41ms p(95)=12.28s p(99)=31.18s
{ expected_response:true }............................................: min=1.12ms avg=1.84s max=38.72s p(50)=256.14ms p(95)=11.83s p(99)=31.19s
http_req_failed.........................................................: 0.19% 1117 out of 581385
http_reqs...............................................................: 581385 311.330421/s
EXECUTION
iteration_duration......................................................: min=983.33ms avg=14.88s max=1m11s p(50)=12.64s p(95)=36.85s p(99)=40.62s
iterations..............................................................: 72904 39.039936/s
vus.....................................................................: 18 min=0 max=600
vus_max.................................................................: 600 min=600 max=600
NETWORK
data_received...........................................................: 1.2 GB 658 kB/s
data_sent...............................................................: 235 MB 126 kB/s
running (31m07.4s), 000/600 VUs, 72904 complete and 0 interrupted iterations
```
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,98 @@
---
title: introspection benchmark of zitadel v4
sidebar_label: introspection
---
import OutputSource from "./output.json";
import { BenchmarkChart } from '@/components/benchmark_chart';
Benchmark results of v4 release of Zitadel.
## Performance test results
| Metric | Value |
|:--------------------------------------|:------|
| Baseline | none |
| Purpose | Test current performance |
| Test start | 13:33 UTC |
| Test duration | 30min |
| Executed test | introspect |
| k6 version | v1.0.0-rc1 |
| VUs | 600 |
| Client location | US1 |
| ZITADEL location | US1 |
| ZITADEL container specification | vCPU: 6<br/> Memory: 6 Gi <br/>Container min scale: 2<br/>Container max scale: 7 |
| ZITADEL Version | v4.0.0-rc2 |
| ZITADEL feature flags | webKey: true, improvedPerformance: \[\"IMPROVED\_PERFORMANCE\_ORG\_BY\_ID\", \"IMPROVED\_PERFORMANCE\_PROJECT\", \"IMPROVED\_PERFORMANCE\_USER\_GRANT\", \"IMPROVED\_PERFORMANCE\_ORG\_DOMAIN\_VERIFIED\", \"IMPROVED\_PERFORMANCE\_PROJECT\_GRANT\"\] |
| Database | type: psql<br />version: v17.4 |
| Database location | US1 |
| Database specification | vCPU: 8<br/> memory: 32Gib |
| ZITADEL metrics during test | |
| Observed errors | A lot of over fetching on the database |
| Top 3 most expensive database queries | 1: get app<br/>2: Query events by instance id, aggregate types, event types, position<br/>3: Query executions<br/> |
| k6 Iterations per second | 18 |
| k6 output | [output](#k6-output) |
| flowchart outcome | |
## Endpoint latencies
<BenchmarkChart testResults={OutputSource} />
## k6 output
```bash
█ TOTAL RESULTS
checks_total.......................: 36207 19.664102/s
checks_succeeded...................: 99.99% 36205 out of 36207
checks_failed......................: 0.00% 2 out of 36207
✓ user defined
✓ authorize status ok
✓ login name status ok
✓ login shows password page
✓ password status ok
✓ password callback
✓ code set
✓ token status ok
✓ access token created
✓ id token created
✓ info created
✓ org created
✓ add project status ok
✓ add api status ok
✓ add app key status ok
✓ openid configuration
✗ introspect status ok
↳ 99% — ✓ 33792 / ✗ 2
✓ org removed
CUSTOM
app_add_app_duration....................................................: min=32.73ms avg=779.27ms max=1.11s p(50)=771.76ms p(95)=1.08s p(99)=1.1s
app_add_app_key_duration................................................: min=340.78ms avg=12.14s max=17.19s p(50)=13.51s p(95)=16.78s p(99)=16.93s
login_ui_enter_login_name_duration......................................: min=212.91ms avg=212.91ms max=212.91ms p(50)=212.91ms p(95)=212.91ms p(99)=212.91ms
login_ui_enter_password_duration........................................: min=29.53ms avg=29.53ms max=29.53ms p(50)=29.53ms p(95)=29.53ms p(99)=29.53ms
login_ui_init_login_duration............................................: min=65.24ms avg=65.24ms max=65.24ms p(50)=65.24ms p(95)=65.24ms p(99)=65.24ms
login_ui_token_duration.................................................: min=98.54ms avg=98.54ms max=98.54ms p(50)=98.54ms p(95)=98.54ms p(99)=98.54ms
oidc_introspect_duration................................................: min=731.31µs avg=32.04s max=50.94s p(50)=28.25s p(95)=47.83s p(99)=49.39s
org_create_org_duration.................................................: min=51.76ms avg=51.76ms max=51.76ms p(50)=51.76ms p(95)=51.76ms p(99)=51.76ms
project_add_project_duration............................................: min=52.11ms avg=754.58ms max=1.2s p(50)=778.66ms p(95)=1.14s p(99)=1.19s
HTTP
http_req_duration.......................................................: min=731.31µs avg=30.22s max=50.94s p(50)=26.12s p(95)=47.74s p(99)=49.35s
{ expected_response:true }............................................: min=13.11ms avg=30.22s max=50.94s p(50)=26.12s p(95)=47.74s p(99)=49.35s
http_req_failed.........................................................: 0.00% 2 out of 36204
http_reqs...............................................................: 36204 19.662473/s
EXECUTION
iteration_duration......................................................: min=1.53ms avg=32.14s max=50.94s p(50)=28.25s p(95)=47.83s p(99)=49.39s
iterations..............................................................: 33794 18.353597/s
vus.....................................................................: 0 min=0 max=600
vus_max.................................................................: 600 min=600 max=600
NETWORK
data_received...........................................................: 25 MB 14 kB/s
data_sent...............................................................: 33 MB 18 kB/s
running (30m41.3s), 000/600 VUs, 33794 complete and 0 interrupted iterations
default ✓ [======================================] 600 VUs 30m0s
```
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,103 @@
---
title: machine client credentials login benchmark of zitadel v4
sidebar_label: machine client credentials login
---
import OutputSource from "./output.json";
import { BenchmarkChart } from '@/components/benchmark_chart';
Benchmark results of v4 release of Zitadel.
## Performance test results
| Metric | Value |
|:--------------------------------------|:------|
| Baseline | none |
| Purpose | Test current performance |
| Test start | 08:28 UTC |
| Test duration | 30min |
| Executed test | machine\_client\_credentials\_login |
| k6 version | v1.0.0-rc1 |
| VUs | 600 |
| Client location | US1 |
| ZITADEL location | US1 |
| ZITADEL container specification | vCPU: 6<br/> Memory: 6 Gi <br/>Container min scale: 2<br/>Container max scale: 7 |
| ZITADEL Version | v4.0.0-rc2 |
| ZITADEL feature flags | webKey: true, improvedPerformance: \[\"IMPROVED\_PERFORMANCE\_ORG\_BY\_ID\", \"IMPROVED\_PERFORMANCE\_PROJECT\", \"IMPROVED\_PERFORMANCE\_USER\_GRANT\", \"IMPROVED\_PERFORMANCE\_ORG\_DOMAIN\_VERIFIED\", \"IMPROVED\_PERFORMANCE\_PROJECT\_GRANT\"\] |
| Database | type: psql<br />version: v17.4 |
| Database location | US1 |
| Database specification | vCPU: 8<br/> memory: 32Gib |
| ZITADEL metrics during test | |
| Observed errors | |
| Top 3 most expensive database queries | 1: Locking projections for reduce<br/>2: Query events by aggregate type and event type<br/>3: user by id query<br/> |
| k6 Iterations per second | 424 |
| k6 output | [output](#k6-output) |
| flowchart outcome | |
## Endpoint latencies
Until :37 were containers starting because it was a cold start test.
<BenchmarkChart testResults={OutputSource} />
## k6 output
```bash
█ TOTAL RESULTS
checks_total.......................: 2336254 1274.250394/s
checks_succeeded...................: 99.99% 2336250 out of 2336254
checks_failed......................: 0.00% 4 out of 2336254
✓ user defined
✓ authorize status ok
✓ login name status ok
✓ login shows password page
✓ password status ok
✓ password callback
✓ code set
✓ token status ok
✓ access token created
✓ id token created
✓ info created
✓ org created
✓ create user is status ok
✓ generate machine secret status ok
✓ openid configuration
✗ client credentials status ok
↳ 99% — ✓ 778146 / ✗ 3
✓ client credentials token ok
✗ userinfo status ok
↳ 99% — ✓ 778145 / ✗ 1
✓ org removed
CUSTOM
login_ui_enter_login_name_duration......................................: min=191.97ms avg=191.97ms max=191.97ms p(50)=191.97ms p(95)=191.97ms p(99)=191.97ms
login_ui_enter_password_duration........................................: min=14.65ms avg=14.65ms max=14.65ms p(50)=14.65ms p(95)=14.65ms p(99)=14.65ms
login_ui_init_login_duration............................................: min=91.36ms avg=91.36ms max=91.36ms p(50)=91.36ms p(95)=91.36ms p(99)=91.36ms
login_ui_token_duration.................................................: min=93.72ms avg=93.72ms max=93.72ms p(50)=93.72ms p(95)=93.72ms p(99)=93.72ms
oidc_client_credentials_duration........................................: min=244.96µs avg=794.35ms max=2.22s p(50)=783.9ms p(95)=1.1s p(99)=1.23s
oidc_user_info_duration.................................................: min=578.67µs avg=592.86ms max=1.98s p(50)=583.33ms p(95)=840.94ms p(99)=939.62ms
org_create_org_duration.................................................: min=60.95ms avg=60.95ms max=60.95ms p(50)=60.95ms p(95)=60.95ms p(99)=60.95ms
user_add_machine_secret_duration........................................: min=54.63ms avg=2.62s max=4.92s p(50)=2.67s p(95)=4.69s p(99)=4.87s
user_create_machine_duration............................................: min=159.68ms avg=681.87ms max=868.85ms p(50)=746.38ms p(95)=840.65ms p(99)=859.33ms
HTTP
http_req_duration.......................................................: min=244.96µs avg=693.86ms max=4.92s p(50)=671.37ms p(95)=1.05s p(99)=1.18s
{ expected_response:true }............................................: min=14.27ms avg=693.86ms max=4.92s p(50)=671.37ms p(95)=1.05s p(99)=1.18s
http_req_failed.........................................................: 0.00% 4 out of 1558705
http_reqs...............................................................: 1558705 850.156045/s
EXECUTION
iteration_duration......................................................: min=656.54µs avg=1.38s max=3.43s p(50)=1.38s p(95)=1.8s p(99)=2.01s
iterations..............................................................: 778149 424.421604/s
vus.....................................................................: 292 min=0 max=600
vus_max.................................................................: 600 min=600 max=600
NETWORK
data_received...........................................................: 1.6 GB 898 kB/s
data_sent...............................................................: 528 MB 288 kB/s
running (30m33.4s), 000/600 VUs, 778149 complete and 0 interrupted iterations
default ✓ [======================================] 600 VUs 30m0s
```
@@ -0,0 +1,95 @@
---
title: machine jwt profile grant benchmark of zitadel v4
sidebar_label: machine jwt profile grant
---
import OutputSource from "./output.json";
import { BenchmarkChart } from '@/components/benchmark_chart';
Benchmark results of v4 release of Zitadel.
## Performance test results
| Metric | Value |
|:--------------------------------------|:------|
| Baseline | none |
| Purpose | Test current performance |
| Test start | 18:33 UTC |
| Test duration | 30min |
| Executed test | machine\_jwt\_profile\_grant |
| k6 version | v1.0.0-rc1 |
| VUs | 600 |
| Client location | US1 |
| ZITADEL location | US1 |
| ZITADEL container specification | vCPU: 6<br/> Memory: 6 Gi <br/>Container min scale: 2<br/>Container max scale: 7 |
| ZITADEL Version | v4.0.0-rc2 |
| ZITADEL feature flags | webKey: true, improvedPerformance: \[\"IMPROVED\_PERFORMANCE\_ORG\_BY\_ID\", \"IMPROVED\_PERFORMANCE\_PROJECT\", \"IMPROVED\_PERFORMANCE\_USER\_GRANT\", \"IMPROVED\_PERFORMANCE\_ORG\_DOMAIN\_VERIFIED\", \"IMPROVED\_PERFORMANCE\_PROJECT\_GRANT\"\] |
| Database | type: psql<br />version: v17.4 |
| Database location | US1 |
| Database specification | vCPU: 8<br/> memory: 32Gib |
| ZITADEL metrics during test | |
| Observed errors | Zitadel containers did not scale but database would have headroom |
| Top 3 most expensive database queries | 1: Query events by instance id, aggregate types, event types, position<br/>2: write events<br/>3: get user by id<br/> |
| k6 Iterations per second | 851 |
| k6 output | [output](#k6-output) |
| flowchart outcome | |
## Endpoint latencies
<BenchmarkChart testResults={OutputSource} />
## k6 output
```bash
█ TOTAL RESULTS
checks_total.......................: 3128039 1702.561961/s
checks_succeeded...................: 100.00% 3128039 out of 3128039
checks_failed......................: 0.00% 0 out of 3128039
✓ user defined
✓ authorize status ok
✓ login name status ok
✓ login shows password page
✓ password status ok
✓ password callback
✓ code set
✓ token status ok
✓ access token created
✓ id token created
✓ info created
✓ org created
✓ create user is status ok
✓ generate machine key status ok
✓ openid configuration
✓ access token returned
✓ org removed
CUSTOM
login_ui_enter_login_name_duration......................................: min=185.23ms avg=185.23ms max=185.23ms p(50)=185.23ms p(95)=185.23ms p(99)=185.23ms
login_ui_enter_password_duration........................................: min=19.29ms avg=19.29ms max=19.29ms p(50)=19.29ms p(95)=19.29ms p(99)=19.29ms
login_ui_init_login_duration............................................: min=103.07ms avg=103.07ms max=103.07ms p(50)=103.07ms p(95)=103.07ms p(99)=103.07ms
login_ui_token_duration.................................................: min=147.4ms avg=147.4ms max=147.4ms p(50)=147.4ms p(95)=147.4ms p(99)=147.4ms
oidc_token_duration.....................................................: min=58.6ms avg=688.05ms max=3.95s p(50)=470.3ms p(95)=1.16s p(99)=1.24s
org_create_org_duration.................................................: min=88.83ms avg=88.83ms max=88.83ms p(50)=88.83ms p(95)=88.83ms p(99)=88.83ms
user_add_machine_key_duration...........................................: min=29.55ms avg=481.01ms max=742.31ms p(50)=487.76ms p(95)=728.51ms p(99)=739.94ms
user_create_machine_duration............................................: min=88.42ms avg=748.16ms max=4.46s p(50)=732.58ms p(95)=1.01s p(99)=1.03s
HTTP
http_req_duration.......................................................: min=19.29ms avg=687.53ms max=4.46s p(50)=470.17ms p(95)=1.16s p(99)=1.24s
{ expected_response:true }............................................: min=19.29ms avg=687.53ms max=4.46s p(50)=470.17ms p(95)=1.16s p(99)=1.24s
http_req_failed.........................................................: 0.00% 0 out of 1565523
http_reqs...............................................................: 1565523 852.099321/s
EXECUTION
iteration_duration......................................................: min=61.7ms avg=690.96ms max=3.96s p(50)=473.07ms p(95)=1.16s p(99)=1.25s
iterations..............................................................: 1563113 850.787581/s
vus.....................................................................: 1 min=0 max=600
vus_max.................................................................: 600 min=600 max=600
NETWORK
data_received...........................................................: 2.3 GB 1.2 MB/s
data_sent...............................................................: 1.3 GB 725 kB/s
running (30m37.3s), 000/600 VUs, 1563113 complete and 0 interrupted iterations
default ✓ [======================================] 600 VUs 30m0s
```
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,102 @@
---
title: machine pat login benchmark of zitadel v4
sidebar_label: machine pat login
---
import OutputSource from "./output.json";
import { BenchmarkChart } from '@/components/benchmark_chart';
Benchmark results of v4 release of Zitadel.
## Performance test results
| Metric | Value |
|:--------------------------------------|:------|
| Baseline | none |
| Purpose | Test current performance |
| Test start | 07:34 UTC |
| Test duration | 30min |
| Executed test | machine\_pat\_login |
| k6 version | v1.0.0-rc1 |
| VUs | 600 |
| Client location | US1 |
| ZITADEL location | US1 |
| ZITADEL container specification | vCPU: 6<br/> Memory: 6 Gi <br/>Container min scale: 2<br/>Container max scale: 7 |
| ZITADEL Version | v4.0.0-rc2 |
| ZITADEL feature flags | webKey: true, improvedPerformance: \[\"IMPROVED\_PERFORMANCE\_ORG\_BY\_ID\", \"IMPROVED\_PERFORMANCE\_PROJECT\", \"IMPROVED\_PERFORMANCE\_USER\_GRANT\", \"IMPROVED\_PERFORMANCE\_ORG\_DOMAIN\_VERIFIED\", \"IMPROVED\_PERFORMANCE\_PROJECT\_GRANT\"\] |
| Database | type: psql<br />version: v17.4 |
| Database location | US1 |
| Database specification | vCPU: 8<br/> memory: 32Gib |
| ZITADEL metrics during test | |
| Observed errors | |
| Top 3 most expensive database queries | 1: Locking projections for reduce<br/>2: Query events by aggregate type and event type<br/>3: Query events by instance id, aggregate types, event types, position<br/> |
| k6 Iterations per second | 1906 |
| k6 output | [output](#k6-output) |
| flowchart outcome | |
## Endpoint latencies
Until :37 were containers starting because it was a cold start test.
<BenchmarkChart testResults={OutputSource} />
## k6 output
```bash
█ TOTAL RESULTS
checks_total.......................: 867495 464.541713/s
checks_succeeded...................: 99.88% 866490 out of 867495
checks_failed......................: 0.11% 1005 out of 867495
✓ user defined
✗ authorize status ok
↳ 99% — ✓ 72470 / ✗ 435
✗ login name status ok
↳ 99% — ✓ 72331 / ✗ 139
✓ login shows password page
✗ password status ok
↳ 99% — ✓ 72032 / ✗ 155
✗ password callback
↳ 99% — ✓ 72000 / ✗ 32
✓ code set
✗ token status ok
↳ 99% — ✓ 71866 / ✗ 134
✓ access token created
✓ id token created
✓ info created
✓ org created
✓ create user is status ok
✗ openid configuration
↳ 98% — ✓ 600 / ✗ 11
✗ userinfo status ok
↳ 99% — ✓ 71755 / ✗ 99
✓ org removed
CUSTOM
human_password_login_duration...........................................: min=984ms avg=14.73s max=58.88s p(50)=12.41s p(95)=36.8s p(99)=40.55s
login_ui_enter_login_name_duration......................................: min=98.28ms avg=10.57s max=38.72s p(50)=5.99s p(95)=32.4s p(99)=36.16s
login_ui_enter_password_duration........................................: min=1.12ms avg=119.12ms max=1m0s p(50)=2.15ms p(95)=3.96ms p(99)=3.8s
login_ui_init_login_duration............................................: min=41.73ms avg=600.61ms max=22.51s p(50)=292.69ms p(95)=1.84s p(99)=2.82s
login_ui_token_duration.................................................: min=53.03ms avg=712.39ms max=21.76s p(50)=339.01ms p(95)=2.11s p(99)=3.32s
oidc_user_info_duration.................................................: min=14.54ms avg=187.13ms max=21.51s p(50)=74.47ms p(95)=398.78ms p(99)=619.27ms
org_create_org_duration.................................................: min=50.09ms avg=50.09ms max=50.09ms p(50)=50.09ms p(95)=50.09ms p(99)=50.09ms
user_create_human_duration..............................................: min=425.26ms avg=11.83s max=16.89s p(50)=13.67s p(95)=16.07s p(99)=16.78s
HTTP
http_req_duration.......................................................: min=1.12ms avg=1.87s max=1m0s p(50)=257.41ms p(95)=12.28s p(99)=31.18s
{ expected_response:true }............................................: min=1.12ms avg=1.84s max=38.72s p(50)=256.14ms p(95)=11.83s p(99)=31.19s
http_req_failed.........................................................: 0.19% 1117 out of 581385
http_reqs...............................................................: 581385 311.330421/s
EXECUTION
iteration_duration......................................................: min=983.33ms avg=14.88s max=1m11s p(50)=12.64s p(95)=36.85s p(99)=40.62s
iterations..............................................................: 72904 39.039936/s
vus.....................................................................: 18 min=0 max=600
vus_max.................................................................: 600 min=600 max=600
NETWORK
data_received...........................................................: 1.2 GB 658 kB/s
data_sent...............................................................: 235 MB 126 kB/s
running (31m07.4s), 000/600 VUs, 72904 complete and 0 interrupted iterations
```
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,102 @@
---
title: manipulate user benchmark of zitadel v4
sidebar_label: manipulate user
---
import OutputSource from "./output.json";
import { BenchmarkChart } from '@/components/benchmark_chart';
Benchmark results of v4 release of Zitadel.
## Performance test results
| Metric | Value |
|:--------------------------------------|:------|
| Baseline | none |
| Purpose | Test current performance |
| Test start | 15:07 UTC |
| Test duration | 30min |
| Executed test | manipulate\_user |
| k6 version | v1.0.0-rc1 |
| VUs | 600 |
| Client location | US1 |
| ZITADEL location | US1 |
| ZITADEL container specification | vCPU: 6<br/> Memory: 6 Gi <br/>Container min scale: 2<br/>Container max scale: 7 |
| ZITADEL Version | v4.0.0-rc2 |
| ZITADEL feature flags | webKey: true, improvedPerformance: \[\"IMPROVED\_PERFORMANCE\_ORG\_BY\_ID\", \"IMPROVED\_PERFORMANCE\_PROJECT\", \"IMPROVED\_PERFORMANCE\_USER\_GRANT\", \"IMPROVED\_PERFORMANCE\_ORG\_DOMAIN\_VERIFIED\", \"IMPROVED\_PERFORMANCE\_PROJECT\_GRANT\"\] |
| Database | type: psql<br />version: v17.4 |
| Database location | US1 |
| Database specification | vCPU: 8<br/> memory: 32Gib |
| ZITADEL metrics during test | |
| Observed errors | |
| Top 3 most expensive database queries | 1: query events by instance id, aggregate types, event types, position<br/>2: write events<br/>3: query user by id<br/> |
| k6 Iterations per second | 16 |
| k6 output | [output](#k6-output) |
| flowchart outcome | |
## Endpoint latencies
Each iteration creates, updates and deletes a user.
<BenchmarkChart testResults={OutputSource} />
## k6 output
```bash
█ TOTAL RESULTS
checks_total.......................: 199261 108.759178/s
checks_succeeded...................: 57.15% 113882 out of 199261
checks_failed......................: 42.84% 85379 out of 199261
✓ user defined
✓ authorize status ok
✓ login name status ok
✓ login shows password page
✓ password status ok
✓ password callback
✓ code set
✓ token status ok
✓ access token created
✓ id token created
✓ info created
✓ org created
✗ create user is status ok
↳ 99% — ✓ 28528 / ✗ 4
✗ update user is status ok
↳ 25% — ✓ 28469 / ✗ 85359
✗ lock user is status ok
↳ 99% — ✓ 28460 / ✗ 8
✗ delete user is status ok
↳ 99% — ✓ 28412 / ✗ 8
✓ org removed
CUSTOM
delete_user_duration....................................................: min=109.24ms avg=3.48s max=1m0s p(50)=2s p(95)=7.65s p(99)=9.78s
lock_user_duration......................................................: min=77.95ms avg=2.7s max=1m0s p(50)=1.1s p(95)=6.36s p(99)=7.86s
login_ui_enter_login_name_duration......................................: min=175.39ms avg=175.39ms max=175.39ms p(50)=175.39ms p(95)=175.39ms p(99)=175.39ms
login_ui_enter_password_duration........................................: min=34.12ms avg=34.12ms max=34.12ms p(50)=34.12ms p(95)=34.12ms p(99)=34.12ms
login_ui_init_login_duration............................................: min=100.5ms avg=100.5ms max=100.5ms p(50)=100.5ms p(95)=100.5ms p(99)=100.5ms
login_ui_token_duration.................................................: min=154.05ms avg=154.05ms max=154.05ms p(50)=154.05ms p(95)=154.05ms p(99)=154.05ms
org_create_org_duration.................................................: min=71.03ms avg=71.03ms max=71.03ms p(50)=71.03ms p(95)=71.03ms p(99)=71.03ms
update_human_duration...................................................: min=70.77ms avg=2.43s max=1m0s p(50)=807.73ms p(95)=5.99s p(99)=7.36s
user_create_human_duration..............................................: min=503.01ms avg=3.69s max=1m0s p(50)=2.07s p(95)=7.66s p(99)=13.98s
HTTP
http_req_duration.......................................................: min=34.12ms avg=7.64s max=1m0s p(50)=4.07s p(95)=32.29s p(99)=41.1s
{ expected_response:true }............................................: min=34.12ms avg=7.63s max=54.32s p(50)=4.07s p(95)=32.26s p(99)=41.03s
http_req_failed.........................................................: 0.02% 31 out of 142384
http_reqs...............................................................: 142384 77.714991/s
EXECUTION
iteration_duration......................................................: min=17.1s avg=38.22s max=1m30s p(50)=36.76s p(95)=58.22s p(99)=1m7s
iterations..............................................................: 28434 15.519637/s
vus.....................................................................: 0 min=0 max=600
vus_max.................................................................: 600 min=600 max=600
NETWORK
data_received...........................................................: 82 MB 45 kB/s
data_sent...............................................................: 26 MB 14 kB/s
running (30m32.1s), 000/600 VUs, 28434 complete and 98 interrupted iterations
default ✓ [======================================] 600 VUs 30m0s
```
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,104 @@
---
title: oidc session benchmark of zitadel v4
sidebar_label: oidc session
---
import OutputSource from "./output.json";
import { BenchmarkChart } from '@/components/benchmark_chart';
Benchmark results of v4 release of Zitadel.
## Performance test results
| Metric | Value |
|:--------------------------------------|:------|
| Baseline | none |
| Purpose | Test current performance |
| Test start | 12:33 UTC |
| Test duration | 30min |
| Executed test | oidc_session |
| k6 version | v1.0.0-rc1 |
| VUs | 600 |
| Client location | US1 |
| ZITADEL location | US1 |
| ZITADEL container specification | vCPU: 6<br/> Memory: 6 Gi <br/>Container min scale: 2<br/>Container max scale: 7 |
| ZITADEL Version | v4.0.0-rc2 |
| ZITADEL feature flags | webKey: true, improvedPerformance: \[\"IMPROVED\_PERFORMANCE\_ORG\_BY\_ID\", \"IMPROVED\_PERFORMANCE\_PROJECT\", \"IMPROVED\_PERFORMANCE\_USER\_GRANT\", \"IMPROVED\_PERFORMANCE\_ORG\_DOMAIN\_VERIFIED\", \"IMPROVED\_PERFORMANCE\_PROJECT\_GRANT\"\] |
| Database | type: psql<br />version: v17.4 |
| Database location | US1 |
| Database specification | vCPU: 8<br/> memory: 32Gib |
| ZITADEL metrics during test | |
| Observed errors | Database CPU usage was 60% |
| Top 3 most expensive database queries | 1: query events by instance id, aggregate types, event types, position<br/>2: writing events<br/>3: query memberships<br/> |
| k6 Iterations per second | 105 |
| k6 output | [output](#k6-output) |
| flowchart outcome | |
## Endpoint latencies
<BenchmarkChart testResults={OutputSource} />
## k6 output
```bash
█ TOTAL RESULTS
checks_total.......................: 1061560 420.116279/s
checks_succeeded...................: 99.99% 1061559 out of 1061560
checks_failed......................: 0.00% 1 out of 1061560
✓ user defined
✓ authorize status ok
✓ login name status ok
✓ login shows password page
✓ password status ok
✓ password callback
✓ code set
✓ token status ok
✓ access token created
✓ id token created
✓ info created
✓ org created
✓ create user is status ok
✓ generate machine key status ok
✓ member added successful
✓ openid configuration
✓ access token returned
✓ auth request id returned
✗ add Session status ok
↳ 99% — ✓ 265394 / ✗ 1
✓ finalize auth request status ok
✓ org removed
CUSTOM
login_ui_enter_login_name_duration......................................: min=100.93ms avg=100.93ms max=100.93ms p(50)=100.93ms p(95)=100.93ms p(99)=100.93ms
login_ui_enter_password_duration........................................: min=4.86ms avg=4.86ms max=4.86ms p(50)=4.86ms p(95)=4.86ms p(99)=4.86ms
login_ui_init_login_duration............................................: min=71.62ms avg=497.81ms max=19m45s p(50)=442.5ms p(95)=557.71ms p(99)=642.49ms
login_ui_token_duration.................................................: min=80.88ms avg=80.88ms max=80.88ms p(50)=80.88ms p(95)=80.88ms p(99)=80.88ms
membership_iam_member...................................................: min=65.45ms avg=65.45ms max=65.45ms p(50)=65.45ms p(95)=65.45ms p(99)=65.45ms
oidc_auth_request_finalize..............................................: min=89.04ms avg=1.32s max=2m8s p(50)=1.26s p(95)=1.62s p(99)=1.78s
oidc_session_duration...................................................: min=1.73s avg=2.88s max=21m54s p(50)=2.73s p(95)=3.26s p(99)=3.54s
oidc_token_duration.....................................................: min=34.16ms avg=34.16ms max=34.16ms p(50)=34.16ms p(95)=34.16ms p(99)=34.16ms
org_create_org_duration.................................................: min=41.58ms avg=41.58ms max=41.58ms p(50)=41.58ms p(95)=41.58ms p(99)=41.58ms
session_add_session_duration............................................: min=105.71ms avg=1.06s max=15m9s p(50)=1s p(95)=1.34s p(99)=1.5s
user_add_machine_key_duration...........................................: min=50.3ms avg=50.3ms max=50.3ms p(50)=50.3ms p(95)=50.3ms p(99)=50.3ms
user_create_machine_duration............................................: min=32.72ms avg=32.72ms max=32.72ms p(50)=32.72ms p(95)=32.72ms p(99)=32.72ms
HTTP
http_req_duration.......................................................: min=4.86ms avg=1.04s max=19m45s p(50)=966.65ms p(95)=1.5s p(99)=1.68s
{ expected_response:true }............................................: min=4.86ms avg=987.51ms max=19m45s p(50)=966.55ms p(95)=1.5s p(99)=1.68s
http_req_failed.........................................................: 0.01% 144 out of 796955
http_reqs...............................................................: 796955 315.397876/s
EXECUTION
iteration_duration......................................................: min=1.73s avg=2.88s max=21m55s p(50)=2.73s p(95)=3.26s p(99)=3.54s
iterations..............................................................: 265117 104.921029/s
vus.....................................................................: 0 min=0 max=600
vus_max.................................................................: 600 min=600 max=600
NETWORK
data_received...........................................................: 475 MB 188 kB/s
data_sent...............................................................: 200 MB 79 kB/s
running (42m06.8s), 000/600 VUs, 265104 complete and 600 interrupted iterations
default ✓ [======================================] 600 VUs 30m0s
```
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,98 @@
---
title: otp session benchmark of zitadel v4
sidebar_label: otp session
---
import OutputSource from "./output.json";
import { BenchmarkChart } from '@/components/benchmark_chart';
Benchmark results of v4 release of Zitadel.
## Performance test results
| Metric | Value |
|:--------------------------------------|:------|
| Baseline | none |
| Purpose | Test current performance |
| Test start | 14:56 UTC |
| Test duration | 30min |
| Executed test | otp_session |
| k6 version | v1.0.0-rc1 |
| VUs | 600 |
| Client location | US1 |
| ZITADEL location | US1 |
| ZITADEL container specification | vCPU: 6<br/> Memory: 6 Gi <br/>Container min scale: 2<br/>Container max scale: 7 |
| ZITADEL Version | v4.0.0-rc2 |
| ZITADEL feature flags | webKey: true, improvedPerformance: \[\"IMPROVED\_PERFORMANCE\_ORG\_BY\_ID\", \"IMPROVED\_PERFORMANCE\_PROJECT\", \"IMPROVED\_PERFORMANCE\_USER\_GRANT\", \"IMPROVED\_PERFORMANCE\_ORG\_DOMAIN\_VERIFIED\", \"IMPROVED\_PERFORMANCE\_PROJECT\_GRANT\"\] |
| Database | type: psql<br />version: v17.4 |
| Database location | US1 |
| Database specification | vCPU: 8<br/> memory: 32Gib |
| ZITADEL metrics during test | |
| Observed errors | database usage was 80% |
| Top 3 most expensive database queries | 1: query events by instance id, aggregate types, event types, position<br/>2: lock projection<br/>3: write events<br/> |
| k6 Iterations per second | 60 |
| k6 output | [output](#k6-output) |
| flowchart outcome | |
## Endpoint latencies
<BenchmarkChart testResults={OutputSource} />
## k6 output
```bash
█ TOTAL RESULTS
checks_total.......................: 338059 181.945817/s
checks_succeeded...................: 99.99% 338058 out of 338059
checks_failed......................: 0.00% 1 out of 338059
✓ user defined
✓ authorize status ok
✓ login name status ok
✓ login shows password page
✓ password status ok
✓ password callback
✓ code set
✓ token status ok
✓ access token created
✓ id token created
✓ info created
✓ org created
✓ create user is status ok
✓ set email otp status ok
✓ add Session status ok
✗ set Session status ok
↳ 99% — ✓ 224563 / ✗ 1
✓ org removed
CUSTOM
login_ui_enter_login_name_duration......................................: min=179.7ms avg=179.7ms max=179.7ms p(50)=179.7ms p(95)=179.7ms p(99)=179.7ms
login_ui_enter_password_duration........................................: min=14.73ms avg=14.73ms max=14.73ms p(50)=14.73ms p(95)=14.73ms p(99)=14.73ms
login_ui_init_login_duration............................................: min=76.13ms avg=76.13ms max=76.13ms p(50)=76.13ms p(95)=76.13ms p(99)=76.13ms
login_ui_token_duration.................................................: min=152.52ms avg=152.52ms max=152.52ms p(50)=152.52ms p(95)=152.52ms p(99)=152.52ms
org_create_org_duration.................................................: min=59.87ms avg=59.87ms max=59.87ms p(50)=59.87ms p(95)=59.87ms p(99)=59.87ms
otp_session_duration....................................................: min=2.2s avg=9.63s max=15.61s p(50)=9.59s p(95)=12.09s p(99)=13.33s
session_add_session_duration............................................: min=154ms avg=8.31s max=12.63s p(50)=8.42s p(95)=10.1s p(99)=10.52s
session_set_session_duration............................................: min=0s avg=659.94ms max=4.45s p(50)=513.63ms p(95)=1.62s p(99)=1.9s
set_human_email_otp_duration............................................: min=29.12ms avg=494.09ms max=806.37ms p(50)=581.1ms p(95)=646.91ms p(99)=680.35ms
user_create_human_duration..............................................: min=698.95ms avg=9.83s max=17.55s p(50)=11.1s p(95)=17s p(99)=17.46s
HTTP
http_req_duration.......................................................: min=0s avg=3.21s max=20.01s p(50)=1.21s p(95)=9.59s p(99)=10.3s
{ expected_response:true }............................................: min=14.73ms avg=3.21s max=20.01s p(50)=1.21s p(95)=9.59s p(99)=10.3s
http_req_failed.........................................................: 0.00% 1 out of 338656
http_reqs...............................................................: 338656 182.267126/s
EXECUTION
iteration_duration......................................................: min=2.2s avg=9.63s max=15.61s p(50)=9.59s p(95)=12.09s p(99)=13.33s
iterations..............................................................: 112282 60.43099/s
vus.....................................................................: 0 min=0 max=600
vus_max.................................................................: 600 min=600 max=600
NETWORK
data_received...........................................................: 183 MB 98 kB/s
data_sent...............................................................: 82 MB 44 kB/s
running (30m58.0s), 000/600 VUs, 112282 complete and 0 interrupted iterations
default ✓ [======================================] 600 VUs 30m0s
```
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,99 @@
---
title: session with password benchmark of zitadel v4
sidebar_label: password session
---
import OutputSource from "./output.json";
import { BenchmarkChart } from '@/components/benchmark_chart';
Benchmark results of v4 release of Zitadel.
## Performance test results
| Metric | Value |
|:--------------------------------------|:------|
| Baseline | none |
| Purpose | Test current performance |
| Test start | 15:14 UTC |
| Test duration | 30min |
| Executed test | password\_session |
| k6 version | v1.0.0-rc1 |
| VUs | 600 |
| Client location | US1 |
| ZITADEL location | US1 |
| ZITADEL container specification | vCPU: 6<br/> Memory: 6 Gi <br/>Container min scale: 2<br/>Container max scale: 7 |
| ZITADEL Version | v4.0.0-rc2 |
| ZITADEL feature flags | webKey: true, improvedPerformance: \[\"IMPROVED\_PERFORMANCE\_ORG\_BY\_ID\", \"IMPROVED\_PERFORMANCE\_PROJECT\", \"IMPROVED\_PERFORMANCE\_USER\_GRANT\", \"IMPROVED\_PERFORMANCE\_ORG\_DOMAIN\_VERIFIED\", \"IMPROVED\_PERFORMANCE\_PROJECT\_GRANT\"\] |
| Database | type: psql<br />version: v17.4 |
| Database location | US1 |
| Database specification | vCPU: 8<br/> memory: 32Gib |
| ZITADEL metrics during test | |
| Observed errors | Container startup latency is > 3 minutes |
| Top 3 most expensive database queries | 1: lock projection<br/>2: query events by instance id, aggregate types, event types, position<br/>3: query events by aggregate type, event type<br/> |
| k6 Iterations per second | 51 |
| k6 output | [output](#k6-output) |
| flowchart outcome | |
## Endpoint latencies
<BenchmarkChart testResults={OutputSource} />
## k6 output
```bash
█ TOTAL RESULTS
checks_total.......................: 188813 101.308159/s
checks_succeeded...................: 99.35% 187589 out of 188813
checks_failed......................: 0.64% 1224 out of 188813
✓ user defined
✓ authorize status ok
✓ login name status ok
✓ login shows password page
✓ password status ok
✓ password callback
✓ code set
✓ token status ok
✓ access token created
✓ id token created
✓ info created
✓ org created
✓ create user is status ok
✓ set email otp status ok
✗ add Session status ok
↳ 99% — ✓ 93457 / ✗ 686
✗ set Session status ok
↳ 99% — ✓ 92919 / ✗ 538
✓ org removed
CUSTOM
login_ui_enter_login_name_duration......................................: min=240.4ms avg=240.4ms max=240.4ms p(50)=240.4ms p(95)=240.4ms p(99)=240.4ms
login_ui_enter_password_duration........................................: min=14.32ms avg=14.32ms max=14.32ms p(50)=14.32ms p(95)=14.32ms p(99)=14.32ms
login_ui_init_login_duration............................................: min=84.45ms avg=84.45ms max=84.45ms p(50)=84.45ms p(95)=84.45ms p(99)=84.45ms
login_ui_token_duration.................................................: min=148.53ms avg=148.53ms max=148.53ms p(50)=148.53ms p(95)=148.53ms p(99)=148.53ms
org_create_org_duration.................................................: min=59.73ms avg=59.73ms max=59.73ms p(50)=59.73ms p(95)=59.73ms p(99)=59.73ms
password_session_duration...............................................: min=371ms avg=10.97s max=1m4s p(50)=9.52s p(95)=23.81s p(99)=27.57s
session_add_session_duration............................................: min=36.81ms avg=10.62s max=1m0s p(50)=9.13s p(95)=23.23s p(99)=29.47s
session_set_session_duration............................................: min=94.6ms avg=881.65ms max=1m0s p(50)=365.48ms p(95)=1.88s p(99)=4.75s
set_human_email_otp_duration............................................: min=39.12ms avg=570.13ms max=868.83ms p(50)=549.06ms p(95)=844.01ms p(99)=864.28ms
user_create_human_duration..............................................: min=502.43ms avg=10.82s max=16.72s p(50)=13.42s p(95)=16.28s p(99)=16.56s
HTTP
http_req_duration.......................................................: min=14.32ms avg=5.75s max=1m0s p(50)=583.63ms p(95)=22.01s p(99)=26.51s
{ expected_response:true }............................................: min=14.32ms avg=5.47s max=59.87s p(50)=566.75ms p(95)=21.8s p(99)=24.55s
http_req_failed.........................................................: 0.64% 1224 out of 189410
http_reqs...............................................................: 189410 101.628481/s
EXECUTION
iteration_duration......................................................: min=38.37ms avg=11.5s max=1m21s p(50)=9.68s p(95)=24.21s p(99)=54.53s
iterations..............................................................: 94143 50.512698/s
vus.....................................................................: 0 min=0 max=600
vus_max.................................................................: 600 min=600 max=600
NETWORK
data_received...........................................................: 102 MB 55 kB/s
data_sent...............................................................: 45 MB 24 kB/s
running (31m03.7s), 000/600 VUs, 94143 complete and 0 interrupted iterations
default ✓ [======================================] 600 VUs 30m0s
```
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,96 @@
---
title: user info benchmark of zitadel v4
sidebar_label: user info
---
import OutputSource from "./output.json";
import { BenchmarkChart } from '@/components/benchmark_chart';
Benchmark results of v4 release of Zitadel.
## Performance test results
| Metric | Value |
|:--------------------------------------|:------|
| Baseline | none |
| Purpose | Test current performance |
| Test start | 16:06 UTC |
| Test duration | 30min |
| Executed test | user\_info |
| k6 version | v1.0.0-rc1 |
| VUs | 600 |
| Client location | US1 |
| ZITADEL location | US1 |
| ZITADEL container specification | vCPU: 6<br/> Memory: 6 Gi <br/>Container min scale: 2<br/>Container max scale: 7 |
| ZITADEL Version | v4.0.0-rc2 |
| ZITADEL feature flags | webKey: true, improvedPerformance: \[\"IMPROVED\_PERFORMANCE\_ORG\_BY\_ID\", \"IMPROVED\_PERFORMANCE\_PROJECT\", \"IMPROVED\_PERFORMANCE\_USER\_GRANT\", \"IMPROVED\_PERFORMANCE\_ORG\_DOMAIN\_VERIFIED\", \"IMPROVED\_PERFORMANCE\_PROJECT\_GRANT\"\] |
| Database | type: psql<br />version: v17.4 |
| Database location | US1 |
| Database specification | vCPU: 8<br/> memory: 32Gib |
| ZITADEL metrics during test | Container startup latency is > 3 minutes |
| Observed errors | Container startup latency is > 3 minutes, elevated error rate during container startup |
| Top 3 most expensive database queries | 1: events by aggregate type and event type<br/>2: events by instance id, aggregate types, event types, position<br/>3: user by id<br/> |
| k6 Iterations per second | 2653 |
| k6 output | [output](#k6-output) |
| flowchart outcome | |
## Endpoint latencies
<BenchmarkChart testResults={OutputSource} />
## k6 output
```bash
█ TOTAL RESULTS
checks_total.......................: 4784344 2653.094897/s
checks_succeeded...................: 99.97% 4783333 out of 4784344
checks_failed......................: 0.02% 1011 out of 4784344
✓ user defined
✓ authorize status ok
✓ login name status ok
✓ login shows password page
✓ password status ok
✓ password callback
✓ code set
✓ token status ok
✓ access token created
✓ id token created
✓ info created
✓ org created
✓ create user is status ok
✗ openid configuration
↳ 89% — ✓ 600 / ✗ 74
✗ userinfo status ok
↳ 99% — ✓ 4782709 / ✗ 936
✗ org removed
↳ 0% — ✓ 0 / ✗ 1
CUSTOM
login_ui_enter_login_name_duration......................................: min=149.1ms avg=176.4ms max=203.69ms p(50)=176.4ms p(95)=200.96ms p(99)=203.15ms
login_ui_enter_password_duration........................................: min=34.7ms avg=248.44ms max=462.19ms p(50)=248.44ms p(95)=440.82ms p(99)=457.92ms
login_ui_init_login_duration............................................: min=83.7ms avg=85.38ms max=87.05ms p(50)=85.38ms p(95)=86.88ms p(99)=87.01ms
login_ui_token_duration.................................................: min=92.78ms avg=118.84ms max=144.89ms p(50)=118.84ms p(95)=142.29ms p(99)=144.37ms
oidc_user_info_duration.................................................: min=453.34µs avg=224.58ms max=1m0s p(50)=83.13ms p(95)=792.51ms p(99)=926.8ms
org_create_org_duration.................................................: min=53.42ms avg=53.42ms max=53.42ms p(50)=53.42ms p(95)=53.42ms p(99)=53.42ms
user_create_human_duration..............................................: min=373.72ms avg=373.72ms max=373.72ms p(50)=373.72ms p(95)=373.72ms p(99)=373.72ms
HTTP
http_req_duration.......................................................: min=453.34µs avg=225.49ms max=1m0s p(50)=83.13ms p(95)=792.51ms p(99)=927.03ms
{ expected_response:true }............................................: min=3.29ms avg=212.92ms max=29.69s p(50)=83.1ms p(95)=792.3ms p(99)=923.17ms
http_req_failed.........................................................: 0.02% 1011 out of 4784339
http_reqs...............................................................: 4784339 2653.092124/s
EXECUTION
iteration_duration......................................................: min=731.85µs avg=225.74ms max=1m0s p(50)=83.33ms p(95)=792.71ms p(99)=927.27ms
iterations..............................................................: 4783719 2652.748311/s
vus.....................................................................: 600 min=0 max=600
vus_max.................................................................: 600 min=600 max=600
NETWORK
data_received...........................................................: 3.4 GB 1.9 MB/s
data_sent...............................................................: 378 MB 210 kB/s
running (30m03.3s), 000/600 VUs, 4783719 complete and 0 interrupted iterations
default ✓ [======================================] 600 VUs 30m0s
```
File diff suppressed because it is too large Load Diff
+333
View File
@@ -0,0 +1,333 @@
---
title: ZITADEL API Reference Overview
sidebar_label: Overview
---
import { ApiCard } from "@/components/apicard";
import Column from "@/components/column";
ZITADEL exposes all features via different gRPC and REST APIs and provides SDKs for popular languages and frameworks.
The [OpenID Connect & OAuth endpoints](/apis/openidoauth/endpoints) and [SAML 2.0 endpoints](/apis/saml/endpoints) are implemented and exposed according to the specific standards. Managing resources such as users, organizations, instances, or settings must be done with the different [ZITADEL APIs](#zitadel-apis-resource-based).
[Actions](/guides/manage/console/actions-overview) allow to extend ZITADEL with custom code to change default behaviors or calling external systems.
## Authentication & authorization
### Authentication for human users (interactive)
ZITADEL implements industry standards such as OpenID Connect, OAuth 2.0, or SAML for authentication.
Please refer to our guides how to [authenticate users](/guides/integrate/login/login-users) through an interactive authentication process.
For user authentication on devices with limited accessibility (eg, SmartTV, Smart Watch etc.) use the [device authorization grant](/guides/integrate/login/oidc/device-authorization).
Additionally, you can use the [session API](/reference/api/session) to authenticate users, for example by building a [custom login UI](/guides/integrate/login-ui).
### Authenticate service users and machines
Service users allow for machine-to-machine (M2M) communication.
Follow the guides to learn how to [authenticate service users](/guides/integrate/service-users/authenticate-service-users).
Accessing the ZITADEL APIs through a service user might require additional steps, please follow the guide on how to [access ZITADEL APIs](../guides/integrate/zitadel-apis/access-zitadel-apis) to include the correct audience scope in your requests.
### OpenID Connect & OAuth
- [OpenID Connect endpoints](/apis/openidoauth/endpoints) definition
- Standard and reserved [scopes reference](/apis/openidoauth/scopes)
- Standard, custom, and reserved [claims reference](/apis/openidoauth/claims)
The [OIDC Playground](https://zitadel.com/playgrounds/oidc) is for testing OpenID authentication requests and their parameters.
### SAML 2.0
- [SAML 2.0 endpoints](/apis/saml/endpoints) definition
- [Custom attributes](https://github.com/zitadel/actions/blob/main/examples/set_custom_attribute.js) can be added with an action
### Custom
ZITADEL allows to authenticate users by creating a session with the [Session API](/reference/api/session), get OIDC authentication request details with the [OIDC service API](/reference/api/oidc) or get SAML request details with the [SAML service API](/reference/api/saml).
User authorizations can be [retrieved as roles from our APIs](/guides/integrate/retrieve-user-roles).
Refer to our guide to learn how to [build your own login UI](/guides/integrate/login-ui)
## ZITADEL APIs (resource-based)
ZITADEL provides APIs for each [core resource](/apis/v2):
- [User](/reference/api/user)
- [Session](/reference/api/session)
- [Settings](/reference/api/settings)
<Callout>
We are migrating to a resource-based API approach.
You might need to use the existing [service-based](#zitadel-apis-service-based) APIs for now to manage Organizations, Instances, Assets etc.
</Callout>
## ZITADEL APIs (service-based)
<Callout title="Prefer resource-based API">
ZITADEL APIs were organized by UseCase/Context, such as Auth API for authenticated users and Management API for organization managers.
This led to confusion about which API to use, particularly for requests that could be useful across multiple APIs but with different filters.
For instance, SearchUsers on an Instance Level or on an Organization Level.
To address this issue, ZITADEL is migrating to a [resource-based API](#zitadel-apis-resource-based).
</Callout>
<ApiCard title="Authentication" type="AUTH">
<Column>
<div>
### Authentication
The authentication API (aka Auth API) is used for all operations on the currently logged in user. The user id is taken from the sub claim in the token.
</div>
<div className="apicard-right">
#### Auth GRPC
Endpoint:
`${CUSTOM_DOMAIN}/zitadel.auth.v1.AuthService/`
Definition:
[Auth Proto](https://github.com/zitadel/zitadel/blob/main/proto/zitadel/auth.proto)
#### Auth REST
Endpoint:
`${CUSTOM_DOMAIN}/auth/v1/`
API Reference:
[OpenAPI Docs](/reference/api/auth)
</div>
</Column>
</ApiCard>
<ApiCard title="Management" type="MGMT">
<Column>
<div>
### Management
The management API is as the name states the interface where systems can mutate IAM objects like, organizations, projects, clients, users and so on if they have the necessary access rights.
To identify the current organization you can send a header `x-zitadel-orgid` or if no header is set, the organization of the authenticated user is set.
</div>
<div className="apicard-right">
#### Mgmt GRPC
Endpoint:
`${CUSTOM_DOMAIN}/zitadel.management.v1.ManagementService/`
Definition:
[Management Proto](https://github.com/zitadel/zitadel/blob/main/proto/zitadel/management.proto)
#### Mgmt REST
Endpoint:
`${CUSTOM_DOMAIN}/management/v1/`
API Reference:
[OpenAPI Docs](/reference/api/management)
</div>
</Column>
</ApiCard>
<ApiCard title="Administration" type="ADMIN">
<Column>
<div>
### Administration
This API is intended to configure and manage one ZITADEL instance itself.
</div>
<div className="apicard-right">
#### Admin GRPC
Endpoint:
`${CUSTOM_DOMAIN}/zitadel.admin.v1.AdminService/`
Definition:
[Admin Proto](https://github.com/zitadel/zitadel/blob/main/proto/zitadel/admin.proto)
#### Admin REST
Endpoint:
`${CUSTOM_DOMAIN}/admin/v1/`
API Reference:
[OpenAPI Docs](/reference/api/admin)
</div>
</Column>
</ApiCard>
<ApiCard title="Administration" type="SYSTEM">
<Column>
<div>
### System
This API is intended to manage the different ZITADEL instances within the system.
Checkout the guide how to [access the ZITADEL System API](/guides/integrate/zitadel-apis/access-zitadel-system-api).
</div>
<div className="apicard-right">
#### System GRPC
Endpoint:
`${CUSTOM_DOMAIN}/zitadel.system.v1.SystemService/`
Definition:
[System Proto](https://github.com/zitadel/zitadel/blob/main/proto/zitadel/system.proto)
#### System REST
Endpoint:
`${CUSTOM_DOMAIN}/system/v1/`
API Reference:
[OpenAPI Docs](/reference/api/system)
</div>
</Column>
</ApiCard>
<ApiCard title="Assets" type="ASSET">
<Column>
<div>
### Assets
The Assets API allows you to up- and download all kinds of assets. This can be files such as logos, fonts or user avatar.
</div>
<div>
#### Assets REST
Endpoint:
`${CUSTOM_DOMAIN}/assets/v1/`
Definition:
[Assets](./assets/assets)
</div>
</Column>
</ApiCard>
## API definitions
Each service's proto definition is located in the source control on GitHub.
As we generate the REST services and Swagger file out of the proto definition we recommend that you rely on the proto file.
We annotate the corresponding REST methods on each possible call as well as the AuthN and AuthZ requirements.
The last API (assets) is only a REST API because ZITADEL uses multipart form data for certain elements.
### SDKs
ZITADEL provides some [official and community supported SDKs](/sdk-examples/introduction) for multiple languages and frameworks.
Most languages allow you to build a client from proto definitions, which allows you to build your own client in case an SDK is missing.
### Proto
All of our APIs are generated by proto definitions. You can find all the proto definitions in the [Proto API Definitions](https://github.com/zitadel/zitadel/tree/main/proto/zitadel).
> More about [Protocol Buffer](https://developers.google.com/protocol-buffers)
### Example
See below for an example with the call **GetMyUser**.
```go
//User
rpc GetMyUser(google.protobuf.Empty) returns (UserView) {
option (google.api.http) = {
get: "/users/me"
};
option (zitadel.v1.auth_option) = {
permission: "authenticated"
};
}
```
As you can see the `GetMyUser` function is also available as a REST service under the path `/users/me`.
In the table below you can see the URI of those calls.
| Service | URI |
| :------ | :---------------------------------------------------- |
| REST | `${CUSTOM_DOMAIN}/auth/v1/users/me` |
| GRPC | `${CUSTOM_DOMAIN}/zitadel.auth.v1.AuthService/GetMyUser` |
## Domains
ZITADEL hosts everything under a single domain: `{instance}.zitadel.cloud` or your Custom Domain `${CUSTOM_DOMAIN}`
The domain is used as the OIDC issuer and as the base url for the gRPC and REST APIs, the Login and Management Console UI, which you'll find under `${CUSTOM_DOMAIN}/ui/console/`.
Are you self-hosting and having troubles with _Instance not found_ errors? [Check out this page](/self-hosting/manage/custom-domain).
## API path prefixes
If you run ZITADEL on a Custom Domain, you may want to reuse that domain for other applications.
For easy copying to your reverse proxy configuration, here is the list of URL path prefixes, ZITADEL uses.
```yaml
/zitadel.admin.v1.AdminService/
/admin/v1/
/zitadel.auth.v1.AuthService/
/auth/v1/
/zitadel.management.v1.ManagementService/
/management/v1/
/zitadel.system.v1.SystemService/
/system/v1/
/assets/v1/
/ui/
/oidc/v1/
/saml/v2/
/oauth/v2/
/device
/.well-known/openid-configuration
/openapi/
/idps/callback
/v2beta/
/zitadel.user.v2beta.UserService/
/zitadel.session.v2beta.SessionService/
/zitadel.settings.v2beta.SettingsService/
/zitadel.oidc.v2beta.OIDCService/
/zitadel.org.v2beta.OrganizationService/
/v2/
/zitadel.user.v2.UserService/
/zitadel.session.v2.SessionService/
/zitadel.settings.v2.SettingsService/
/zitadel.oidc.v2.OIDCService/
/zitadel.org.v2.OrganizationService/
```
## Postman Collection (Beta)
We published an official Postman collection to help you explore and test the ZITADEL APIs.
The collection is organized by services, and includes a script to automatically authenticate requests. Before using it, make sure to configure the required environment variables in Postman:
| Variable Name | Description |
|-------------------------|-------------|
| `protocol` | **http** for local testing, or **https** for cloud/self hosted instances |
| `custom_domain` | The domain of your Zitadel instance. |
| `service_client_id` | Client ID of service account with **IAM_OWNER** role in your Zitadel instance. |
| `service_client_secret` | Client secret for the service account. |
| `api_client_id` | Client ID of an API application. |
| `api_client_secret` | Client secret for the API application. |
| `project_id` | A project ID within your Zitadel instance. |
[Fork the Postman Collection](https://app.getpostman.com/run-collection/44392838-6417c238-b9b9-4334-bef0-233ae38cbc51?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D44392838-6417c238-b9b9-4334-bef0-233ae38cbc51%26entityType%3Dcollection%26workspaceId%3D54ef8cdd-5ea3-4f31-89f6-20720b45528c)
This release is currently in **beta**, and we welcome your feedback to improve it.
@@ -0,0 +1,173 @@
---
title: Migrate from v1 APIs to v2 APIs
---
This guide gives you an overview for migrating from our v1 API to the new and improved v2 API.
This upgrade introduces some significant architectural changes designed to make your development experience smoother, more intuitive, and efficient.
## The v1 Approach: Use-Case Based APIs
Our v1 API was structured around use cases, meaning we provided distinct APIs tailored to different user roles:
- Auth API: For authenticated users.
- Management API: For administrators of an organization.
- Admin API: For administrators of an instance.
- System API: For managing multiple instances.
While this approach served its initial purpose, it presented a few challenges.
Developers often found it difficult to determine which specific API endpoint to use for their needs.
Additionally, this model sometimes led to redundant implementations of similar functionalities across different APIs – for example, listing users might have existed in slightly different forms for an instance context versus an organization context.
This often required more extensive reading of documentation and further explanation to ensure correct usage.
## The v2 Approach: A Resource-Based API
With our v2 API, we introduce a resource-based architecture.
This means instead of organizing by user type, we now structure our API around logical resources, such as:
- Users API
- Instance API
- Organization API
- And more...
A key improvement in v2 is how context and permissions are handled.
The data you receive from an endpoint will now automatically be scoped based on the role and permissions of the authenticated user.
For example:
- An instance administrator calling a GET /users endpoint will receive a list of all users within that instance.
- An organization administrator calling the exact same GET /users endpoint will receive a list of users belonging only to their specific organization.
## Why the Change
The primary goals behind this architectural shift are to make our API:
- More Intuitive: Finding the right endpoint should feel natural. If you want to interact with users, you look at the Users API.
- Self-Explanatory: The structure itself guides you, reducing the need to sift through extensive documentation to understand which API "hat" you need to wear.
- Developer-Friendly: A cleaner, more consistent API surface means faster integration and less room for confusion.
We're confident that these changes will significantly enhance your experience working with our platform.
The following sections will detail the specific resources that have been migrated and outline any changes you'll need to be aware of.
## Resource Migration
This section details the migrated resources, including any breaking changes and other important considerations for your transition from v1 to v2.
### General Changes
#### Sunsetting OpenAPI/REST Support in Favor of Connect RPC
While our v1 API already offered gRPC, it also provided a parallel REST/OpenAPI interface for clients who preferred making traditional HTTP calls.
In our v2 API, we are consolidating our efforts to provide a more streamlined and efficient development experience.
The primary change is the removal of the OpenAPI/REST interface.
We will now exclusively support interaction with our gRPC services directly or through [Connect RPC](https://connectrpc.com/).
Connect RPC is being introduced as the new, official way to interact with our gRPC services using familiar, plain HTTP/1.1.
It effectively replaces the previous REST gateway.
For teams already using gRPC, your transition will be minimal.
For teams who were using the v1 REST API, migrating to v2 will involve adopting one of the following methods:
- Native gRPC: For the highest performance and to leverage features like bi-directional streaming.
- Connect RPC: For making CRUD-like (Create, Read, Update, Delete) calls over HTTP. This is the recommended path for most clients migrating from our v1 REST API.
A significant advantage of this new architecture remains the automatic generation of client libraries.
Based on our .proto service definitions, you can generate type-safe clients for your specific programming language, whether you use native gRPC or Connect RPC.
This eliminates the need to write boilerplate code for handling HTTP requests and parsing responses, leading to a more streamlined and less error-prone development process.
#### Contextual Information in the Request Body
A key change in v2 is that contextual data, like organization_id, must now be sent in the request body.
Previously, this was sent in the request headers.
**v1 (Header)**
```
x-zitadel-orgid: 1234567890
```
**v2 (Request Body)**
```
{
"organization_id": "1234567890"
}
```
### Instances
No major changes have been made to the organization requests.
### Organizations
No major changes have been made to the organization requests.
### Users
When migrating your user management from v1 to v2, the most significant updates involve user states and the initial onboarding process:
- **Unified User Creation Endpoint**:
- A significant simplification in v2 is the consolidation of user creation. There is now one primary endpoint for creating users, regardless of whether they are human or machine users.
- You can use this single endpoint to provision both human users (individuals interacting with your application) and machine users (e.g., service accounts, API clients), typically by specifying the user type in the request payload.
- **No More "Initial" State**:
- In v1, new users without a password or verified email were automatically assigned an initial state. This default assumption wasn't always ideal.
- In v2, this initial state has been removed. All newly created users are now active by default, regardless of their initial attributes.
- **New Onboarding Process**:
- To enable users to set up their accounts, you can now send them an invitation code. This allows them to securely add their authentication methods.
- **Flexible Email Verification**:
- v2 provides more control over email verification:
- You can choose at user creation whether an email verification code should be sent automatically.
- Alternatively, the API can return the verification code directly to you, empowering you to send a customized verification email.
[Users API v2 Documentation](/reference/api/user)
### Projects
We've simplified how you interact with projects by unifying projects and granted_projects into a single resource.
From a consumer's perspective, it no longer matters if you own a project or if it was granted to you by another organization; it's all just a project.
The main difference now is your level of permission.
Your permissions determine whether you have administrative rights (like updating the project's details) or if you can only view the project and manage authorizations for your users.
This change significantly streamlines API calls.
For example, where you previously had to make two separate requests to see all projects, you now make one.
**v1 (Separate Requests):**
```
- ListProjects
- ListGrantedProjects
```
**v2 (Single Request with Filter):**
```
- ListProjects (returns all projects you have access to)
```
You can now use filters within the single ListProjects request if you need to differentiate between project types, such as filtering by projects you own versus those that have been granted to you.
Update your code to use this new unified ListProjects endpoint.
### Applications
We have streamlined the creation and management of applications.
In v1, each application type had its own unique endpoints.
In v2, we have unified these into a single set of endpoints for all application types.
The biggest change is in how you create/update applications.
Instead of calling a specific endpoint for each type (e.g., CreateOidcApp, CreateSamlApp), you will now use a single CreateApp endpoint.
To specify the type of application, you will include its specific configuration object within the request body.
For example, to create a OIDC app, you will provide a oidc object in the request.
All properties that are common to every application, such as name, are now top-level fields in the request body, consistent across all types.
This approach simplifies client-side logic, as you no longer need to route requests to different endpoints.
**v1 (Multiple, Type-Specific Endpoints):**
```
- AddOIDCApp
- AddSAMLApp
- AddAPIApp
```
**v2 (Single Endpoint with Type-Specific Body):**
```
- CreateApplication
- ProjectID
- Name
- Type
- OIDC
- SAML
- API
```
@@ -0,0 +1,24 @@
---
title: ZITADEL Ready and Health Endpoints
sidebar_label: Ready and Health Endpoints
---
ZITADEL exposes a `Ready`- and `Healthy` endpoint to allow external systems like load balancers, orchestration systems, uptime probes and others to check the status.
## Ready
The `Ready` endpoint is located on the path `/debug/ready` and allows systems to probe if a ZITADEL process is ready to serve and accept traffic.
This endpoint is useful for operations like [zero downtime upgrade](../../concepts/architecture/solution#zero-downtime-updates) since it allows systems like Kubernetes to verify that ZITADEL is working on something (e.g. database schema migration) but is not yet ready to accept traffic.
<Callout>
In Kubernetes this is called the `readinessProbe`.
</Callout>
## Healthy
The `Health` endpoint is located on the path `/debug/healthz` and allows systems to probe if a ZITADEL process is still alive.
This helps system like kubernetes or a load balancer to observe if the process is still alive to accept traffic.
<Callout>
In Kubernetes this is called the `livenessProbe`.
</Callout>
@@ -0,0 +1,15 @@
---
title: ZITADEL Metrics
sidebar_label: Metrics
---
ZITADEL provides a `metrics` endpoint with the help of the [opentelemetry-go](https://github.com/open-telemetry/opentelemetry-go) package.
If you are self-hosting ZITADEL, you can access this endpoint with on the path `/debug/metrics`.
For example when running ZITADEL locally the endpoint is accessible on `http://localhost:8080/debug/metrics`.
The metrics endpoint can be scrubbed by any tool of choice that supports the `otel` format, e.g an existing Prometheus.
For our [Kubernetes/Helm](/self-hosting/deploy/kubernetes) users, we provide an out of the box support for the [ServiceMonitor](https://github.com/zitadel/zitadel-charts/blob/main/charts/zitadel/templates/servicemonitor.yaml) custom resource.
By default, metrics are enabled but can be turned off through ZITADEL's [configuration](/self-hosting/manage/configure).
The (default) configuration is located in the [defaults.yaml](https://github.com/zitadel/zitadel/blob/main/cmd/defaults.yaml).
@@ -0,0 +1,11 @@
| Parameter | Description |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| grant_type | Must be `urn:ietf:params:oauth:grant-type:token-exchange` |
| subject_token | A token that represents the identity of the party on behalf of whom the request is being made. |
| subject_token_type | An identifier that indicates the type of the token in the subject_token parameter. |
| actor_token | Optional. A token that represents the identity of the acting party. In ZITADEL this the impersonator. |
| actor_token_type | An identifier that indicates the type of the token in the actor_token parameter. Required when actor_token is provided |
| requested_token_type | Optional. An identifier that indicates the type of the token requested. Defaults to access token if not provided. |
| scope | [Scopes](/apis/openidoauth/scopes) you would like to request from ZITADEL for the requested token. Scopes are space delimited, e.g. `openid email profile`. |
| audience | Optional. Must be a subset of the combined audiences from both subject and actor tokens. |
| resource | Currently not supported |
@@ -0,0 +1,9 @@
| Property | Description |
| ----------------- | ------------------------------------------------------------------------------------ |
| access_token | An `access_token` as opaque token or JWT for the subject user |
| token_type | Type of the `access_token`. Value can be `Bearer` or `N_A` |
| issued_token_type | [Token type](#token-types) of the returned token, matches the `requested_token_type` |
| refresh_token | A refresh token if the `offline_access` scope was requested |
| id_token | An ID Token of the subject user, only with `openid` scope |
| expires_in | Number of second until the expiration of the `access_token` |
| scope | Scopes of the `access_token`. These might differ from the provided `scope` parameter |
@@ -0,0 +1,9 @@
The following table provides a matrix of supported token type parameter and responses for Token Exchange.
| Identifier | subject_token | actor_token | requested_token_type |
| ------------------------------------------------ | ------------------------------------------------------------ | ------------- | -------------------- |
| `urn:ietf:params:oauth:token-type:access_token` | JWT or Opaque | JWT or Opaque | Opaque only |
| `urn:ietf:params:oauth:token-type:refresh_token` | Not allowed | Not allowed | Not allowed |
| `urn:ietf:params:oauth:token-type:id_token` | Allowed | Allowed | Allowed |
| `urn:ietf:params:oauth:token-type:jwt` | JWT signed by client, only in combination with `actor_token` | Not allowed | Access Token as JWT |
| `urn:zitadel:params:oauth:token-type:user_id` | user ID as string, only in combination with `actor_token` | Not allowed | Not allowed |
@@ -0,0 +1,72 @@
---
title: Authentication Methods in ZITADEL
sidebar_label: Authentication Methods
---
## Client Secret Basic
When using `client_secret_basic` on token or introspection endpoints, provide an`Authorization` header with a Basic auth value in the following form:
```markdown
Authorization: "Basic " + base64( formUrlEncode(client_id) + ":" + formUrlEncode(client_secret) )
```
Given the client_id `78366401571920522@amce` and client_secret `veryweaksecret!`, this would result in the following `Authorization` header:
`Basic NzgzNjY0MDE1NzE5MjA1MjIlNDBhbWNlOnZlcnl3ZWFrc2VjcmV0JTIx`
## JWT with Private Key
When using `private_key_jwt` (`urn:ietf:params:oauth:client-assertion-type:jwt-bearer`) for token or introspection endpoints, provide a JWT as assertion generated with the following structure and signed with a downloaded key:
---
Key JSON
| Key | Example | Description |
|:---------|:--------------------------------------------------------------------|:-------------------------------------------------------------------------------|
| type | `"application"` | The type of account, right now only application is valid |
| keyId | `"81693565968962154"` | This is unique ID of the key |
| key | `"-----BEGIN RSA PRIVATE KEY-----...-----END RSA PRIVATE KEY-----"` | The private key generated by ZITADEL, this can not be regenerated! |
| clientId | `78366401571920522@acme` | The client_id of the application, this is the same as the subject from tokens |
| appId | `78366403256846242` | The id of the application (just for completeness, not used for JWT) |
```json
{
"type": "application",
"keyId": "81693565968962154",
"key": "-----BEGIN RSA PRIVATE KEY-----...-----END RSA PRIVATE KEY-----",
"clientId": "78366401571920522@acme",
"appId": "78366403256846242"
}
```
---
JWT
| Claim | Example | Description |
|:------|:---------------------------|:----------------------------------------------------------------------------------------------------------------|
| aud | `"https://${CUSTOM_DOMAIN}"` | String or Array of intended audiences MUST include ZITADEL's issuing domain |
| exp | `1605183582` | Unix timestamp of the expiry |
| iat | `1605179982` | Unix timestamp of the creation singing time of the JWT, MUST NOT be older than 1h |
| iss | `"78366401571920522@acme"` | String which represents the requesting party (owner of the key), normally the `clientID` from the json key file |
| sub | `"78366401571920522@acme"` | The subject ID of the application, normally the `clientID` from the json key file |
```json
{
"iss": "78366401571920522@acme",
"sub": "78366401571920522@acme",
"aud": "https://${CUSTOM_DOMAIN}",
"exp": 1605183582,
"iat": 1605179982
}
```
> To identify your key, it is necessary that you provide a JWT with a `kid` header claim representing your keyId from the Key JSON:
>
> ```json
> {
> "alg": "RS256",
> "kid": "81693565968962154"
> }
> ```
@@ -0,0 +1,130 @@
---
title: ZITADEL OIDC Authentication Request Playground
sidebar_label: OIDC Playground
---
import { ExternalLink } from "lucide-react";
The OIDC Playground is for testing OpenID Authentication Requests, giving you more insight how OpenID Connect works and how you can customize ZITADEL behavior with different parameters.
An OpenID Connect (OIDC) [authentication request](https://openid.net/specs/openid-connect-core-1_0.html) is an OAuth 2.0 Authorization Request using additional parameters and scopes to request that the end-user be authenticated by ZITADEL.
<a href="https://zitadel.com/playgrounds/oidc" target="_blank" className="flex items-center gap-2 text-primary hover:underline">
Go to the OIDC Playground <ExternalLink className="h-4 w-4" />
</a>
## Why this OIDC playground?
Currently ZITADEL requires human users to authenticate trough the hosted login page.
Your application should initiate a login by issuing an authentication request and redirecting the user to the login page. You can customize the behavior of ZITADEL by providing additional parameters and scopes in the request.
This playground should help you to initially craft an authentication request and to explore the behavior of ZITADEL in more depth.
## Request parameters explained
Not all request parameters are available in the playground. Please refer to the full documentation of the [authorization endpoint](/apis/openidoauth/endpoints#authorization-endpoint).
### Your Domain
The <span className="text-yellow-500">Custom Domain</span> to your ZITADEL instance. Use the base-path, the playground will add the required path to the request.
### Required Parameters
<p>
<span className="text-green-500">Client ID</span> is the resource id of an
application. It's the application where you want your users to login. You can
find the resource id in the Console. When using organization grants, use the
client id from the origin organization.
</p>
<p>
<span className="text-blue-500">Redirect URI</span> be one of the
pre-configured redirect uris for your application. You must add the redirect
uri for your application, else you will receive an error.
</p>
<p>
<span className="text-orange-500">Response Type</span> defines whether a code,
id_token token or just id_token will be returned. Most use cases will need
code.
</p>
More in the <a href="/apis/openidoauth/endpoints#required-request-parameters">documentation</a> about required Parameters.
### Authentication methods
Depending on the authentication and authorization flow of your application you might need to append some information to the authentication request.
<span className="text-teal-600">Authentication method</span> "(none) PKCE" is recommended
for most application types. The playground appends automatically a code challenge
for PKCE flows.
You need to append a "Code Challenge" by providing a random <span className="text-teal-600">Code Verifier</span> that is being hashed and encoded in the request to the token endpoint, please see our [guide](/guides/integrate/login/oidc/oauth-recommended-flows#our-recommended-authorization-flows) for more details.
More in the [documentation](/apis/openidoauth/authn-methods) about authentication methods.
### Additional Parameters
<div>
<span className="text-cyan-500">Prompt</span> defines if and how the user
should be prompted on login. For example:
<ul>
<li>
<code>select_account</code>: user is prompted to select one of the
existing sessions or create a new one
</li>
<li>
<code>create</code>: present the register form
</li>
<li>
<code>login</code>: requires the user to re-authenticate
</li>
<li>
<code>none</code>: user must be authenticated without interaction, an
error is returned otherwise; use for silent-refresh
</li>
</ul>
</div>
<p>
<span className="text-rose-500">Login hint</span> must be a valid logon name
of a user. You can skip the account picker by providing the Login hint.
</p>
There are many more additional parameters. Please refer to the [documentation](/apis/openidoauth/endpoints#additional-parameters) about additional parameters.
## Standard Scopes
Used to request additional information from ZITADEL.
These scopes are defined in the OpenID Connect specification.
The `openid` scope is mandatory.
Not all scopes are available in the playground. Please refer to the full [documentation](/apis/openidoauth/scopes) for the exhaustive list of available standard and reserved scopes.
## Reserved Scopes
You can request additional information that is specific to ZITADEL or customize the behavior of ZITADEL by including reserved scopes.
Please refer to the [documentation](/apis/openidoauth/scopes#reserved-scopes) for a full list of available reserved scopes.
### Organization policies and branding
Enforce an organization's policies and branding as well as membership of the user by passing the scope `urn:zitadel:iam:org:id:{id}` with the required <span className="text-purple-500">Organization ID</span>.
Please refer to the full [guide on branding](/guides/manage/customize/branding).
### Get user metadata
Pass the scope `urn:zitadel:iam:user:metadata` to request a user's metadata.
Please refer to the full [guide on user-metadata](/guides/manage/customize/user-metadata) for further details.
### Access core apis
Calling the [core API](/apis/introduction) with the authenticated user, requires that the projectID of ZITADEL is included in the audience claim.
This can be achieved by adding the scope `urn:zitadel:iam:org:project:id:zitadel:aud` to your applications authorization request.
## How to use ZITADEL in your project
Please refer to our [guide](/guides/integrate/login/oidc/login-users) on how to login users.
OpenID Connect certified libraries should allow you to customize the parameters and define scopes for the authorization request. You can also continue by using one of our [example applications](/sdk-examples/introduction).
@@ -0,0 +1,118 @@
---
title: Claims in ZITADEL
sidebar_label: Claims
---
ZITADEL asserts claims on different places according to the corresponding specifications or project and clients settings.
Please check below the matrix for an overview where which scope is asserted.
| Claims | Userinfo | Introspection | ID Token | Access Token |
|:--------------------------------------------------|:---------------|-----------------------------------------|-------------------------------------------------|------------------------------------------------------|
| acr | No | No | Yes | No |
| act | No | After Token Exchange with `actor_token` | After Token Exchange with `actor_token` | When JWT and after Token Exchange with `actor_token` |
| address | When requested | When requested | When requested and response_type `id_token` | No |
| amr | No | No | Yes | No |
| aud | No | Yes | Yes | When JWT |
| auth_time | No | No | Yes | No |
| azp (client_id when Introspect) | No | Yes | Yes | When JWT |
| email | When requested | When requested | When requested and response_type `id_token` | No |
| email_verified | When requested | When requested | When requested and response_type `id_token` | No |
| exp | No | Yes | Yes | When JWT |
| family_name | When requested | When requested | When requested and response_type `id_token` | No |
| gender | When requested | When requested | When requested and response_type `id_token` | No |
| given_name | When requested | When requested | When requested and response_type `id_token` | No |
| iat | No | Yes | Yes | When JWT |
| iss | No | Yes | Yes | When JWT |
| jti | No | Yes | No | When JWT |
| locale | When requested | When requested | When requested and response_type `id_token` | No |
| name | When requested | When requested | When requested and response_type `id_token` | No |
| nbf | No | Yes | No | When JWT |
| nonce | No | No | When provided in the authorization request [^1] | No |
| phone | When requested | When requested | When requested and response_type `id_token` | No |
| phone_verified | When requested | When requested | When requested and response_type `id_token` | No |
| preferred_username (username when Introspect) | When requested | When requested | Yes | No |
| sid | No | No | Yes | No |
| sub | Yes | Yes | Yes | When JWT |
| urn:zitadel:iam:org:domain:primary:\{domainname} | When requested | When requested | When requested | When JWT and requested |
| urn:zitadel:iam:org:project:roles | When requested | When requested | When requested or configured | When JWT and requested or configured |
| urn:zitadel:iam:user:metadata | When requested | When requested | When requested | When JWT and requested |
| urn:zitadel:iam:user:resourceowner:id | When requested | When requested | When requested | When JWT and requested |
| urn:zitadel:iam:user:resourceowner:name | When requested | When requested | When requested | When JWT and requested |
| urn:zitadel:iam:user:resourceowner:primary_domain | When requested | When requested | When requested | When JWT and requested |
[^1]: The nonce can also be used to distinguish between an id_token and a logout_token as latter must never include a nonce.
## Standard Claims
| Claims | Example | Description |
|:-------------------|:---------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| acr | TBA | TBA |
| act | `{"iss": "${CUSTOM_DOMAIN}","sub": "259241944654282754"}` | JSON object describing the actor from the `actor_token` after [token exchange](/guides/integrate/token-exchange#actor-token) |
| address | `Lerchenfeldstrasse 3, 9014 St. Gallen` | TBA |
| amr | `pwd mfa` | Authentication Method References as defined in [RFC8176](https://tools.ietf.org/html/rfc8176) <br/> `password` value is deprecated, please check `pwd` |
| aud | `69234237810729019` | The audience of the token, by default all client id's and the project id are included |
| auth_time | `1311280969` | Unix time of the authentication |
| azp | `69234237810729234` | Client id of the client who requested the token |
| email | `[email protected]` | Email Address of the subject |
| email_verified | `true` | Boolean if the email was verified by ZITADEL |
| events | `{ "http://schemas.openid.net/event/backchannel-logout": {} }` | Security Events such as Back-Channel Logout |
| exp | `1311281970` | Time the token expires (as unix time) |
| family_name | `Runner` | Last name of the subject |
| family_name | `Runner` | Last name of the subject |
| gender | `other` | Gender of the subject |
| given_name | `Road` | First name of the subject |
| given_name | `Road` | First name of the subject |
| iat | `1311280970` | Time of the token was issued at (as unix time) |
| iss | `${CUSTOM_DOMAIN}` | Issuing domain of a token |
| jti | `69234237813329048` | Unique id of the token |
| locale | `en` | Language from the subject |
| name | `Road Runner` | The subjects full name |
| nbf | `1311280970` | Time the token must not be used before (as unix time) |
| nonce | `blQtVEJHNTF0WHhFQmhqZ0RqeHJsdzdkd2d...` | The nonce provided by the client |
| phone | `+41 79 XXX XX XX` | Phone number provided by the user |
| phone_verified | `true` | Boolean if the phone was verified by ZITADEL |
| preferred_username | `[email protected]` | ZITADEL's login name of the user. Consist of `username@primarydomain` |
| sid | `291693710356251044` | String identifier for a session. This represents a session of a user agent for a logged-in end-User. Different sid values are used to identify distinct sessions at an OP. |
| sub | `77776025198584418` | Subject ID of the user |
## Custom Claims
Custom claims are being inserted into user tokens in addition to the standard claims.
Your app can use custom claims to handle more complex scenarios, such as restricting access based on these claims.
You can add custom claims using the [complement token flow](/apis/actions/complement-token) of the [actions feature](/guides/manage/console/actions-overview).
Multiple examples of Actions that result in custom claims can be found in our [Marketplace for ZITADEL Actions](https://github.com/zitadel/actions).
### Static values as custom claim
```javascript reference
https://github.com/zitadel/actions/blob/de69b56f6d0463817953b59a52ffd6afc6a366fb/examples/add_claim.js#L9-L11
```
### Metadata as custom claim
```javascript reference
https://github.com/zitadel/actions/blob/main/examples/add_metadata.js#L9-L15
```
### Format roles claims
```javascript reference
https://github.com/zitadel/actions/blob/main/examples/custom_roles.js#L20-L33
```
## Reserved Claims
ZITADEL reserves some claims to assert certain data. Please check out the [reserved scopes](./scopes#reserved-scopes).
| Claims | Example | Description |
|:--------------------------------------------------|:---------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| urn:zitadel:iam:action:\{actionname}:log | `{"urn:zitadel:iam:action:appendCustomClaims:log": ["test log", "another test log"]}` | This claim is set during Actions as a log, e.g. if two custom claims with the same keys are set. |
| urn:zitadel:iam:org:domain:primary:\{domainname} | `{"urn:zitadel:iam:org:domain:primary": "acme.ch"}` | This claim represents the primary domain of the organization the user belongs to. |
| urn:zitadel:iam:org:project:roles | `{"urn:zitadel:iam:org:project:roles": [ {"user": {"id1": "acme.zitade.ch", "id2": "caos.ch"} } ] }` | When roles are asserted, ZITADEL does this by providing the `id` and `primaryDomain` below the role. This gives you the option to check in which organization a user has the role on the current project (where your client belongs to). |
| urn:zitadel:iam:org:project:\{projectid}:roles | `{"urn:zitadel:iam:org:project:id3:roles": [ {"user": {"id1": "acme.zitade.ch", "id2": "caos.ch"} } ] }` | When roles are asserted, ZITADEL does this by providing the `id` and `primaryDomain` below the role. This gives you the option to check in which organization a user has the role on a specific project. |
| urn:zitadel:iam:user:metadata | `{"urn:zitadel:iam:user:metadata": [ {"key": "VmFsdWU=" } ] }` | The metadata claim will include all metadata of a user. The values are base64 encoded. |
| urn:zitadel:iam:user:resourceowner:id | `{"urn:zitadel:iam:user:resourceowner:id": "orgid"}` | This claim represents the user's organization ID. |
| urn:zitadel:iam:user:resourceowner:name | `{"urn:zitadel:iam:user:resourceowner:name": "ACME"}` | This claim represents the user's organization's name. |
| urn:zitadel:iam:user:resourceowner:primary_domain | `{"urn:zitadel:iam:user:resourceowner:primary_domain": "acme.ch"}` | This claim represents the user's organization's primary domain. |
@@ -0,0 +1,689 @@
---
title: OpenID Connect Endpoints in ZITADEL
sidebar_label: OpenID Connect Endpoints
---
import TokenExchangeRequest from "./_token_exchange_request.mdx";
import TokenExchangeResponse from "./_token_exchange_response.mdx";
import TokenExchangeTypes from "./_token_exchange_types.mdx";
## OpenID Connect 1.0 Discovery
The OpenID Connect Discovery Endpoint is located within the issuer domain.
This would give us `${CUSTOM_DOMAIN}/.well-known/openid-configuration`.
**Link to spec.** [OpenID Connect Discovery 1.0 incorporating errata set 1](https://openid.net/specs/openid-connect-discovery-1_0.html)
## authorization_endpoint
`${CUSTOM_DOMAIN}/oauth/v2/authorize`
<Callout>
The authorization_endpoint is located with the login page, due to the need of accessing the same cookie domain
</Callout>
The authorization_endpoint is the starting point for all initial user authentications. The user agent (browser) will be redirected to this endpoint to
authenticate the user in exchange for an authorization_code (authorization code flow) or tokens (implicit flow).
<details>
<summary>Links to specs</summary>
<ul>
<li>
<a href="https://datatracker.ietf.org/doc/html/rfc6749#section-3.1">
Section 3.1 of OAuth2.0 (RFC6749)
</a>
</li>
<li>
<a href="https://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint">
Section 3.1.2 of OpenID Connect Core 1.0 incorporating errata set 1
</a>
</li>
</ul>
</details>
### Required request parameters
| Parameter | Description |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| client_id | The id of your client as shown in Console. |
| redirect_uri | Callback uri of the authorization request where the code or tokens will be sent to. Must match exactly one of the preregistered in Console. |
| response_type | Determines whether a `code`, `id_token token` or just `id_token` will be returned. Most use cases will need `code`. See flow guide for more info. |
| scope | `openid` is required, see [Scopes](./scopes) for more possible values. Scopes are space delimited, e.g. `openid email profile` |
<Callout>
Following the [OIDC Core 1.0 specs](https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims) whenever an access_token is issued,
the id_token will not contain any claims of the scopes `profile`, `email`, `phone` and `address`.
Send the access_token to the [userinfo_endpoint](#userinfo-endpoint) or [introspection_endpoint](#introspection-endpoint) the retrieve these claims
or set the `id_token_userinfo_assertion` Option ("User Info inside ID Token" in Management Console) to true.
</Callout>
Depending on your authorization method you will have to provide additional parameters or headers:
<Tabs
groupId="token-auth-methods"
defaultValue="client_secret_basic"
values={[
{label: 'client_secret_basic', value: 'client_secret_basic'},
{label: 'client_secret_post', value: 'client_secret_post'},
{label: 'none (PKCE)', value: 'none'},
{label: 'private_key_jwt', value: 'private_key_jwt'},
]}
>
<Tab value="client_secret_basic">
no additional parameters required
</Tab>
<Tab value="client_secret_post">
no additional parameters required
</Tab>
<Tab value="none">
| Parameter | Description |
| --------------------- | ----------------------------------------------------- |
| code_challenge | The SHA-256 value of the generated `code_verifier` |
| code_challenge_method | Method used to generate the challenge, must be `S256` |
see PKCE guide for more information
</Tab>
<Tab value="private_key_jwt">
no additional parameters required
</Tab>
</Tabs>
### Additional parameters
| Parameter | Description |
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| id_token_hint | Valid `id_token` (of an existing session) used to identity the subject. **SHOULD** be provided when using prompt `none`. |
| login_hint | A valid logon name of a user. Will be used for username inputs or preselecting a user on `select_account`. Be sure to encode the hint correctly using url encoding (especially when using `+` or alike in the loginname) |
| max_age | Seconds since the last active successful authentication of the user |
| nonce | Random string value to associate the client session with the ID Token and for replay attacks mitigation. **MUST** be provided when using **implicit flow**. |
| prompt | If the Auth Server prompts the user for (re)authentication. <br />no prompt: the user will have to choose a session if more than one session exists<br />`none`: user must be authenticated without interaction, an error is returned otherwise <br />`login`: user must reauthenticate / provide a user name <br />`select_account`: user is prompted to select one of the existing sessions or create a new one <br />`create`: the registration form will be displayed to the user directly |
| state | Opaque value used to maintain state between the request and the callback. Used for Cross-Site Request Forgery (CSRF) mitigation as well, therefore highly **recommended**. |
| ui_locales | Spaces delimited list of preferred locales for the login UI, e.g. `de-CH de en`. If none is provided or matches the possible locales provided by the login UI, the `accept-language` header of the browser will be taken into account. |
| response_mode | The mechanism to be used for returning parameters to the application. See [response modes](#response-modes) for valid values. Invalid values are ignored. |
#### Response modes
ZITADEL supports the following `response_mode` values. When no response mode is requested, the response mode is chosen based on the configured Response Type of the application.
ZITADEL supports the following `response_mode` values. When no response mode is requested, the response mode is chosen based on the configured Response Type of the application.
As per [OpenID Connect Core 1.0, Section 3.1.2.1](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest):
> The use of this parameter is NOT RECOMMENDED when the Response Mode that would be requested is the default mode specified for the Response Type.
| Response Mode | Description |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| query | Encode the returned parameters in the URL query string. This is the default when the Response type is `code`, for example [Web applications](/guides/manage/console/applications-overview#web). |
| fragment | Encode the returned parameters in the URL fragment. This is the default when the Response Type is `id_token`, for example implicit [User Agent apps](/guides/manage/console/applications-overview#user-agent). This mode will not work for server-side applications, because fragments are never sent by the browser to the server. |
| form_post[^1] | ZITADEL serves a small JavaScript to the browser which will send the returned parameters to the `redirect_uri` using HTTP POST. This mode only works for server-side applications and user agents which support / allow JavaScript. |
[^1]: Implements [OAuth 2.0 Form Post Response Mode](https://openid.net/specs/oauth-v2-form-post-response-mode-1_0.html)
### Successful code response
When your `response_type` was `code` and no error occurred, the following response will be returned:
| Property | Description |
| -------- | ----------------------------------------------------------------------------- |
| code | Opaque string which will be necessary to request tokens on the token endpoint |
| state | Unmodified `state` parameter from the request |
### Successful implicit response
When your `response_type` was either `id_token` or `id_token token` and no error occurred, the following response will be returned:
| Property | Description |
| ------------ | ------------------------------------------------------------------------------------- |
| access_token | Only returned if `response_type` included `token` |
| expires_in | Number of second until the expiration of the `access_token` |
| id_token | An `id_token` of the authorized user |
| token_type | Type of the `access_token`. Value is always `Bearer` |
| scope | Scopes of the `access_token`. These might differ from the provided `scope` parameter. |
| state | Unmodified `state` parameter from the request |
### Error response
Regardless of the authorization flow chosen, if an error occurs the following response will be returned to the redirect_uri.
<Callout>
If the redirect_uri is not provided, was not registered or anything other prevents the auth server form returning the response to the client,
the error will be display directly to the user on the auth server
</Callout>
| Property | Description |
| ----------------- | -------------------------------------------------------------------- |
| error | An OAuth / OIDC [error_type](#authorize-errors) |
| error_description | Description of the error type or additional information of the error |
| state | Unmodified `state` parameter from the request |
#### Possible errors
| error_type | Possible reason |
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| invalid_request | The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed. |
| invalid_scope | The requested scope is invalid. Typically the required `openid` value is missing. |
| unauthorized_client | The client is not authorized to request an access_token using this method. Check in Management Console that the requested `response_type` is allowed in your application configuration. |
| unsupported_response_type | The authorization server does not support the requested response_type. |
| server_error | The authorization server encountered an unexpected condition that prevented it from fulfilling the request. |
| interaction_required | The authorization server requires end-user interaction of some form to proceed. This error MAY be returned when the prompt parameter value in the Authentication Request is none, but the Authentication Request cannot be completed without displaying a user interface for end-user interaction. |
| login_required | The authorization server requires end-user authentication. This error MAY be returned when the prompt parameter value in the Authentication Request is none, but the Authentication Request cannot be completed without displaying a user interface for end-user authentication. |
## token_endpoint
`${CUSTOM_DOMAIN}/oauth/v2/token`
The token_endpoint will as the name suggests return various tokens (access, id and refresh) depending on the used `grant_type`.
When using [`authorization_code`](#authorization-code-grant-code-exchange) flow call this endpoint after receiving the code from the authorization_endpoint.
When using [`refresh_token`](#authorization-code-grant-code-exchange) or [`urn:ietf:params:oauth:grant-type:jwt-bearer` (JWT Profile)](#jwt-profile-grant) you will call this endpoint directly.
### Authorization code grant (Code Exchange)
As mention above, when using `authorization_code` grant, this endpoint will be your second request for authorizing a user with its user agent (browser).
#### Required request parameters
| Parameter | Description |
| ------------ | ------------------------------------------------------------------------------------------------------------- |
| code | Code that was issued from the authorization request. |
| grant_type | Must be `authorization_code` |
| redirect_uri | Callback uri where the code was be sent to. Must match exactly the redirect_uri of the authorization request. |
Depending on your authorization method you will have to provide additional parameters or headers:
<Tabs
groupId="token-auth-methods"
defaultValue="client_secret_basic"
values={[
{label: 'client_secret_basic', value: 'client_secret_basic'},
{label: 'client_secret_post', value: 'client_secret_post'},
{label: 'none (PKCE)', value: 'none'},
{label: 'private_key_jwt', value: 'private_key_jwt'},
]}
>
<Tab value="client_secret_basic">
Send your `client_id` and `client_secret` as Basic Auth Header. Check [Client Secret Basic Auth Method](./authn-methods#client-secret-basic) on how to build it correctly.
</Tab>
<Tab value="client_secret_post">
Send your `client_id` and `client_secret` as parameters in the body:
| Parameter | Description |
| ------------- | -------------------------------- |
| client_id | client_id of the application |
| client_secret | client_secret of the application |
</Tab>
<Tab value="none">
Send your `client_id` and `code_verifier` for us to recompute the `code_challenge` of the authorization request.
| Parameter | Description |
| ------------- | ------------------------------------------------------------ |
| client_id | client_id of the application |
| code_verifier | code_verifier previously used to generate the code_challenge |
</Tab>
<Tab value="private_key_jwt">
Send a client assertion as JWT for us to validate the signature against the registered public key.
| Parameter | Description |
| --------------------- | ------------------------------------------------------------------------------------------------------------ |
| client_assertion | JWT built and signed according to [Using JWTs for Client Authentication](./authn-methods#jwt-with-private-key) |
| client_assertion_type | Must be `urn:ietf:params:oauth:client-assertion-type:jwt-bearer` |
</Tab>
</Tabs>
#### Successful code response
| Property | Description |
| ------------- | ------------------------------------------------------------------------------------- |
| access_token | An `access_token` as JWT or opaque token |
| expires_in | Number of second until the expiration of the `access_token` |
| id_token | An `id_token` of the authorized user |
| scope | Scopes of the `access_token`. These might differ from the provided `scope` parameter. |
| refresh_token | An opaque token. Only returned if `offline_access` scope was requested |
| token_type | Type of the `access_token`. Value is always `Bearer` |
### JWT profile grant
#### Required request parameters
| Parameter | Description |
| ---------- | ----------------------------------------------------------------------------------------------------------------------- |
| grant_type | Must be `urn:ietf:params:oauth:grant-type:jwt-bearer` |
| assertion | JWT built and signed according to [Using JWTs for Authorization Grants](./grant-types#using-jw-ts-as-authorization-grants) |
| scope | [Scopes](./scopes) you would like to request from ZITADEL. Scopes are space delimited, e.g. `openid email profile` |
```bash
curl --request POST \
--url ${CUSTOM_DOMAIN}/oauth/v2/token \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer \
--data assertion=eyJhbGciOiJSUzI1Ni...
```
#### Successful JWT profile response
| Property | Description |
| ------------ | ------------------------------------------------------------------------------------- |
| access_token | An `access_token` as JWT or opaque token |
| expires_in | Number of second until the expiration of the `access_token` |
| id_token | An `id_token` of the authorized service user |
| scope | Scopes of the `access_token`. These might differ from the provided `scope` parameter. |
| token_type | Type of the `access_token`. Value is always `Bearer` |
### Refresh token grant
To request a new `access_token` without user interaction, you can use the `refresh_token` grant.
See [offline_access Scope](./scopes#standard-scopes) for how to request a `refresh_token` in the authorization request.
#### Required request parameters
| Parameter | Description |
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| grant_type | Must be `refresh_token` |
| refresh_token | The refresh_token previously issued in the last authorization_code or refresh_token request. |
| scope | [Scopes](./scopes) you would like to request from ZITADEL for the new access_token. Must be a subset of the scope originally requested by the corresponding auth request. When omitted, the scopes requested by the original auth request will be reused. Scopes are space delimited, e.g. `openid email profile` |
Depending on your authorization method you will have to provide additional parameters or headers:
<Tabs
groupId="token-auth-methods"
defaultValue="client_secret_basic"
values={[
{label: 'client_secret_basic', value: 'client_secret_basic'},
{label: 'client_secret_post', value: 'client_secret_post'},
{label: 'none (PKCE)', value: 'none'},
{label: 'private_key_jwt', value: 'private_key_jwt'},
]}
>
<Tab value="client_secret_basic">
Send your `client_id` and `client_secret` as Basic Auth Header. Check [Client Secret Basic Auth Method](./authn-methods#client-secret-basic) on how to build it correctly.
</Tab>
<Tab value="client_secret_post">
Send your `client_id` and `client_secret` as parameters in the body:
| Parameter | Description |
| ------------- | -------------------------------- |
| client_id | client_id of the application |
| client_secret | client_secret of the application |
</Tab>
<Tab value="none">
Send your `client_id` as parameter in the body. No authentication is required.
</Tab>
<Tab value="private_key_jwt">
Send a `client_assertion` as JWT for us to validate the signature against the registered public key.
| Parameter | Description |
| --------------------- | ------------------------------------------------------------------------------------------------------------ |
| client_assertion | JWT built and signed according to [Using JWTs for Client Authentication](./authn-methods#jwt-with-private-key) |
| client_assertion_type | Must be `urn:ietf:params:oauth:client-assertion-type:jwt-bearer` |
</Tab>
</Tabs>
#### Successful refresh token response
| Property | Description |
| ------------- | ------------------------------------------------------------------------------------- |
| access_token | An `access_token` as JWT or opaque token |
| expires_in | Number of second until the expiration of the `access_token` |
| id_token | An `id_token` of the authorized user |
| scope | Scopes of the `access_token`. These might differ from the provided `scope` parameter. |
| refresh_token | An new opaque refresh_token. |
| token_type | Type of the `access_token`. Value is always `Bearer` |
### Client credentials grant
#### Required request parameters
| Parameter | Description |
| ---------- | ---------------------------------------------------------------------------------------------------------- |
| grant_type | Must be `client_credentials` |
| scope | [Scopes](./scopes) you would like to request from ZITADEL. Scopes are space delimited, e.g. `openid profile` |
Additionally, you need to authenticate your client by either sending `client_id` and `client_secret` as Basic Auth Header.
Check [Client Secret Basic Auth Method](./authn-methods#client-secret-basic) on how to build it correctly.
```bash
curl --request POST \
--url ${CUSTOM_DOMAIN}/oauth/v2/token \
--header 'Content-Type: application/x-www-form-urlencoded' \
--header 'Authorization: Basic ${BASIC_AUTH}' \
--data grant_type=client_credentials \
--data scope=openid profile
```
Or you can also send your `client_id` and `client_secret` as parameters in the body:
| Parameter | Description |
| ------------- | -------------------------------- |
| client_id | client_id of the application |
| client_secret | client_secret of the application |
```bash
curl --request POST \
--url ${CUSTOM_DOMAIN}/oauth/v2/token \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data grant_type=client_credentials \
--data client_id=${CLIENT_ID} \
--data client_secret=${CLIENT_SECRET} \
--data scope=openid profile
```
#### Successful client credentials response
| Property | Description |
| ------------ | ------------------------------------------------------------------------------------- |
| access_token | An `access_token` as JWT or opaque token |
| expires_in | Number of second until the expiration of the `access_token` |
| scope | Scopes of the `access_token`. These might differ from the provided `scope` parameter. |
| token_type | Type of the `access_token`. Value is always `Bearer` |
### Token Exchange grant
The Token Exchange grant implements [RFC 8693, OAuth 2.0 Token Exchange](https://www.rfc-editor.org/rfc/rfc8693) and can be used to exchange tokens to a different scope, audience or subject. Changing the subject of an authenticated token is called impersonation or delegation. ZITADEL also provides a [token exchange guide](/guides/integrate/token-exchange) with more details on using the Token Exchange Grant.
<Callout>
Token Exchange is currently an experimental [beta](https://help.zitadel.com/zitadel-software-release-cycle#beta) feature. Be sure to enable it on the [feature API](/guides/integrate/token-exchange#feature-api) before using it.
</Callout>
#### Request parameters
<TokenExchangeRequest components={props.components} />
Depending on your authorization method you will have to provide additional parameters or headers:
<Tabs
groupId="token-auth-methods"
defaultValue="client_secret_basic"
values={[
{label: 'client_secret_basic', value: 'client_secret_basic'},
{label: 'client_secret_post', value: 'client_secret_post'},
{label: 'none', value: 'none'},
{label: 'private_key_jwt', value: 'private_key_jwt'},
]}
>
<Tab value="client_secret_basic">
Send your `client_id` and `client_secret` as Basic Auth Header. Check [Client Secret Basic Auth Method](./authn-methods#client-secret-basic) on how to build it correctly.
</Tab>
<Tab value="client_secret_post">
Send your `client_id` and `client_secret` as parameters in the body:
| Parameter | Description |
| ------------- | -------------------------------- |
| client_id | client_id of the application |
| client_secret | client_secret of the application |
</Tab>
<Tab value="none">
Send your `client_id` as parameter in the body. No authentication is required.
</Tab>
<Tab value="private_key_jwt">
Send a `client_assertion` as JWT for us to validate the signature against the registered public key.
| Parameter | Description |
| --------------------- | ------------------------------------------------------------------------------------------------------------ |
| client_assertion | JWT built and signed according to [Using JWTs for Client Authentication](./authn-methods#jwt-with-private-key) |
| client_assertion_type | Must be `urn:ietf:params:oauth:client-assertion-type:jwt-bearer` |
</Tab>
</Tabs>
#### Successful token exchange response
<TokenExchangeResponse components={props.components} />
#### Token types
<TokenExchangeTypes components={props.components} />
### Error response
| error_type | Possible reason |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| invalid_request | The request is missing a required parameter, includes an unsupported parameter value (other than grant type), repeats a parameter, includes multiple credentials, utilizes more than one mechanism for authenticating the client, or is otherwise malformed. |
| invalid_scope | The requested scope is invalid, unknown, malformed, or exceeds the scope granted by the resource owner. |
| unauthorized_client | The authenticated client is not authorized to use this authorization grant type. |
| unsupported_grant_type | The authorization grant type is not supported by the authorization server. |
| server_error | The authorization server encountered an unexpected condition that prevented it from fulfilling the request. |
| invalid_grant | The provided authorization grant (e.g., authorization code, resource owner credentials) or refresh token is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client. |
| invalid_client | Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method). |
## introspection_endpoint
`${CUSTOM_DOMAIN}/oauth/v2/introspect`
This endpoint enables clients to validate an `acccess_token`, either opaque or JWT. Unlike client side JWT validation,
this endpoint will check if the token is not revoked (by client or logout).
| Parameter | Description |
| --------- | --------------- |
| token | An access token |
Depending on your authorization method you will have to provide additional parameters or headers:
<Tabs
groupId="introspect-auth-methods"
defaultValue="client_secret_basic"
values={[
{label: 'client_secret_basic', value: 'client_secret_basic'},
{label: 'private_key_jwt', value: 'private_key_jwt'},
]}
>
<Tab value="client_secret_basic">
Send your `client_id` and `client_secret` as Basic Auth Header. Check [Client Secret Basic Auth Method](./authn-methods#client-secret-basic) on how to build it correctly.
```bash
curl --request POST \
--url ${CUSTOM_DOMAIN}/oauth/v2/introspect \
--header 'Content-Type: application/x-www-form-urlencoded' \
--header 'Authorization: Basic {your_basic_auth_header}' \
--data token=VjVxyCZmRmWYqd3_F5db9Pb9mHR5fqzhn...
```
</Tab>
<Tab value="private_key_jwt">
Send a `client_assertion` as JWT for us to validate the signature against the registered public key.
| Parameter | Description |
| --------------------- | ----------------------------------------------------------------------------------------------------------- |
| client_assertion | JWT built and signed according to [Using JWTs for Client Authentication](./authn-methods#client-secret-basic) |
| client_assertion_type | must be `urn:ietf:params:oauth:client-assertion-type:jwt-bearer` |
```bash
curl --request POST \
--url ${CUSTOM_DOMAIN}/oauth/v2/introspect \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer \
--data client_assertion=eyJhbGciOiJSUzI1Ni... \
--data token=VjVxyCZmRmWYqd3_F5db9Pb9mHR5fqzhn...
```
</Tab>
</Tabs>
### Successful introspection response
Upon successful authorization of the client a response with the boolean `active` is returned, indicating if the provided token
is active and the requesting client is part of the token audience.
If `active` is **true**, further information will be provided:
| Property | Description |
| ---------- | --------------------------------------------------------------------- |
| aud | The audience of the token |
| client_id | The client_id of the application the token was issued to |
| exp | Time the token expires (as unix time) |
| iat | Time of the token was issued at (as unix time) |
| iss | Issuer of the token |
| jti | Unique id of the token |
| nbf | Time the token must not be used before (as unix time) |
| scope | Space delimited list of scopes granted to the token |
| token_type | Type of the inspected token. Value is always `Bearer` |
| username | ZITADEL's login name of the user. Consist of `username@primarydomain` |
Additionally and depending on the granted scopes, information about the authorized user is provided.
Check the [Claims](./claims) page if a specific claims might be returned and for detailed description.
### Error response
If the authorization fails, an HTTP 401 with `invalid_client` will be returned.
## userinfo_endpoint
`${CUSTOM_DOMAIN}/oidc/v1/userinfo`
This endpoint will return information about the authorized user.
Send the `access_token` of the **user** (not the client) as Bearer Token in the `authorization` header:
```bash
curl --request GET \
--url ${CUSTOM_DOMAIN}/oidc/v1/userinfo
--header 'Authorization: Bearer dsfdsjk29fm2as...'
```
### Successful userinfo response
If the `access_token` is valid, the information about the user depending on the granted scopes is returned.
Check the [Claims](./claims) page if a specific claims might be returned and for detailed description.
### Error response
If the token is invalid or expired, an HTTP 401 will be returned.
## revocation_endpoint
`${CUSTOM_DOMAIN}/oauth/v2/revoke`
This endpoint enables clients to revoke an `access_token` or `refresh_token` they have been granted.
<Callout>
If you revoke an `access_token` only the specific token will be revoked. When revoking a `refresh_token`,
the corresponding `access_token` will be revoked as well.
</Callout>
| Parameter | Description |
| --------- | -------------------------------- |
| token | An access token or refresh token |
Depending on your authorization method you will have to provide additional parameters or headers:
<Tabs
groupId="token-auth-methods"
defaultValue="client_secret_basic"
values={[
{label: 'client_secret_basic', value: 'client_secret_basic'},
{label: 'client_secret_post', value: 'client_secret_post'},
{label: 'none (PKCE)', value: 'none'},
{label: 'private_key_jwt', value: 'private_key_jwt'},
]}
>
<Tab value="client_secret_basic">
Send your `client_id` and `client_secret` as Basic Auth Header. Check [Client Secret Basic Auth Method](./authn-methods#client-secret-basic) on how to construct a request correctly.
</Tab>
<Tab value="client_secret_post">
Send your `client_id` and `client_secret` as parameters in the body:
| Parameter | Description |
| ------------- | -------------------------------- |
| client_id | client_id of the application |
| client_secret | client_secret of the application |
</Tab>
<Tab value="none">
Send your `client_id` as parameters in the body:
| Parameter | Description |
| --------- | ---------------------------- |
| client_id | client_id of the application |
</Tab>
<Tab value="private_key_jwt">
Send a `client_assertion` as JWT for ZITADEL to verify the signature against the registered public key.
| Parameter | Description |
| --------------------- | ------------------------------------------------------------------------------------------------------------- |
| client_assertion | JWT created and signed according to [Using JWTs for Client Authentication](./authn-methods#client-secret-basic) |
| client_assertion_type | must be `urn:ietf:params:oauth:client-assertion-type:jwt-bearer` |
```bash
curl --request POST \
--url ${CUSTOM_DOMAIN}/oauth/v2/revoke \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer \
--data client_assertion=eyJhbGciOiJSUzI1Ni... \
--data token=VjVxyCZmRmWYqd3_F5db9Pb9mHR5fqzhn...
```
</Tab>
</Tabs>
## end_session_endpoint
`${CUSTOM_DOMAIN}/oidc/v1/end_session`
The endpoint has to be opened in the user agent (browser) to terminate the user sessions.
No parameters are needed apart from the user agent cookie, but you can provide the following to customize the behavior:
| Parameter | Description |
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| id_token_hint | the id_token that was previously issued to the client |
| client_id | client_id of the application |
| post_logout_redirect_uri | Callback uri of the logout where the user (agent) will be redirected to. Must match exactly one of the preregistered in Console. |
| state | Opaque value used to maintain state between the request and the callback |
| logout_hint | A valid login name of a user. Will be used to select the user to logout. Only supported when using the login UI V2. |
| ui_locales | Spaces delimited list of preferred locales for the login UI, e.g. `de-CH de en`. If none is provided or matches the possible locales provided by the login UI, the `accept-language` header of the browser will be taken into account. |
The `post_logout_redirect_uri` will be checked against the previously registered uris of the client provided by the `azp` claim of the `id_token_hint` or the `client_id` parameter.
If both parameters are provided, they must be equal.
If neither an `id_token_hint` nor a `client_id` parameter is provided, the `post_logout_redirect_uri` will be ignored.
## jwks_uri
`${CUSTOM_DOMAIN}/oauth/v2/keys`
The endpoint returns a JSON Web Key Set (JWKS) containing the public keys that can be used to locally validate JWTs you received from ZITADEL.
The alternative would be to validate tokens with the [introspection endpoint](#introspection-endpoint).
### Key rotation
Keys are automatically rotated on a regular basis or on demand, meaning keys can change in irregular intervals.
ZITADEL ensures that a proper `kid` is set with each key.
<Callout title="Keys rotate without prior notice">
Be aware that these keys can be rotated without any prior notice.
</Callout>
### Caching
You can optimize performance of your clients by caching the response from the keys endpoint.
We recommend to regularly update the cached response, since the [keys can be rotated without prior notice](#key-rotation).
You could also combine caching with a risk-based on-demand refresh when a critical operation is executed.
Without caching you will call this endpoint on each request.
This might result in being rate limited for a large number of requests that come from the same backend.
## OAuth 2.0 metadata
**ZITADEL** does not yet provide a OAuth 2.0 Metadata endpoint but instead provides a [OpenID Connect Discovery Endpoint](https://openid.net/specs/openid-connect-discovery-1_0.html).
@@ -0,0 +1,134 @@
---
title: Grant Types in ZITADEL
sidebar_label: Grant Types
---
For a list of supported or unsupported `Grant Types` please have a look at the table below.
| Grant Type | Supported |
| :---------------------------------------------------- | :-------- |
| Authorization Code | yes |
| Authorization Code with PKCE | yes |
| Client Credentials | yes |
| Device Authorization | yes |
| Implicit | yes |
| JSON Web Token (JWT) Profile | yes |
| Refresh Token | yes |
| Resource Owner Password Credentials | no |
| Security Assertion Markup Language (SAML) 2.0 Profile | no |
| Token Exchange | yes |
## Authorization Code
**Link to spec.** [The OAuth 2.0 Authorization Framework Section 1.3.1](https://tools.ietf.org/html/rfc6749#section-1.3.1)
## Proof Key for Code Exchange
**Link to spec.** [Proof Key for Code Exchange by OAuth Public Clients](https://tools.ietf.org/html/rfc7636)
## Implicit
**Link to spec.** [The OAuth 2.0 Authorization Framework Section 1.3.2](https://tools.ietf.org/html/rfc6749#section-1.3.2)
## Client Credentials
**Link to spec.** [The OAuth 2.0 Authorization Framework Section 1.3.4](https://tools.ietf.org/html/rfc6749#section-1.3.4)
## Refresh Token
**Link to spec.** [The OAuth 2.0 Authorization Framework Section 1.5](https://tools.ietf.org/html/rfc6749#section-1.5)
## JSON Web Token (JWT) Profile
**Link to spec.** [JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants](https://tools.ietf.org/html/rfc7523)
### Using JWTs as Authorization Grants
Our service user work with the JWT profile to authenticate them against ZITADEL.
1. Create or use an existing service user
2. Create a new key and download it
3. Generate a JWT with the structure below and sign it with the downloaded key
4. Send the JWT Base64 encoded to ZITADEL's token endpoint
5. Use the received access token
---
Key JSON
| Key | Example | Description |
| :----- | :------------------------------------------------------------------ | :----------------------------------------------------------------- |
| type | `"serviceaccount"` | The type of account, right now only serviceaccount is valid |
| keyId | `"81693565968772648"` | This is unique ID of the key |
| key | `"-----BEGIN RSA PRIVATE KEY-----...-----END RSA PRIVATE KEY-----"` | The private key generated by ZITADEL, this can not be regenerated! |
| userId | `78366401571647008` | The service users ID, this is the same as the subject from tokens |
```json
{
"type": "serviceaccount",
"keyId": "81693565968772648",
"key": "-----BEGIN RSA PRIVATE KEY-----...-----END RSA PRIVATE KEY-----",
"userId": "78366401571647008"
}
```
---
JWT
| Claim | Example | Description |
| :---- | :------------------------- | :------------------------------------------------------------------------------------------------------------ |
| aud | `"https://${CUSTOM_DOMAIN}"` | String or Array of intended audiences MUST include ZITADEL's issuing domain |
| exp | `1605183582` | Unix timestamp of the expiry |
| iat | `1605179982` | Unix timestamp of the creation singing time of the JWT, MUST NOT be older than 1h |
| iss | `"77479219772321307"` | String which represents the requesting party (owner of the key), normally the `userId` from the json key file |
| sub | `"77479219772321307"` | The subject ID of the service user, normally the `userId` from the json key file |
```json
{
"iss": "77479219772321307",
"sub": "77479219772321307",
"aud": "https://${CUSTOM_DOMAIN}",
"exp": 1605183582,
"iat": 1605179982
}
```
> To identify your key, it is necessary that you provide a JWT with a `kid` header claim representing your keyId from the Key JSON:
>
> ```json
> {
> "alg": "RS256",
> "kid": "81693565968772648"
> }
> ```
---
See [JWT Profile Grant on Token Endpoint](./endpoints#token-endpoint) for usage.
### Using JWTs for Client Authentication
See how to build a [JWT for client authentication](./authn-methods#jwt-with-private-key) from the downloaded key.
Find out how to use it on the [token endpoint](./endpoints#token-endpoint) or the [introspection endpoint](./endpoints#introspection-endpoint).
## Token Exchange
**Link to spec.** [OAuth 2.0 Token Exchange](https://tools.ietf.org/html/rfc8693)
## Device Authorization
**Link to spec.** [OAuth 2.0 Device Authorization Grant](https://tools.ietf.org/html/rfc8628)
## Security Assertion Markup Language (SAML) 2.0 Profile
**Link to spec.** [Security Assertion Markup Language (SAML) 2.0 Profile for OAuth 2.0 Client Authentication and Authorization Grants](https://tools.ietf.org/html/rfc7522)
## Not Supported Grant Types
### Resource Owner Password Credentials
> Due to growing security concerns we do not support this grant type. With OAuth 2.1 it looks like this grant will be removed.
**Link to spec.** [The OAuth 2.0 Authorization Framework Section 1.3.3](https://tools.ietf.org/html/rfc6749#section-1.3.3)
@@ -0,0 +1,34 @@
---
title: Scopes in ZITADEL
sidebar_label: Scopes
---
ZITADEL supports the usage of scopes as way of requesting information from the IAM and also instruct ZITADEL to do certain operations.
## Standard Scopes
| Scopes | Description |
| :------------- | ------------------------------------------------------------------------------ |
| openid | When using openid connect this is a mandatory scope |
| profile | Optional scope to request the profile of the subject |
| email | Optional scope to request the email of the subject |
| address | Optional scope to request the address of the subject |
| phone | Optional scope to request the phone of the subject |
| offline_access | Optional scope to request a refresh_token (only possible when using code flow) |
## Reserved Scopes
In addition to the standard compliant scopes we utilize the following scopes.
| Scopes | Example | Description |
| :------------------------------------------------ | :----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `urn:zitadel:iam:org:project:role:{rolekey}` | `urn:zitadel:iam:org:project:role:user` | By using this scope a client can request the claim `urn:zitadel:iam:org:project:roles` to be asserted when possible. As an alternative approach you can enable all roles to be asserted from the [project](/guides/manage/console/roles#authorizations) a client belongs to. |
| `urn:zitadel:iam:org:projects:roles` | `urn:zitadel:iam:org:projects:roles` | By using this scope a client can request the claim `urn:zitadel:iam:org:project:{projectid}:roles` to be asserted for each requested project. All projects of the token audience, requested by the `urn:zitadel:iam:org:project:id:{projectid}:aud` scopes will be used. |
| `urn:zitadel:iam:org:id:{id}` | `urn:zitadel:iam:org:id:178204173316174381` | When requesting this scope **ZITADEL** will enforce that the user is a member of the selected organization. If the organization does not exist a failure is displayed. It will assert the `urn:zitadel:iam:user:resourceowner` claims. |
| `urn:zitadel:iam:org:domain:primary:{domainname}` | `urn:zitadel:iam:org:domain:primary:acme.ch` | When requesting this scope **ZITADEL** will enforce that the user is a member of the selected organization and the username is suffixed by the provided domain. If the organization does not exist a failure is displayed |
| `urn:zitadel:iam:org:roles:id:{orgID}` | `urn:zitadel:iam:org:roles:id:178204173316174381` | This scope can be used one or more times to limit the granted organization IDs in the returned roles. Unknown organization IDs are ignored. When this scope is not used, all granted organizations are returned inside the roles. |
| `urn:zitadel:iam:org:project:id:{projectid}:aud` | `urn:zitadel:iam:org:project:id:69234237810729019:aud` | By adding this scope, the requested project id will be added to the audience of the access token |
| `urn:zitadel:iam:org:project:id:zitadel:aud` | `urn:zitadel:iam:org:project:id:zitadel:aud` | By adding this scope, the ZITADEL project id will be added to the audience of the access token |
| `urn:zitadel:iam:user:metadata` | `urn:zitadel:iam:user:metadata` | By adding this scope, the metadata of the user will be included in the token. The values are base64 encoded. |
| `urn:zitadel:iam:user:resourceowner` | `urn:zitadel:iam:user:resourceowner` | By adding this scope: id, name and primary_domain of the user's organization will be included in the token. |
| `urn:zitadel:iam:org:idp:id:{idp_id}` | `urn:zitadel:iam:org:idp:id:76625965177954913` | By adding this scope the user will directly be redirected to the identity provider to authenticate. Make sure you also send the primary domain scope if a custom login policy is configured. Otherwise the system will not be able to identify the identity provider. |
+2
View File
@@ -0,0 +1,2 @@
*
!.gitignore
+69
View File
@@ -0,0 +1,69 @@
---
title: SAML Endpoints in ZITADEL
---
## SAML 2.0 metadata
The SAML Metadata is located within the issuer domain. This would give us `${CUSTOM_DOMAIN}/saml/v2/metadata`.
This metadata contains all the information defined in the spec.
**Link to
spec.** [Metadata for the OASIS Security Assertion Markup Language (SAML) V2.0 – Errata Composite](https://www.oasis-open.org/committees/download.php/35391/sstc-saml-metadata-errata-2.0-wd-04-diff.pdf)
## Certificate endpoint
`${CUSTOM_DOMAIN}/saml/v2/certificate`
The certificate endpoint provides the certificate which is used to sign the responses for download, for easier use with
different service providers which want the certificate separately instead of inside the metadata.
## SSO endpoint
`${CUSTOM_DOMAIN}/saml/v2/SSO`
The SSO endpoint is the starting point for all initial user authentications. The user agent (browser) will be redirected
to this endpoint to authenticate the user.
Supported on this endpoint or currently `urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect`
or `urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST` bindings.
**Link to
spec.** [Bindings for the OASIS Security Assertion Markup Language (SAML) V2.0 – Errata Composite](https://www.oasis-open.org/committees/download.php/35387/sstc-saml-bindings-errata-2.0-wd-05-diff.pdf)
### Required request parameters
| Parameter | Description |
|-------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| RelayState | (Optional) ID to associate the exchange with the original request. |
| SAMLRequest | The request made to the SAML IDP. (base64 encoded) |
| SigAlg | Algorithm used to sign the request, only if binding is 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect' as signature has to be provided es separate parameter. (base64 encoded) |
| Signature | Signature of the request as parameter with 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect' binding. (base64 encoded) |
### Successful response
Depending on the content of the request the response comes back in the requested binding, but the content is the same.
| Parameter | Description |
|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| RelayState | ID to associate the exchange with the original request. |
| SAMLResponse | The response form the SAML IDP. (base64 encoded) |
| SigAlg | Algorithm used to sign the response, only if binding is 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect' as signature has to be provided es separate parameter. (base64 encoded) |
| Signature | Signature of the response as parameter with 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect' binding. (base64 encoded) |
### Error response
Regardless of the error, the used http error code will be '200', which represents a successful request. Whereas the
response will contain a StatusCode include a message which provides more information if an error occurred.
**Link to
spec** [Assertions and Protocols for the OASIS Security Assertion Markup Language (SAML) V2.0 – Errata Composite](https://www.oasis-open.org/committees/download.php/35711/sstc-saml-core-errata-2.0-wd-06-diff.pdf)
## Custom attributes
Custom attributes are being inserted into SAML response if not already present.
Your app can use custom claims to handle more complex scenarios, such as restricting access based on these claims.
You can add custom attributes using the [complement SAMLresponse](/apis/actions/customize-samlresponse) of the [actions feature](/guides/manage/console/actions-overview).
Examples of Actions that result in custom attributes can be found in our [Marketplace for ZITADEL Actions](https://github.com/zitadel/actions).
+858
View File
@@ -0,0 +1,858 @@
---
title: SCIM v2.0 (Preview)
---
The Zitadel [SCIM v2](https://scim.cloud/) service provider interface enables seamless integration of identity and
access management (IAM) systems with Zitadel,
following the System for Cross-domain Identity Management (SCIM) v2.0 specification.
This interface allows standardized management of IAM resources, making it easier to automate user provisioning and
deprovisioning.
## Supported endpoints
The Zitadel SCIM v2.0 service provider implementation supports the following endpoints.
The base URL for the SCIM endpoint in Zitadel is: `https://${CUSTOM_DOMAIN}/scim/v2/{orgId}`.
| Endpoint | Remarks |
|-------------------------------------------------------------------------|------------------------------------------------------------|
| `GET /scim/v2/{orgId}/ServiceProviderConfig` | Retrieve the configuration of the Zitadel service provider |
| `GET /scim/v2/{orgId}/Schemas` | Retrieve all supported schemas |
| `GET /scim/v2/{orgId}/Schemas/{id}` | Retrieve a known supported schema |
| `GET /scim/v2/{orgId}/ResourceTypes` | Retrieve all supported resource types |
| `GET /scim/v2/{orgId}/ResourceTypes/{name}` | Retrieve a known supported resource type |
| `GET /scim/v2/{orgId}/Users/{id}` | Retrieve a known user |
| `GET /scim/v2/{orgId}/Users`<br />`POST /scim/v2/{orgId}/Users/.search` | Query users (including filtering, sorting, paging) |
| `POST /scim/v2/{orgId}/Users` | Create a user |
| `PUT /scim/v2/{orgId}/Users/{id}` | Replace a user |
| `PATCH /scim/v2/{orgId}/Users/{id}` | Modify a user |
| `DELETE /scim/v2/{orgId}/Users/{id}` | Delete a user |
| `POST /scim/v2/{orgId}/Bulk` | Apply multiple operations in a single request |
## Authentication
The SCIM interface adheres to Zitadel's standard API authentication methods.
For detailed instructions on authenticating with the SCIM interface, refer to the [Authenticate Service Users Guide](/guides/integrate/service-users/authenticate-service-users).
## Query
The list users endpoint supports sorting and filtering for both `GET /scim/v2/{orgId}/Users` and `POST /scim/v2/{orgId}/Users/.search` requests.
By default, the response includes up to 100 users, with a maximum allowable value for `count` set to 100.
### Sort
The following attributes are supported in the `SortBy` attribute.
- `meta.created`
- `meta.lastModified`
- `id`
- `username`
- `name.familyName`
- `name.givenName`
- `emails` and `emails.value`
### Filter
The following filter attributes and operators are supported:
| Attribute | Supported operators |
|------------------------------|------------------------------|
| `meta.created` | `EQ`, `GT`, `GE`, `LT`, `LE` |
| `meta.lastModified` | `EQ`, `GT`, `GE`, `LT`, `LE` |
| `id` | `EQ`, `NE`, `CO`, `SW`, `EW` |
| `externalId` | `EQ`, `NE` |
| `username` | `EQ`, `NE`, `CO`, `SW`, `EW` |
| `name.familyName` | `EQ`, `NE`, `CO`, `SW`, `EW` |
| `name.givenName` | `EQ`, `NE`, `CO`, `SW`, `EW` |
| `emails`<br />`emails.value` | `EQ`, `NE`, `CO`, `SW`, `EW` |
| `active` | `EQ`, `NE` |
Filters can have a maximum length of 1000 characters.
## Examples
Here are practical examples demonstrating how to interact with the SCIM API,
providing clear guidance on common use cases such as creating a user.
Make sure to replace any placeholder values (`${}`) with the actual values from your environment.
<details>
<summary>`POST /Users`: Create a minimal user</summary>
```bash
curl -X POST "https://${DOMAIN}/scim/v2/${ORG_ID}/Users" \
-H 'Content-Type: application/scim+json' \
-H 'Accept: application/scim+json' \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
--data-raw '
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"userName": "john.doe",
"name": {
"familyName": "Doe",
"givenName": "John"
},
"password": "Password1!",
"emails": [
{
"value": "[email protected]",
"primary": true
}
]
}
'
```
**Response (`201 Created`)**
```json
{
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User"
],
"meta": {
"resourceType": "User",
"created": "2025-01-27T15:30:27.651321Z",
"lastModified": "2025-01-27T15:30:27.651321Z",
"version": "2",
"location": "https://${DOMAIN}/scim/v2/${ORG_ID}/Users/304499468865155777"
},
"id": "304499468865155777",
"userName": "john.doe",
"name": {
"familyName": "Doe",
"givenName": "John"
},
"preferredLanguage": "en",
"emails": [
{
"value": "[email protected]",
"primary": true
}
]
}
```
</details>
<details>
<summary>`POST /Users`: Create a full user</summary>
```bash
curl -X POST "https://${DOMAIN}/scim/v2/${ORG_ID}/Users" \
-H 'Content-Type: application/scim+json' \
-H 'Accept: application/scim+json' \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
--data-raw '
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"externalId": "8d4b51c0-51bd-4386-ae17-79ce5fd36517",
"userName": "[email protected]",
"name": {
"formatted": "Mr. John J Doe, III",
"familyName": "Doe",
"givenName": "John",
"middleName": "Jim",
"honorificPrefix": "Mr.",
"honorificSuffix": "III"
},
"displayName": "John Doe",
"nickName": "Johnny",
"profileUrl": "https://login.example.com/john.doe",
"emails": [
{
"value": "[email protected]",
"type": "work",
"primary": true
}
],
"addresses": [
{
"type": "work",
"streetAddress": "100 Universal City Plaza",
"locality": "Hollywood",
"region": "CA",
"postalCode": "91608",
"country": "USA",
"formatted": "100 Universal City Plaza\nHollywood, CA 91608 USA",
"primary": true
}
],
"phoneNumbers": [
{
"value": "+1 555-555-5555",
"type": "work",
"primary": true
}
],
"ims": [
{
"value": "@j.doe",
"type": "X"
}
],
"photos": [
{
"value": "https://photos.example.com/profilephoto/john.doe/F",
"type": "photo"
}
],
"roles": [
{
"value": "user-admin",
"display": "User administrator"
}
],
"entitlements": [
{
"value": "read-passports",
"display": "Read Passports"
}
],
"userType": "Employee",
"title": "Tour Guide",
"preferredLanguage": "en-US",
"locale": "en-US",
"timezone": "America/Los_Angeles",
"active": true,
"password": "Password1!"
}'
```
**Response (`201 Created`)**
```json
{
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User"
],
"meta": {
"resourceType": "User",
"created": "2025-01-27T15:31:47.84572Z",
"lastModified": "2025-01-27T15:31:47.84572Z",
"version": "16",
"location": "https://localhost:8080/scim/v2/303879575732073153/Users/304499603368096449"
},
"id": "304499603368096449",
"externalId": "8d4b51c0-51bd-4386-ae17-79ce5fd36517",
"userName": "[email protected]",
"name": {
"formatted": "John Doe",
"familyName": "Doe",
"givenName": "John",
"middleName": "Jim",
"honorificPrefix": "Mr.",
"honorificSuffix": "III"
},
"displayName": "John Doe",
"nickName": "Johnny",
"profileUrl": "https://login.example.com/john.doe",
"title": "Tour Guide",
"preferredLanguage": "en-US",
"locale": "en-US",
"timezone": "America/Los_Angeles",
"active": true,
"emails": [
{
"value": "[email protected]",
"primary": true
}
],
"phoneNumbers": [
{
"value": "+15555555555",
"primary": true
}
],
"ims": [
{
"value": "@j.doe",
"type": "X"
}
],
"addresses": [
{
"type": "work",
"streetAddress": "100 Universal City Plaza",
"locality": "Hollywood",
"region": "CA",
"postalCode": "91608",
"country": "USA",
"formatted": "100 Universal City Plaza\nHollywood, CA 91608 USA",
"primary": true
}
],
"photos": [
{
"value": "https://photos.example.com/profilephoto/john.doe/F",
"type": "photo"
}
],
"entitlements": [
{
"value": "read-passports",
"display": "Read Passports"
}
],
"roles": [
{
"value": "user-admin",
"display": "User administrator"
}
]
}
```
</details>
<details>
<summary>`GET /Users/{id}`: Retrieve a known user</summary>
```bash
curl -G "https://${DOMAIN}/scim/v2/${ORG_ID}/Users/${USER_ID}" \
-H 'Accept: application/scim+json' \
-H "Authorization: Bearer ${ACCESS_TOKEN}"
```
**Response (`200 OK`)**
```json
{
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User"
],
"meta": {
"resourceType": "User",
"created": "2025-01-27T15:31:47.84572Z",
"lastModified": "2025-01-27T15:31:47.84572Z",
"version": "16",
"location": "https://localhost:8080/scim/v2/303879575732073153/Users/304499603368096449"
},
"id": "304499603368096449",
"externalId": "8d4b51c0-51bd-4386-ae17-79ce5fd36517",
"userName": "[email protected]",
"name": {
"formatted": "John Doe",
"familyName": "Doe",
"givenName": "John",
"middleName": "Jim",
"honorificPrefix": "Mr.",
"honorificSuffix": "III"
},
"displayName": "John Doe",
"nickName": "Johnny",
"profileUrl": "https://login.example.com/john.doe",
"title": "Tour Guide",
"preferredLanguage": "en-US",
"locale": "en-US",
"timezone": "America/Los_Angeles",
"active": true,
"emails": [
{
"value": "[email protected]",
"primary": true
}
],
"phoneNumbers": [
{
"value": "+15555555555",
"primary": true
}
],
"ims": [
{
"value": "@j.doe",
"type": "X"
}
],
"addresses": [
{
"type": "work",
"streetAddress": "100 Universal City Plaza",
"locality": "Hollywood",
"region": "CA",
"postalCode": "91608",
"country": "USA",
"formatted": "100 Universal City Plaza\nHollywood, CA 91608 USA",
"primary": true
}
],
"photos": [
{
"value": "https://photos.example.com/profilephoto/john.doe/F",
"type": "photo"
}
],
"entitlements": [
{
"value": "read-passports",
"display": "Read Passports"
}
],
"roles": [
{
"value": "user-admin",
"display": "User administrator"
}
]
}
```
</details>
<details>
<summary>`GET /Users`: List users created after a given date sorted by the creation date</summary>
```bash
curl -G "https://${DOMAIN}/scim/v2/${ORG_ID}/Users" \
-H 'Accept: application/scim+json' \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
--data-urlencode "sortBy=meta.created" \
--data-urlencode "sortOrder=descending" \
--data-urlencode "filter=meta.created gt \"2025-01-24T09:22:35.695245Z\""
```
**Response (`200 OK`)**
```json
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
"itemsPerPage": 100,
"totalResults": 1,
"startIndex": 1,
"Resources": [
{
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User"
],
"meta": {
"resourceType": "User",
"created": "2025-01-27T15:31:47.84572Z",
"lastModified": "2025-01-27T15:31:47.84572Z",
"version": "3",
"location": "https://localhost:8080/scim/v2/303879575732073153/Users/304499603368096449"
},
"id": "304499603368096449",
"externalId": "8d4b51c0-51bd-4386-ae17-79ce5fd36517",
"userName": "[email protected]",
"name": {
"formatted": "John Doe",
"familyName": "Doe",
"givenName": "John",
"middleName": "Jim",
"honorificPrefix": "Mr.",
"honorificSuffix": "III"
},
"displayName": "John Doe",
"nickName": "Johnny",
"profileUrl": "https://login.example.com/john.doe",
"title": "Tour Guide",
"preferredLanguage": "und",
"locale": "en-US",
"timezone": "America/Los_Angeles",
"active": true,
"emails": [
{
"value": "[email protected]",
"primary": true
}
],
"phoneNumbers": [
{
"value": "+15555555555",
"primary": true
}
],
"ims": [
{
"value": "@j.doe",
"type": "X"
}
],
"addresses": [
{
"type": "work",
"streetAddress": "100 Universal City Plaza",
"locality": "Hollywood",
"region": "CA",
"postalCode": "91608",
"country": "USA",
"formatted": "100 Universal City Plaza\nHollywood, CA 91608 USA",
"primary": true
}
],
"photos": [
{
"value": "https://photos.example.com/profilephoto/john.doe/F",
"type": "photo"
}
],
"entitlements": [
{
"value": "read-passports",
"display": "Read Passports"
}
],
"roles": [
{
"value": "user-admin",
"display": "User administrator"
}
]
}
]
}
```
</details>
<details>
<summary>`PATCH /Users/{id}`: Set a user inactive</summary>
```bash
curl -X PATCH "https://${DOMAIN}/scim/v2/${ORG_ID}/Users/${USER_ID}" \
-H 'Content-Type: application/scim+json' \
-H 'Accept: application/scim+json' \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
--data-raw '
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [
{
"op": "replace",
"path": "active",
"value": false
}
]
}
'
```
**Response**: `204 No Content`
</details>
<details>
<summary>`PATCH /Users/{id}`: Set the password of a user</summary>
```bash
curl -X PATCH "https://${DOMAIN}/scim/v2/${ORG_ID}/Users/${USER_ID}" \
-H 'Content-Type: application/scim+json' \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
--data-raw '
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [
{
"op": "replace",
"path": "password",
"value": "Password2!"
}
]
}
'
```
**Response**: `204 No Content`
</details>
<details>
<summary>`PUT /Users/{id}`: Replace a full user</summary>
```bash
curl -X PUT "https://${DOMAIN}/scim/v2/${ORG_ID}/Users/${USER_ID}" \
-H 'Content-Type: application/scim+json' \
-H 'Accept: application/scim+json' \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
--data-raw '
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"externalId": "8d4b51c0-51bd-4386-ae17-79ce5fd36517",
"userName": "[email protected]",
"name": {
"formatted": "Mr. John J Doe, III",
"familyName": "Doe",
"givenName": "John",
"middleName": "Jim",
"honorificPrefix": "Mr.",
"honorificSuffix": "III"
},
"displayName": "John Doe",
"nickName": "Johnny",
"profileUrl": "https://login.example.com/john.doe",
"emails": [
{
"value": "[email protected]",
"type": "work",
"primary": true
}
],
"addresses": [
{
"type": "work",
"streetAddress": "100 Universal City Plaza",
"locality": "Hollywood",
"region": "CA",
"postalCode": "91608",
"country": "USA",
"formatted": "100 Universal City Plaza\nHollywood, CA 91608 USA",
"primary": true
}
],
"phoneNumbers": [
{
"value": "+1 555-555-5555",
"type": "work",
"primary": true
}
],
"ims": [
{
"value": "@j.doe",
"type": "X"
}
],
"photos": [
{
"value": "https://photos.example.com/profilephoto/john.doe/F",
"type": "photo"
}
],
"roles": [
{
"value": "user-admin",
"display": "User administrator"
}
],
"entitlements": [
{
"value": "read-passports",
"display": "Read Passports"
}
],
"userType": "Employee",
"title": "Tour Guide",
"preferredLanguage": "en-US",
"locale": "en-US",
"timezone": "America/Los_Angeles",
"active": true,
"password": "Password1!"
}'
```
**Response (`200 OK`)**
```json
{
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User"
],
"meta": {
"resourceType": "User",
"created": "2025-01-27T15:31:47.84572Z",
"lastModified": "2025-01-27T15:31:47.84572Z",
"version": "16",
"location": "https://localhost:8080/scim/v2/303879575732073153/Users/304499603368096449"
},
"id": "304499603368096449",
"externalId": "8d4b51c0-51bd-4386-ae17-79ce5fd36517",
"userName": "[email protected]",
"name": {
"formatted": "John Doe",
"familyName": "Doe",
"givenName": "John",
"middleName": "Jim",
"honorificPrefix": "Mr.",
"honorificSuffix": "III"
},
"displayName": "John Doe",
"nickName": "Johnny",
"profileUrl": "https://login.example.com/john.doe",
"title": "Tour Guide",
"preferredLanguage": "en-US",
"locale": "en-US",
"timezone": "America/Los_Angeles",
"active": true,
"emails": [
{
"value": "[email protected]",
"primary": true
}
],
"phoneNumbers": [
{
"value": "+15555555555",
"primary": true
}
],
"ims": [
{
"value": "@j.doe",
"type": "X"
}
],
"addresses": [
{
"type": "work",
"streetAddress": "100 Universal City Plaza",
"locality": "Hollywood",
"region": "CA",
"postalCode": "91608",
"country": "USA",
"formatted": "100 Universal City Plaza\nHollywood, CA 91608 USA",
"primary": true
}
],
"photos": [
{
"value": "https://photos.example.com/profilephoto/john.doe/F",
"type": "photo"
}
],
"entitlements": [
{
"value": "read-passports",
"display": "Read Passports"
}
],
"roles": [
{
"value": "user-admin",
"display": "User administrator"
}
]
}
```
</details>
<details>
<summary>`DELETE /Users/{id}`: Delete a user</summary>
```bash
curl -X DELETE "https://${DOMAIN}/scim/v2/${ORG_ID}/Users/${USER_ID}" \
-H "Authorization: Bearer ${ACCESS_TOKEN}"
```
**Response**: `204 No Content`
</details>
<details>
<summary>`POST /Bulk`: Update the password of one user and delete another one</summary>
```bash
curl -X POST "https://${DOMAIN}/scim/v2/${ORG_ID}/Bulk" \
-H 'Content-Type: application/scim+json' \
-H 'Accept: application/scim+json' \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
--data-raw '
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:BulkRequest"],
"Operations": [
{
"method": "PATCH",
"path": "/Users/${USER_ID}",
"data": {
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [
{
"op": "replace",
"path": "password",
"value": "Password2!"
}
]
}
},
{
"method": "DELETE",
"path": "/Users/${USER_ID2}"
}
]
}'
```
**Response**: `200 OK`
```json
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:BulkResponse"],
"Operations": [
{
"method": "PATCH",
"location": "https://${DOMAIN}/scim/v2/${ORG_ID}/Users/${USER_ID}",
"status": "204"
},
{
"method": "DELETE",
"location": "https://${DOMAIN}/scim/v2/${ORG_ID}/Users/${USER_ID2}",
"status": "204"
}
]
}
```
</details>
<details>
<summary>`GET /ServiceProviderConfig`: Get service provider configuration</summary>
```bash
curl -G "https://${DOMAIN}/scim/v2/${ORG_ID}/ServiceProviderConfig" \
-H 'Accept: application/scim+json'
```
**Response**: `200 OK`
```json
{
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"
],
"meta": {
"resourceType": "ServiceProviderConfig",
"location": "https://${DOMAIN}/scim/v2/${ORG_ID}/ServiceProviderConfig"
},
"documentationUri": "https://zitadel.com/docs/guides/manage/user/scim2",
"patch": {
"supported": true
},
"bulk": {
"supported": true,
"maxOperations": 100,
"maxPayloadSize": 1000000
},
"filter": {
"supported": true,
"maxResults": 100
},
"changePassword": {
"supported": true
},
"sort": {
"supported": true
},
"etag": {
"supported": false
},
"authenticationSchemes": [
{
"name": "Zitadel authentication token",
"description": "Authentication scheme using the OAuth Bearer Token Standard",
"specUri": "https://www.rfc-editor.org/info/rfc6750",
"documentationUri": "https://zitadel.com/docs/guides/integrate/service-users/authenticate-service-users",
"type": "oauthbearertoken",
"primary": false
}
]
}
```
</details>
## Error handling
The SCIM interface uses standard HTTP status codes and error messages to indicate the success or failure of API requests
following the error handling guidelines of [RFC7644](https://datatracker.ietf.org/doc/html/rfc7644#section-3.12).
In addition to the default SCIM error schema (`urn:ietf:params:scim:api:messages:2.0:Error`),
Zitadel extends the error response with a custom schema, `urn:ietf:params:scim:api:zitadel:messages:2.0:ErrorDetail`.
This schema includes additional attributes, such as the untranslated error message and an error id,
which aids pinpointing the source of the error in the system.
## Resources
- **[Zitadel SCIM Documentation](/guides/manage/user/scim2)**: Documentation of Zitadel's SCIM implementation, including configuration details and known limitations.
- **[SCIM](https://scim.cloud/)**: The Webpage of SCIM.
- **[RFC7643](https://tools.ietf.org/html/rfc7643) Core Schema**:
The Core Schema provides a platform-neutral schema and extension model for representing users and groups.
- **[RFC7644](https://tools.ietf.org/html/rfc7644) Protocol**:
The SCIM Protocol is an application-level, REST protocol for provisioning and managing identity data on the web.
- **[RFC7642](https://tools.ietf.org/html/rfc7642) Definitions, Overview, Concepts, and Requirements**:
This document lists the user scenarios and use cases of System for Cross-domain Identity Management (SCIM).
+19
View File
@@ -0,0 +1,19 @@
---
title: GRPC Status Codes in ZITADEL
sidebar_label: GRPC Status Codes
---
| GRPC Number | GRPC Code | HTTP Status Code | HTTP Status Text |Description |
| :---------| :------------------- | ------------------ | ------------- | -------------------------------------------------------------------------------------------------------------------------- |
| 0 | OK | 200 |OK | Not an error; returned on success. |
| 2 | UNKNOWN | 500 |Internal | Unknown error, this is sent if the error could not be identified as one of the errors described below |
| 3 | INVALID_ARGUMENT | 400 |Bad Request | The client specified an invalid argument. Note that this differs from FAILED_PRECONDITION. INVALID_ARGUMENT indicates arguments that are problematic regardless of the state of the system (e.g., a malformed file name). |
| 4 | DEADLINE_EXCEEDED | 504 |Gateway Timeout | The deadline expired before the operation could complete. |
| 5 | NOT_FOUND | 404 |Not found | Some requested entity (e.g. user or project) was not found. |
| 6 | ALREADY_EXISTS | 409 |Conflict | The entity that a client attempted to create (e.g. user or project) already exists. |
| 7 | PERMISSION_DENIED | 403 |Forbidden |The caller does not have permission to execute the specified operation. |
| 9 | FAILED_PRECONDITION | 400 |Bad Request | The operation was rejected because the system is not in a state required for the operation's execution. e.g a project that is already deactivated, should be deactivated |
| 12 | UNIMPLEMENTED | 501 |Not Implemented |The operation is not implemented or is not supported/enabled in this service. |
| 13 | INTERNAL | 500 |Internal |Internal errors. This means that some invariants expected by the underlying system have been broken. This error code is reserved for serious errors. |
| 14 | UNAVAILABLE | 503 |Service Unavailable | The service is currently unavailable. |
| 16 | UNAUTHENTICATED | 401 |Unauthorized |The request does not have valid authentication credentials for the operation. |
+13
View File
@@ -0,0 +1,13 @@
---
title: APIs V2 (Generally Available)
---
import DocCardList from '@/components/docusaurus/doc-card-list';
APIs V2 organize access by resources (users, settings, etc.), unlike context-specific V1 APIs.
This simplifies finding the right API, especially for multi-organization resources.
Users created with the V2 API have no initial state anymore, so new users are immediately active.
Requesting ZITADEL to send a verification email on user creation is still possible.
<DocCardList />
@@ -0,0 +1,137 @@
---
title: How ZITADEL Processes and Stores Secrets
sidebar_label: Secrets
---
In this chapter you can find information of how ZITADEL processes and stores secrets and credentials in a secure fashion.
<Callout>
We use the terms secret and credentials interchangeable to keep this guide lean.
</Callout>
## Secrets Principles
ZITADEL uses the following principles when handling Secrets across their lifecycle:
- Automate rotation
- Limit lifetime
- Show only once
- Prefer public / private key concepts (FIDO2, U2F, JWT Profile, ...)
- Irreversible hash secrets / passwords
- Encrypt secrets storage
- When sent across unsecure channels (eMail, SMS, ...)
- Forced changed through receiver
- Verify that the secret can only be used once
## Secrets Storage
By default ZITADEL stores secrets from its users, clients as well as its generated secrets like signing keys in the database.
To protect the secrets against extraction from database as well as database dumps they are encrypted with AES256.
<Callout>
The key used to encrypt and decrypt the secrets in the ZITADEL database is called `masterkey` and needs to be exactly 32 bytes long.
The only secrets stored outside of the Secrets Storage are the masterkey, the TLS Keys, the initial Admin User (including the password)
</Callout>
## Secrets stored in the Secrets Storage
### Public Keys
ZITADEL does handle many different public keys. These include:
- FIDO2
- U2F
- JWT Profile
- Signing Keys
<Callout>
Due to the inherent nature of a public key being public we safeguard them against malicious key changes with our unique [eventstore concept](../eventstore/overview).
</Callout>
### Hashed Secrets
ZITADEL does handle many different passwords and secrets. These include:
- User Authentication
- Password
- Client / Machine Authentication
- Client Secrets
<Callout>
ZITADEL hashes all Passwords and Client Secrets in an non reversible way to further reduce the risk of a Secrets Storage breach.
</Callout>
Passwords and secrets are always hashed with a random salt and stored as an encoded string that contains the Algorithm, its Parameters, Salt and Hash.
The storage encoding used by ZITADEL is Modular Crypt Format and a full reference can be found in our [Passwap library](https://github.com/zitadel/passwap#encoding).
The following hash algorithms are supported:
- argon2i / id[^1]
- bcrypt (Default)
- md5: implementation of md5Crypt with salt and password shuffling [^2]
- md5plain: md5 digest of a password without salt [^2]
- md5salted: md5 digest of a salted password [^2]
- phpass: md5 digest with PHPass algorithm (used in WordPress) [^2]
- drupal7: Custom hashing format used in Drupal 7 [^2]
- sha2: implementation of crypt(3) SHA-256 & SHA-512
- scrypt
- pbkdf2
[^1]: argon2 algorithms are currently disabled on ZITADEL Cloud due to its steep memory requirements.
[^2]: md5 and drupal7 are insecure and can only be used to import and verify users, not hash new passwords.
<Callout>
ZITADEL updates stored hashes when the configured algorithm or its parameters are updated,
the first time verification succeeds.
This allows to increase cost along with growing computing power.
ZITADEL allows to import user passwords from systems that use any of the above hashing algorithms.
Note however that by default, only `bcrypt` is enabled.
Further `Verifiers` must be enabled in the [configuration](/self-hosting/manage/configure) by the system administrator.
</Callout>
### Encrypted Secrets
Some secrets cannot be hashed because they need to be used in their raw form. These include:
- Federation
- Client Secrets of Identity Providers (IdPs)
- Multi-factor Authentication
- TOTP Seed Values
- Validation Secrets
- Verifying contact information like eMail, Phonenumbers
- Verifying proof of ownership over domain names (DNS)
- Resting accounts of users (password, MFA reset, ...)
- Private Keys
- Token Signing (JWT, ...)
- Token Encryption (Opaque Bearer Tokens)
- Useragent Cookies (Session Cookies) Encryption
- CSRF Cookie Encryption
- Mail Provider
- SMTP Passwords
- SMS Provider
- Twilio API Keys
<Callout>
By default ZITADEL uses `RSA256` for signing purposes and `AES256` for encryption
</Callout>
## Secrets stored outside the Secrets Storage
### Masterkey
Since the Masterkey is used as means of protecting the Secrets Storage it cannot be stored in the storage.
You find [here the many ways how ZITADEL can consume the Masterkey](/self-hosting/manage/configure).
### TLS Material
ZITADEL does support end to end TLS as such it can consume TLS Key Material.
Please check our [TLS Modes documentation](/self-hosting/manage/tls_modes) for more details.
### Admin User
The initial Admin User of ZITADEL can be configured through [ZITADELs config options](/self-hosting/manage/configure).
<Callout>
To prevent elevated breaches ZITADEL forces the Admin Users password to be changed during the first login.
</Callout>
@@ -0,0 +1,151 @@
---
title: Zitadel's Software Architecture
sidebar_label: Software Architecture
---
Zitadel is built with two essential patterns. Event Sourcing (ES) and Command and Query Responsibility Segregation (CQRS).
Due to the nature of Event Sourcing Zitadel provides the unique capability to generate a strong audit trail of ALL the things that happen to its resources, without compromising on storage cost or audit trail length.
The combination of ES and CQRS makes Zitadel eventual consistent which, from our perspective, is a great benefit in many ways.
It allows us to build a Source of Records (SOR) which is the one single point of truth for all computed states.
The SOR needs to be transaction safe to make sure all operations are in order.
You can read more about this in our [ES documentation](../eventstore/overview).
Each Zitadel binary contains all components necessary to serve traffic
From serving the API, rendering GUI's, background processing of events and task.
This All in One (AiO) approach makes operating Zitadel simple.
## The Architecture
Zitadels software architecture is built around multiple components at different levels.
This chapter should give you an idea of the components as well as the different layers.
![](../../../public/img/zitadel_software_architecture.png)
### Service Layer
The service layer includes all components who are potentially exposed to consumers of Zitadel.
#### HTTP Server
The http server is responsible for the following functions:
- serving the management GUI called Zitadel Management Console
- serving the static assets
- rendering server side html (login, password-reset, verification, ...)
#### API Server
The API layer consist of the multiple APIs provided by Zitadel. Each serves a dedicated purpose.
All APIs of Zitadel are always available as gRCP, gRPC-web and REST service.
The only exception is the [OpenID Connect & OAuth](/apis/openidoauth/endpoints) and [Asset API](/apis/introduction#assets) due their unique nature.
- [OpenID Connect & OAuth](/apis/openidoauth/endpoints) - allows to request authentication and authorization of Zitadel
- [SAML](/apis/saml/endpoints) - allows to request authentication and authorization of Zitadel through the SAML standard
- [Authentication API](/apis/introduction#authentication) - allow a user to do operation in its own context
- [Management API](/apis/introduction#management) - allows an admin or machine to manage the Zitadel resources on an organization level
- [Administration API](/apis/introduction#administration) - allows an admin or machine to manage the Zitadel resources on an instance level
- [System API](/apis/introduction#system) - allows to create and change new Zitadel instances
- [Asset API](/apis/introduction#assets) - is used to upload and download static assets
### Core Layer
#### Commands
The Command Side has some unique requirements, these include:
- Transaction safety is a MUST
- Availability MUST be high
> When we classify this with the CAP theorem we would choose Consistent and Available but leave Partition Tolerance aside.
##### Command Handler
The command handler receives all operations who alter a resource managed by Zitadel.
For example if a user changes his name. The API Layer will pass the instruction received through the API call to the command handler for further processing.
The command handler is then responsible of creating the necessary commands.
After creating the commands the command hand them down to the command validation.
##### Command Validation
With the received commands the command validation will execute the business logic to verify if a certain action can take place.
For example if the user really can change his name is verified in the command validation.
If this succeeds the command validation will create the events that reflect the changes.
These events now are being handed down to the storage layer for storage.
#### Events
Zitadel handles events in two ways.
Events that should be processed in near real time are processed by a in memory pub sub system.
Some events can be handled asynchronously using the spooler.
##### Pub Sub
The pub sub system job is it to keep a query view up-to-date by feeding a constant stream of events to the projections.
Our pub sub system built into Zitadel works by placing events into an in memory queue for its subscribers.
There is no need for specific guarantees from the pub sub system. Since the SOR is the ES everything can be retried without loss of data.
In case of an error an event can be reapplied in two ways:
- The next event might trigger the projection to apply the whole difference
- The spooler takes care of background cleanups in a scheduled fashion
> The decision to incorporate an internal pub sub system with no need for specific guarantees is a deliberate choice.
> We believe that the toll of operating an additional external service like a MQ system negatively affects the ease of use of Zitadel as well as its availability guarantees.
> One of the authors of Zitadel did his thesis to test this approach against established MQ systems.
##### Spooler
The spoolers job is it to keep a query view up-to-date or at least look that it does not have a too big lag behind the Event Store.
Each query view has its own spooler which is responsible to look for the events who are relevant to generate the query view. It does this by triggering the relevant projection.
Spoolers are especially necessary where someone can query datasets instead of single ids.
> Each view can have exactly one spooler, but spoolers are dynamically leader elected, so even if a spooler crashes it will be replaced in a short amount of time.
#### Projections
Projections are responsible of normalizing data for the query side or for analytical purpose.
They generally work by being invoked either through a scheduled spooler or the pub sub subscription.
When they receive events they will create their normalized object and then store this into the query view and its storage layer.
#### Queries
The query side is responsible for answering read requests on data.
It has some unique requirements, which include:
- It needs to be easy to query
- Short response times are a MUST (80%of queries below 100ms on the api server)
- Availability MUST be high, even during high loads
- The query view MUST be able to be persisted for most request
> When we classify this with the CAP theorem we would choose **Available** and **Performance** but leave **Consistent** aside
##### Query Handler
The query handler receives all read relevant operations. These can either be query or simple `getById` calls.
When receiving a query it will proceed by passing this to the repository which will call the database and return the dataset.
If a request calls for a specific id the call will, most of the times, be revalidated against the Event Store.
This is achieved by triggering the projection to make sure that the last sequence of a id is loaded into the query view.
> The query side has the option to dynamically check the Event Store for newer events on a certain id to make sure for consistent responses without delay.
##### Query View
The query view is responsible to query the storage layer with the request from the command handler.
It is also responsible to execute authorization checks. To check if a request is valid and can be answered.
### Storage Layer
As Zitadel itself is built completely stateless only the storage layer is needed to persist states.
The storage layer of Zitadel is responsible for multiple tasks. For example:
- Guarantee strong consistency for the command side
- Guarantee good query performance for the query side
- Backup and restore operation for disaster recovery purpose
Zitadel currently supports PostgreSQL.
Make sure to read our [Production Guide](/self-hosting/manage/production#prefer-postgre-sql) before you decide on using one of them.
<Callout>
Zitadel v2 supported CockroachDB and PostgreSQL. Zitadel v3 only supports PostgreSQL. Please refer to [the mirror guide](/self-hosting/manage/cli/mirror) to migrate to PostgreSQL.
</Callout>
@@ -0,0 +1,73 @@
---
title: Zitadel's Deployment Architecture
sidebar_label: Deployment Architecture
---
## High Availability
Zitadel can be run as high available system with ease.
Since the storage layer takes the heavy lifting of making sure that data in synched across, server, data centers or regions.
Depending on your projects needs our general recommendation is to run Zitadel across multiple availability zones in the same region or across multiple regions.
Make sure to read our [Production Guide](/self-hosting/manage/production#prefer-postgre-sql) before you decide to use it.
Consult the [Postgres documentation](https://www.postgresql.org/docs/) for more details.
## Scalability
Zitadel can be scaled in a linear fashion in multiple dimensions.
- Vertical on your compute infrastructure
- Horizontal in a region
- Horizontal in multiple regions
Our customers can reuse the same already known binary or container and scale it across multiple server, data center and regions.
To distribute traffic an already existing proxy infrastructure can be reused.
Simply steer traffic by path, hostname, IP address or any other metadata to the Zitadel of your choice.
> To improve your service quality we recommend steering traffic by path to different Zitadel deployments
> Feel free to [contact us](https://zitadel.com/contact/) for details
## Example Deployment Architecture
### Single Cluster / Region
A Zitadel Cluster is a highly available IAM system with each component critical for serving traffic laid out at least three times.
Our storage layer (Postgres) is built for single region deployments.
Hence our reference design for Kubernetes is to have three application nodes and one storage node.
> If you are using a serverless offering like Google Cloud Run you can scale Zitadel from 0 to 1000 Pods without the need of deploying the node across multiple availability zones.
![](../../../public/img/zitadel_cluster_architecture.png)
### Multi Cluster / Region
To scale Zitadel across regions it is recommend to create at least three clusters.
Each cluster is a fully independent ZITADEL setup.
To keep the data in sync across all clusters, we recommend using Postgres with read-only replicas as a storage layer.
Make sure to read our [Production Guide](/self-hosting/manage/production#prefer-postgre-sql) before you decide to use it.
Consult the [Postgres documentation](https://www.postgresql.org/docs/current/high-availability.html) for more details.
![](../../../public/img/zitadel_multicluster_architecture.png)
## Zero Downtime Updates
Since an Identity system tends to be a critical piece of infrastructure, the "in place zero downtime update" is a well needed feature.
Zitadel is built in a way that upgrades can be executed without downtime by just updating to a more recent version.
The common update involves the following steps and do not need manual intervention of the operator:
- Keep the old version running
- Deploy the version in parallel to the old version
- The new version will start ...
- by updating databases schemas if needed
- participate in the leader election for background jobs
- As soon as the new version is ready to accept traffic it will signal this on the readiness endpoint `/debug/ready`
- At this point your network infrastructure can send traffic to the new version
Users who use [Kubernetes/Helm](/self-hosting/deploy/kubernetes) or serverless container services like Google Cloud Run can benefit from the fact the above process is automated.
<Callout>
As a good practice we recommend creating Database Backups prior to an update.
It is also recommend to read the release notes on GitHub before upgrading.
Since Zitadel utilizes Semantic Versioning Breaking Changes of any kind will always increase the major version (e.g Version 2 would become Version 3).
</Callout>

Some files were not shown because too many files have changed in this diff Show More