Files
zitadel/cmd/initialise/init.go
Florian ForsterandGitHub 92a628d892 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
2026-03-03 07:05:32 +01:00

171 lines
3.9 KiB
Go

package initialise
import (
"context"
"embed"
"errors"
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/zitadel/zitadel/backend/v3/instrumentation/logging"
"github.com/zitadel/zitadel/internal/database"
)
var (
//go:embed sql/*.sql
stmts embed.FS
createUserStmt string
grantStmt string
databaseStmt string
createEventstoreStmt string
createProjectionsStmt string
createSystemStmt string
createEncryptionKeysStmt string
createEventsStmt string
createUniqueConstraints string
roleAlreadyExistsCode = "42710"
dbAlreadyExistsCode = "42P04"
)
func New() *cobra.Command {
cmd := &cobra.Command{
Use: "init",
Short: "initialize ZITADEL instance",
Long: `Sets up the minimum requirements to start ZITADEL.
Prerequisites:
- PostgreSql database
The user provided by flags needs privileges to
- create the database if it does not exist
- see other users and create a new one if the user does not exist
- grant all rights of the ZITADEL database to the user created if not yet set
If you don't have admin/superuser credentials (e.g. on a managed PostgreSQL
service), you can provision the database user and database manually and then
run only 'zitadel init schema' to bootstrap the required schemas and tables
using the service user credentials. No admin privileges are needed for that
sub-command. See the database documentation for details.
`,
RunE: func(cmd *cobra.Command, args []string) (err error) {
defer func() {
logging.OnError(cmd.Context(), err).Error("zitadel init command failed")
}()
config, shutdown, err := NewConfig(cmd, viper.GetViper())
if err != nil {
return err
}
defer func() {
err = errors.Join(err, shutdown(cmd.Context()))
}()
return InitAll(cmd.Context(), config)
},
}
cmd.AddCommand(newSchema(), newDatabase(), newUser(), newGrant())
return cmd
}
func InitAll(ctx context.Context, config *Config) error {
err := initialise(ctx, config.Database,
VerifyUser(config.Database.Username(), config.Database.Password()),
VerifyDatabase(config.Database.DatabaseName()),
VerifyGrant(config.Database.DatabaseName(), config.Database.Username()),
)
if err != nil {
return fmt.Errorf("initialize database failed: %w", err)
}
err = verifyZitadel(ctx, config.Database)
if err != nil {
return fmt.Errorf("initialize ZITADEL failed: %w", err)
}
return nil
}
func initialise(ctx context.Context, config database.Config, steps ...func(context.Context, *database.DB) error) error {
logging.Info(ctx, "initialization started")
err := ReadStmts()
if err != nil {
return err
}
db, err := database.Connect(config, true)
if err != nil {
return err
}
defer db.Close()
return Init(ctx, db, steps...)
}
func Init(ctx context.Context, db *database.DB, steps ...func(context.Context, *database.DB) error) error {
for _, step := range steps {
if err := step(ctx, db); err != nil {
return err
}
}
return nil
}
func ReadStmts() (err error) {
createUserStmt, err = readStmt("01_user")
if err != nil {
return err
}
databaseStmt, err = readStmt("02_database")
if err != nil {
return err
}
grantStmt, err = readStmt("03_grant_user")
if err != nil {
return err
}
createEventstoreStmt, err = readStmt("04_eventstore")
if err != nil {
return err
}
createProjectionsStmt, err = readStmt("05_projections")
if err != nil {
return err
}
createSystemStmt, err = readStmt("06_system")
if err != nil {
return err
}
createEncryptionKeysStmt, err = readStmt("07_encryption_keys_table")
if err != nil {
return err
}
createEventsStmt, err = readStmt("08_events_table")
if err != nil {
return err
}
createUniqueConstraints, err = readStmt("10_unique_constraints_table")
if err != nil {
return err
}
return nil
}
func readStmt(step string) (string, error) {
stmt, err := stmts.ReadFile("sql/" + step + ".sql")
return string(stmt), err
}