{"record":{"id":"872b4996ebda17f8","repo":"vxcontrol/pentagi","slug":"failed-to-reach-database-for-tenant-bootstrap-w","errorCode":null,"errorMessage":"failed to reach database for tenant bootstrap: %w","messagePattern":"failed to reach database for tenant bootstrap: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"backend/pkg/database/tenant.go","lineNumber":49,"sourceCode":"func EnsureTenantSchema(ctx context.Context, cfg *config.Config) error {\n\tif !cfg.HasTenant() {\n\t\treturn nil\n\t}\n\n\tschema := cfg.SchemaName()\n\textSchema := cfg.ExtensionSchema()\n\n\t// Short-lived bootstrap connection on the ORIGINAL DSN. Opening it before the\n\t// search_path rewrite means CREATE EXTENSION resolves against the default\n\t// path and lands in the shared schema rather than the tenant's.\n\tdb, err := sql.Open(\"postgres\", cfg.DatabaseURL)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to open bootstrap database connection: %w\", err)\n\t}\n\tdefer db.Close()\n\n\tif err := db.PingContext(ctx); err != nil {\n\t\treturn fmt.Errorf(\"failed to reach database for tenant bootstrap: %w\", err)\n\t}\n\n\t// Serialize concurrent first boots so two instances cannot race on schema and\n\t// extension creation in the shared catalog.\n\tif err := WithAdvisoryLock(ctx, db, \"pentagi-tenant-bootstrap\", func(conn *sql.Conn) error {\n\t\t// QuoteIdentifier is belt-and-braces: ValidateTenantID already restricts\n\t\t// the character set, but this keeps the statement safe if that ever relaxes.\n\t\tif _, err := conn.ExecContext(ctx,\n\t\t\t\"CREATE SCHEMA IF NOT EXISTS \"+pq.QuoteIdentifier(schema),\n\t\t); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create schema %q: %w\", schema, err)\n\t\t}\n\n\t\tfor _, ext := range requiredExtensions {\n\t\t\tif err := ensureSharedExtension(ctx, conn, ext, extSchema); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}","sourceCodeStart":31,"sourceCodeEnd":67,"githubUrl":"https://github.com/vxcontrol/pentagi/blob/ea665308baaff015b226f308438a68d929d0f29b/backend/pkg/database/tenant.go#L31-L67","documentation":"EnsureTenantSchema opens a short-lived bootstrap connection on the original DATABASE_URL and pings it before doing any tenant bootstrap work. PostgreSQL's database/sql driver only parses the DSN at sql.Open time; the first real network contact happens at Ping, so this error wraps any connection failure — wrong host/port, DB down, bad credentials, TLS mismatch. It means the application cannot reach PostgreSQL at all and tenant schema setup cannot proceed.","triggerScenarios":"TENANT_ID is set and EnsureTenantSchema calls db.PingContext(ctx) on the original DSN and the ping fails: Postgres not running, wrong host/port in DATABASE_URL, wrong password, database does not exist, max_connections exhausted, or network/DNS failure between the container and the DB.","commonSituations":"Docker Compose where the postgres service is still starting (no healthcheck/wait-for), DATABASE_URL pointing at localhost inside a container instead of the service name, password containing characters that break URL parsing, pg_hba.conf rejecting the connection, or the DB container being on a different compose network.","solutions":["Verify PostgreSQL is running and reachable: docker compose ps / pg_isready -h <host> -p <port>.","Check DATABASE_URL syntax and credentials in .env (host, port, user, password, dbname); in containers use the compose service name, not localhost.","Retry after the DB is ready — first-boot races are common; add a healthcheck or depends_on condition.","Inspect the wrapped driver error in the message for the precise cause (connection refused vs auth failed vs unknown database)."],"exampleFix":"// before\ndb, _ := sql.Open(\"postgres\", \"postgres://user:pass@localhost:5432/pentagi\")\n// after\ndb, _ := sql.Open(\"postgres\", \"postgres://user:pass@postgres:5432/pentagi?sslmode=disable\") // service name + reachable creds","handlingStrategy":"try-catch","validationCode":"// before calling the library\nconn, err := net.DialTimeout(\"tcp\", \"postgres:5432\", 3*time.Second)\nif err != nil { return fmt.Errorf(\"postgres unreachable: %w\", err) }\n// and validate DSN parses:\nif _, err := url.Parse(cfg.DatabaseURL); err != nil { return err }","typeGuard":null,"tryCatchPattern":"if err := database.EnsureTenantSchema(ctx, cfg); err != nil {\n    var retryable bool\n    if strings.Contains(err.Error(), \"connection refused\") || errors.Is(ctx.Err(), context.DeadlineExceeded) {\n        retryable = true // wait and retry while DB container starts\n    }\n    log.Error().Err(err).Bool(\"retryable\", retryable).Msg(\"tenant bootstrap failed\")\n}","preventionTips":["Add a Docker healthcheck/depends_on condition for the postgres service","Never use localhost for the DB host inside containers","Percent-encode special characters in the DB password inside DATABASE_URL","Keep a startup retry loop with backoff for first boot"],"tags":["postgres","database","connectivity","multi-tenancy"],"backgroundTag":"database-connection-failed","analyzedSha":"ea665308baaff015b226f308438a68d929d0f29b","analyzedAt":"2026-09-01T14:16:31.421Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}