{"record":{"id":"256ba9af04f5d97b","repo":"vxcontrol/pentagi","slug":"search-path-resolved-to-schema-q-expected-q-r","errorCode":null,"errorMessage":"search_path resolved to schema %q, expected %q — refusing to start so tenants do not silently share one dataset","messagePattern":"search_path resolved to schema %q, expected %q — refusing to start so tenants do not silently share one dataset","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"backend/pkg/database/tenant.go","lineNumber":174,"sourceCode":"\tdefault:\n\t\treturn schema, nil\n\t}\n}\n\n// VerifySearchPath asserts that connections really do resolve into the expected\n// schema. A typo in the DSN would otherwise route a tenant silently onto public,\n// where every tenant would share one dataset — a quiet, catastrophic failure.\nfunc VerifySearchPath(ctx context.Context, db *sql.DB, cfg *config.Config) error {\n\tif !cfg.HasTenant() {\n\t\treturn nil\n\t}\n\n\tvar current sql.NullString\n\tif err := db.QueryRowContext(ctx, \"SELECT current_schema()\").Scan(&current); err != nil {\n\t\treturn fmt.Errorf(\"failed to resolve current schema: %w\", err)\n\t}\n\tif current.String != cfg.SchemaName() {\n\t\treturn fmt.Errorf(\n\t\t\t\"search_path resolved to schema %q, expected %q — refusing to start so tenants \"+\n\t\t\t\t\"do not silently share one dataset\",\n\t\t\tcurrent.String, cfg.SchemaName(),\n\t\t)\n\t}\n\n\treturn nil\n}\n\n// RunMigrations applies pending migrations while holding an advisory lock, so\n// that two instances booting simultaneously cannot execute the same migration\n// set concurrently. Without a tenant the lock is still taken, which also fixes\n// the pre-existing race between two single-instance deployments sharing a\n// database.\nfunc RunMigrations(ctx context.Context, db *sql.DB, cfg *config.Config, up func(*sql.DB) error) error {\n\treturn WithAdvisoryLock(ctx, db, \"pentagi-migrations-\"+cfg.SchemaName(), func(*sql.Conn) error {\n\t\treturn up(db)\n\t})","sourceCodeStart":156,"sourceCodeEnd":192,"githubUrl":"https://github.com/vxcontrol/pentagi/blob/ea665308baaff015b226f308438a68d929d0f29b/backend/pkg/database/tenant.go#L156-L192","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nTENANT_ID=AcmeCorp   # schema acmecorp expected, search_path resolves to public via pgbouncer\n// after\n# .env\nTENANT_ID=acmecorp\nDATABASE_SEARCH_PATH_VIA_OPTIONS=true","handlingStrategy":"validation","validationCode":"// pre-start gate: the resolved schema must equal the expected tenant schema\npsql \"$DATABASE_URL\" -c \"SELECT current_schema();\"\n# compare with the schema derived from TENANT_ID before launching the app","typeGuard":null,"tryCatchPattern":"if err := VerifySearchPath(ctx, db, cfg); err != nil {\n    // fail closed: never run with a wrong schema, tenants could share data\n    log.Fatal().Err(err).Str(\"expected\", cfg.SchemaName()).Msg(\"search_path misrouting — refusing to start\")\n}","preventionTips":["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"],"tags":["postgres","search-path","multi-tenancy","configuration","data-isolation"],"backgroundTag":"search-path-mismatch","analyzedSha":"ea665308baaff015b226f308438a68d929d0f29b","analyzedAt":"2026-09-01T14:16:31.421Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}