mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-18 05:52:51 +00:00
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
139 lines
6.2 KiB
Plaintext
139 lines
6.2 KiB
Plaintext
---
|
|
title: Database
|
|
description: Configure and manage the ZITADEL database using PostgreSQL for persistent storage and audit logs
|
|
---
|
|
|
|
## Database Configuration
|
|
|
|
<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
|
|
|
|
The [init phase](/self-hosting/manage/updating_scaling#separating-init-and-setup-from-the-runtime) of ZITADEL creates the ZITADEL database user (`Database.*.User.Username` & `Database.*.User.Password`) if it does not exist and admin credentials are provided. **Init does not update or deprecate existing users or passwords.** If you need to change the ZITADEL database user or rotate its password, you must do this manually.
|
|
|
|
<Callout type="warn">
|
|
The `init` command is designed to be run **only once**, during the initial setup of ZITADEL. Running `init` again after changing the database user or credentials does **not** migrate database object ownerships, nor does it reassign permissions on schemas, tables, or other objects to a new user. You must update these permissions manually if you switch users.
|
|
</Callout>
|
|
|
|
### Rotating Database Credentials
|
|
|
|
If you must rotate the credentials:
|
|
|
|
- You can change the password for the existing ZITADEL user, but you must manually update the password wherever ZITADEL needs it.
|
|
- Creating a new database user requires manual reassignment of ownership and privileges for all required database objects (schemas, tables, etc.) to the new user.
|
|
- After migration, remove or deprecate the old user using admin access and commands appropriate for your database provider.
|
|
|
|
Attempting to run `init` a second time with new credentials will fail if the user already exists, and will not change existing permissions or migrate data.
|
|
|
|
<Callout type="warn">
|
|
Dropping and recreating the database does **not** automatically remove existing users. Always verify and clean up old users if necessary.
|
|
</Callout>
|