vxcontrol/pentagi · error

failed to create schema %q: %w

Error message

failed to create schema %q: %w

What it means

Inside the tenant-bootstrap advisory lock, the code runs CREATE SCHEMA IF NOT EXISTS <schema> (name quoted with pq.QuoteIdentifier). This error wraps any PostgreSQL rejection of that DDL — the schema name itself is pre-validated, so failures are almost always privilege or server-state problems. Bootstrap aborts and the tenant's search_path is never wired up.

Source

Thrown at backend/pkg/database/tenant.go:60

	db, err := sql.Open("postgres", cfg.DatabaseURL)
	if err != nil {
		return fmt.Errorf("failed to open bootstrap database connection: %w", err)
	}
	defer db.Close()

	if err := db.PingContext(ctx); err != nil {
		return fmt.Errorf("failed to reach database for tenant bootstrap: %w", err)
	}

	// Serialize concurrent first boots so two instances cannot race on schema and
	// extension creation in the shared catalog.
	if err := WithAdvisoryLock(ctx, db, "pentagi-tenant-bootstrap", func(conn *sql.Conn) error {
		// QuoteIdentifier is belt-and-braces: ValidateTenantID already restricts
		// the character set, but this keeps the statement safe if that ever relaxes.
		if _, err := conn.ExecContext(ctx,
			"CREATE SCHEMA IF NOT EXISTS "+pq.QuoteIdentifier(schema),
		); err != nil {
			return fmt.Errorf("failed to create schema %q: %w", schema, err)
		}

		for _, ext := range requiredExtensions {
			if err := ensureSharedExtension(ctx, conn, ext, extSchema); err != nil {
				return err
			}
		}

		return nil
	}); err != nil {
		return err
	}

	// Rewrite the DSN once; every consumer reads cfg.DatabaseURL afterwards.
	return RewriteDatabaseURLForTenant(cfg)
}

// RewriteDatabaseURLForTenant appends the tenant search_path to cfg.DatabaseURL.

View on GitHub (pinned to ea665308ba)

Solutions

  1. Grant the role CREATE on the database once: GRANT CREATE ON DATABASE pentagi TO appuser; or pre-create the tenant schema as an admin.
  2. Confirm the DB is not in read-only mode (SHOW transaction_read_only;).
  3. Retry the boot — CREATE SCHEMA IF NOT EXISTS is idempotent and the advisory lock serializes concurrent runs.
  4. Check the wrapped pg error for the exact server reason (permission denied vs duplicate vs admin shutdown).

Example fix

// before (as appuser without DDL rights)
CREATE SCHEMA IF NOT EXISTS tenant_acme; -- ERROR: permission denied
// after (run once as superuser)
GRANT CREATE ON DATABASE pentagi TO appuser; -- or: CREATE SCHEMA tenant_acme AUTHORIZATION appuser;
Defensive patterns

Strategy: validation

Validate before calling

// preflight as the app role, before starting the app
psql "$DATABASE_URL" -c "CREATE SCHEMA IF NOT EXISTS tenant_check;" \
  && psql "$DATABASE_URL" -c "DROP SCHEMA tenant_check;"

Try / catch

if err := EnsureTenantSchema(ctx, cfg); err != nil {
    if strings.Contains(err.Error(), "permission denied") {
        log.Error().Msg("grant CREATE ON DATABASE to the app role or pre-create the tenant schema")
    }
    return err
}

Prevention

When it happens

Trigger: The connected role lacks CREATE privilege on the database, the connection was dropped mid-bootstrap (context canceled, server restart), or a syntax/serialization issue occurs during CREATE SCHEMA IF NOT EXISTS for the tenant schema.

Common situations: Deploying with a least-privilege DB user that can connect but cannot create schemas; running bootstrap while the DB is in read-only mode; connection pooler (pgbouncer) dropping the session mid-statement; context timeout expiring before DDL completes.

Related errors


AI-assisted analysis of vxcontrol/pentagi@ea665308ba (2026-09-01). Data as JSON: /api/errors/b3a23e0b07b0157b. Report an issue: GitHub.