paperclipai/paperclip · critical · StartupRefusalError

database-contract-unmet

database-contract-unmet

Error message

authenticated public deployments require DATABASE_URL or config.database.connectionString; refusing embedded PostgreSQL fallback

What it means

assertCloudDatabaseContract enforces that authenticated public cloud deployments must have an explicit external PostgreSQL connection (DATABASE_URL env var or config.database.connectionString). If config.databaseUrl is missing at boot, it throws a StartupRefusalError with code database-contract-unmet, refusing to silently fall back to the embedded PostgreSQL (PGlite) database. This protects authenticated deployments from accidentally booting with a local, non-persistent database.

Source

Thrown at server/src/index.ts:334

    try {
      const parsed = new URL(connectionString);
      return parsed.protocol === "postgres:" || parsed.protocol === "postgresql:";
    } catch {
      return false;
    }
  }

  function assertCloudDatabaseContract(): void {
    if (config.deploymentMode !== "authenticated" || config.deploymentExposure !== "public") {
      return;
    }
    if (!config.databaseUrl) {
      // Under a managed-cloud supervisor a missing DATABASE_URL on boot
      // is the config-application race (the container can start before
      // the staged variables land), not operator error — the supervisor
      // restarts once the config holds. A malformed value below is a
      // real misconfiguration and stays an always-reported Error.
      throw new StartupRefusalError(
        "database-contract-unmet",
        "authenticated public deployments require DATABASE_URL or config.database.connectionString; refusing embedded PostgreSQL fallback",
      );
    }
    if (!isPostgresConnectionString(config.databaseUrl)) {
      throw new Error(
        "authenticated public deployments require DATABASE_URL to be a postgres/postgresql connection string",
      );
    }
  }

  const LOCAL_BOARD_USER_ID = "local-board";
  const LOCAL_BOARD_USER_EMAIL = "local@paperclip.local";
  const LOCAL_BOARD_USER_NAME = "Board";
  
  async function ensureLocalTrustedBoardPrincipal(db: any): Promise<void> {
    const now = new Date();
    const existingUser = await db

View on GitHub (pinned to 01ad858492)

Solutions

  1. Set DATABASE_URL (or config.database.connectionString) to a valid PostgreSQL connection string in the deployment environment/secret store and restart
  2. If this is a supervisor config race, verify staged variables and let the supervisor restart the container — the next boot with the variable present will pass
  3. Audit the deployment manifest (env/secret references) so DATABASE_URL is guaranteed present for authenticated public deployments
  4. Confirm the deployment mode is actually authenticated-public; dev/local deployments intentionally allow the embedded fallback by leaving DATABASE_URL unset

Example fix

// before (deployment env)
# DATABASE_URL not set -> StartupRefusalError: database-contract-unmet
// after
DATABASE_URL=postgres://user:pass@db-host:5432/paperclip
# or in config
database: { connectionString: 'postgres://user:pass@db-host:5432/paperclip' }
Defensive patterns

Strategy: validation

Validate before calling

// at deploy time / pre-boot check
if (process.env.DEPLOYMENT_MODE === 'authenticated-public' && !process.env.DATABASE_URL && !config.database?.connectionString) { throw new Error('authenticated public deployment missing DATABASE_URL'); }

Type guard

null

Try / catch

try { await startServerWithDatabaseTeardown(); } catch (e) { if (e instanceof StartupRefusalError && e.code === 'database-contract-unmet') { // surface to operator: missing DATABASE_URL; do not retry with embedded fallback } else throw e; }

Prevention

When it happens

Trigger: startServerWithDatabaseTelemetry/boot runs in authenticated-public deployment mode where config.databaseUrl is falsy — DATABASE_URL env var unset AND config.database.connectionString unset. Comment in source notes this can transiently happen under a managed-cloud supervisor when the container starts before staged config variables land.

Common situations: Kubernetes/cloud supervisor race where the pod starts before DATABASE_URL is injected; deployment manifest missing the env var; secret not mounted; staging config not yet applied; forgetting the connection string when promoting a dev (embedded) configuration to production.

Understand the failure class

Background: "environment variable is not set" and "Missing keys in environment" errors: what missing required env var messages mean and how to fix them — this error's family across 28 libraries.

Related errors


AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10). Data as JSON: /api/errors/e8332dc9ced17bbd. Report an issue: GitHub.