vxcontrol/pentagi · critical
search_path resolved to schema %q, expected %q — refusing to
Error message
search_path resolved to schema %q, expected %q — refusing to start so tenants do not silently share one dataset
What it means
This is the safety net for multi-tenancy: after rewriting the DSN, VerifySearchPath asserts current_schema() equals the tenant's schema name and refuses to start otherwise. The concern is a typo'd or ignored search_path silently routing a tenant onto public where all tenants would share one dataset. The error names both the resolved and expected schema so the misconfiguration is obvious.
Source
Thrown at backend/pkg/database/tenant.go:174
default:
return schema, nil
}
}
// VerifySearchPath asserts that connections really do resolve into the expected
// schema. A typo in the DSN would otherwise route a tenant silently onto public,
// where every tenant would share one dataset — a quiet, catastrophic failure.
func VerifySearchPath(ctx context.Context, db *sql.DB, cfg *config.Config) error {
if !cfg.HasTenant() {
return nil
}
var current sql.NullString
if err := db.QueryRowContext(ctx, "SELECT current_schema()").Scan(¤t); err != nil {
return fmt.Errorf("failed to resolve current schema: %w", err)
}
if current.String != cfg.SchemaName() {
return fmt.Errorf(
"search_path resolved to schema %q, expected %q — refusing to start so tenants "+
"do not silently share one dataset",
current.String, cfg.SchemaName(),
)
}
return nil
}
// RunMigrations applies pending migrations while holding an advisory lock, so
// that two instances booting simultaneously cannot execute the same migration
// set concurrently. Without a tenant the lock is still taken, which also fixes
// the pre-existing race between two single-instance deployments sharing a
// database.
func RunMigrations(ctx context.Context, db *sql.DB, cfg *config.Config, up func(*sql.DB) error) error {
return WithAdvisoryLock(ctx, db, "pentagi-migrations-"+cfg.SchemaName(), func(*sql.Conn) error {
return up(db)
})View on GitHub (pinned to ea665308ba)
Solutions
- Check the resolved vs expected schema in the message and align TENANT_ID with the intended schema name.
- If using a pooler, set DATABASE_SEARCH_PATH_VIA_OPTIONS=true so search_path survives pooling, or connect directly to Postgres.
- Run the bootstrap (EnsureTenantSchema) so the expected schema exists before VerifySearchPath runs.
- Verify manually: psql "$DATABASE_URL" -c 'SELECT current_schema();' and compare with the tenant schema.
Example fix
// before TENANT_ID=AcmeCorp # schema acmecorp expected, search_path resolves to public via pgbouncer // after # .env TENANT_ID=acmecorp DATABASE_SEARCH_PATH_VIA_OPTIONS=true
Defensive patterns
Strategy: validation
Validate before calling
// pre-start gate: the resolved schema must equal the expected tenant schema psql "$DATABASE_URL" -c "SELECT current_schema();" # compare with the schema derived from TENANT_ID before launching the app
Try / catch
if err := VerifySearchPath(ctx, db, cfg); err != nil {
// fail closed: never run with a wrong schema, tenants could share data
log.Fatal().Err(err).Str("expected", cfg.SchemaName()).Msg("search_path misrouting — refusing to start")
} Prevention
- Treat this error as fail-fast and never bypass it
- Set DATABASE_SEARCH_PATH_VIA_OPTIONS=true with transaction-mode poolers
- Use lowercase, consistent TENANT_ID values (schema names derive from them)
- Smoke-test multi-tenant isolation after every config change
When it happens
Trigger: cfg.SchemaName() (derived from TENANT_ID) differs from the schema current_schema() reports: TENANT_ID casing/typo, pooler stripping the search_path parameter so it defaults to public, the tenant schema was never created (bootstrap skipped), or DSN search_path overwritten by an options block.
Common situations: TENANT_ID=acme but data bootstrapped earlier as tenant_acme (naming-convention change); pgbouncer in transaction mode ignoring startup parameters; two compose stacks sharing a DB with different TENANT_IDs; forgetting DATABASE_SEARCH_PATH_VIA_OPTIONS with Supavisor.
Related errors
- extension %q is installed in schema %q, but multi-tenant mod
- failed to resolve current schema: %w
- failed to reach database for tenant bootstrap: %w
- failed to create schema %q: %w
- token validation disabled with default salt
AI-assisted analysis of vxcontrol/pentagi@ea665308ba (2026-09-01).
Data as JSON: /api/errors/256ba9af04f5d97b.
Report an issue: GitHub.