feat(database): enhance PostgreSQL setup documentation and commands for non-admin access (#11631)

Users deploying ZITADEL against a managed PostgreSQL service (RDS, Cloud
SQL, Azure Database, etc.) often do not have superuser access and cannot
provide `Admin.*` credentials. The documented workaround — provisioning
the user and database manually and then running `start-from-setup` —
silently skips schema bootstrapping, causing `relation
"eventstore.events" does not exist` errors with no clear recovery path.

The root cause is that `zitadel init` conflates two steps that require
different privileges without exposing them separately:

- **Provisioning step** (`CREATE ROLE`, `CREATE DATABASE`, `GRANT`) —
requires superuser.
- **Schema bootstrapping step** (create
`eventstore`/`projections`/`system` schemas and base tables) — requires
only DB owner.

Users who handle the provisioning step externally have no supported way
to run schema bootstrapping alone.

## Changes

- **`cmd/initialise/verify_schema.go`** (renamed from
`verify_zitadel.go`): Rename `newZitadel()` → `newSchema()` (internal);
rename the `init zitadel` sub-command to `zitadel init schema`
(backwards-compatible alias kept) with a clear description that it
bootstraps the ZITADEL database schema without admin/superuser
privileges. Fix stale error log message to reference `init schema`.
- **`cmd/initialise/verify_schema_test.go`** (renamed from
`verify_zitadel_test.go`): Test file renamed to match source file.
- **`cmd/initialise/init.go`**: Update call site to `newSchema()`. Add
guidance in the `init` command's Long description about using `zitadel
init schema` for users without admin credentials.
- **`cmd/initialise/verify_database.go`**: Add a `pg_database` catalog
pre-check before attempting `CREATE DATABASE`, so `zitadel init` with
`ADMIN=service_user` no longer fails with `permission denied to create
database` when the database was already provisioned externally.
- **`cmd/initialise/verify_database_test.go`**: Add test cases covering
the new catalog-check skip path, the existing error-skip path, and the
error-propagation path when the `pg_database` query itself fails.
- **`apps/docs/content/self-hosting/manage/database/index.mdx`**: Inline
the `_postgres.mdx` partial (now only PostgreSQL is supported), add a
top-level callout, and add a **Managed PostgreSQL / No Admin Access**
section with explicit 3-step instructions and security guidance (strong
passwords, SSL, TLS). Additional improvements: add inline `# Use
'require' or 'verify-full' for production` comments on `Mode: disable`
lines in the YAML example; add clarifying comment to the redundant
`GRANT` in the SQL snippet; replace admonition syntax with proper
Fumadocs `<Callout>` components; add full database connection env vars
to the `start-from-setup` example.
- **`apps/docs/content/self-hosting/manage/updating_scaling.mdx`**:
Rewrite the init phase description to clearly distinguish the
provisioning and schema bootstrapping steps, and document `zitadel init
schema` as the path for manual provisioning. Replace admonition syntax
with proper Fumadocs `<Callout>` components.
- **`apps/docs/content/self-hosting/manage/database/_postgres.mdx`**:
Deleted (content merged into `index.mdx`).

## Problem

This relates to https://github.com/zitadel/zitadel/discussions/9363

I think we can improve our UX in cases where a user wants to use an
external DB and/or does not want to share too broad permissions with
zitadel

## Related problems

* https://github.com/zitadel/zitadel/issues/10432
* https://github.com/zitadel/zitadel/discussions/8583
* https://github.com/zitadel/zitadel/issues/7903
* https://github.com/zitadel/zitadel/issues/9718
* https://github.com/zitadel/zitadel/issues/8012
* https://github.com/zitadel/zitadel/issues/8558

## Related PRs

https://github.com/zitadel/zitadel/pull/11021
This commit is contained in:
Florian Forster
2026-03-03 07:05:32 +01:00
committed by GitHub
parent 9fb0eea125
commit 92a628d892
8 changed files with 195 additions and 78 deletions
@@ -1,56 +0,0 @@
## ZITADEL with Postgres
PostgreSQL is the default database for ZITADEL due to its reliability, robustness, and adherence to SQL standards. It is well-suited for handling the complex data requirements of an identity management system.
If you are using Zitadel v2 and want to use a PostgreSQL database, you can [overwrite the default configuration](/self-hosting/manage/configure/configure).
Currently, versions 14 to 18 are supported.
Postgres can be configured as follows:
```yaml
Database:
postgres:
Host: localhost
Port: 5432
Database: zitadel
AwaitInitialConn: 5m
MaxOpenConns: 15
MaxIdleConns: 10
MaxConnLifetime: 1h
MaxConnIdleTime: 5m
Options:
User:
Username: zitadel
Password: zitadel
SSL:
Mode: disable
RootCert:
Cert:
Key:
Admin:
Username: postgres
Password: postgres
SSL:
Mode: disable
RootCert:
Cert:
Key:
```
The admin user is only needed for the installation step but can also be avoided
by preparing the necessary requirements:
* the zitadel user
* the zitadel database
* granting required permissions to the zitadel user
```sql
CREATE ROLE zitadel LOGIN;
CREATE DATABASE zitadel;
GRANT CONNECT, CREATE ON DATABASE zitadel TO zitadel;
```
Don't forget to adjust `pg_hba.conf` and set a password for the zitadel user.
With the setup done, follow the [phases guide](/self-hosting/manage/updating_scaling#separating-init-and-setup-from-the-runtime)
to run the init and then setup phase to get all necessary tables and data set up.
@@ -3,11 +3,117 @@ title: Database
description: Configure and manage the ZITADEL database using PostgreSQL for persistent storage and audit logs
---
import Postgres from './_postgres.mdx'
## Database Configuration
<Postgres components={props.components} />
<Callout type="info">
Even if you provision the PostgreSQL user and database manually, you must still run `zitadel init schema`
to bootstrap the required schemas and tables before running `start-from-setup` or `zitadel start`.
Skipping this step causes `relation "eventstore.events" does not exist` errors.
See the [Managed PostgreSQL / No Admin Access](#managed-postgresql--no-admin-access) section below for the exact steps.
The command is `zitadel init schema` (alias: `zitadel init zitadel` for backwards compatibility).
</Callout>
## ZITADEL with Postgres
PostgreSQL is the default database for ZITADEL due to its reliability, robustness, and adherence to SQL standards. It is well-suited for handling the complex data requirements of an identity management system.
If you are using Zitadel v2 and want to use a PostgreSQL database, you can [overwrite the default configuration](/self-hosting/manage/configure/configure).
Currently, versions 14 to 18 are supported.
Postgres can be configured as follows:
```yaml
Database:
postgres:
Host: localhost
Port: 5432
Database: zitadel
AwaitInitialConn: 5m
MaxOpenConns: 15
MaxIdleConns: 10
MaxConnLifetime: 1h
MaxConnIdleTime: 5m
Options:
User:
Username: zitadel
Password: zitadel
SSL:
Mode: disable # Use 'require' or 'verify-full' for production
RootCert:
Cert:
Key:
Admin:
Username: postgres
Password: postgres
SSL:
Mode: disable # Use 'require' or 'verify-full' for production
RootCert:
Cert:
Key:
```
### Managed PostgreSQL / No Admin Access
If you don't have a PostgreSQL superuser available (e.g. managed services like RDS, Cloud SQL, or Azure Database),
you can pre-provision the user and database yourself and skip the admin-credential requirement.
The `init` command performs two steps that require different privileges:
- **Provisioning step** — `CREATE ROLE`, `CREATE DATABASE`, `GRANT` → requires a superuser/admin.
- **Schema bootstrapping step** — creates schemas (`eventstore`, `projections`, `system`) and tables → requires only database owner privileges.
If you handle the provisioning step manually, you still **must** run the schema bootstrapping step. Use the dedicated sub-command `zitadel init schema`, which performs only schema bootstrapping using the service user credentials — no admin privileges needed.
**Provisioning step — Provision user and database (run as superuser)**
<Callout type="warn">
Replace `<strong-password>` with a strong, unique password and store it in a secret manager. Do not use trivial passwords in production.
</Callout>
```sql
CREATE ROLE zitadel LOGIN PASSWORD '<strong-password>';
CREATE DATABASE zitadel WITH OWNER zitadel;
-- The GRANT below is redundant when zitadel is the database owner,
-- but is included for clarity. Owners implicitly hold all privileges.
GRANT CONNECT, CREATE ON DATABASE zitadel TO zitadel;
```
Adjust `pg_hba.conf` if needed to allow the zitadel user to connect.
**Schema bootstrapping step — Bootstrap schemas and tables (run as the service user)**
Set only the service user credentials — no `Admin.*` variables are needed for this command.
For managed/remote PostgreSQL, set `SSL_MODE` to `require` or `verify-full` to encrypt the connection. `disable` is only appropriate for local development.
```bash
ZITADEL_DATABASE_POSTGRES_HOST=<your-host> \
ZITADEL_DATABASE_POSTGRES_PORT=5432 \
ZITADEL_DATABASE_POSTGRES_DATABASE=zitadel \
ZITADEL_DATABASE_POSTGRES_USER_USERNAME=zitadel \
ZITADEL_DATABASE_POSTGRES_USER_PASSWORD=<strong-password> \
ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE=require \
zitadel init schema
```
**Run setup and start**
<Callout type="note">
For production deployments, configure TLS to encrypt authentication traffic. See the [TLS configuration guide](/self-hosting/manage/tls_modes) for the available modes.
</Callout>
```bash
ZITADEL_DATABASE_POSTGRES_HOST=<your-host> \
ZITADEL_DATABASE_POSTGRES_PORT=5432 \
ZITADEL_DATABASE_POSTGRES_DATABASE=zitadel \
ZITADEL_DATABASE_POSTGRES_USER_USERNAME=zitadel \
ZITADEL_DATABASE_POSTGRES_USER_PASSWORD=<strong-password> \
ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE=require \
zitadel start-from-setup --masterkey "<32-character-key>"
```
See the [phases guide](/self-hosting/manage/updating_scaling#separating-init-and-setup-from-the-runtime) for details on separating init, setup, and runtime for production deployments.
## Zitadel Credentials