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 dbView on GitHub (pinned to 01ad858492)
Solutions
- Set DATABASE_URL (or config.database.connectionString) to a valid PostgreSQL connection string in the deployment environment/secret store and restart
- 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
- Audit the deployment manifest (env/secret references) so DATABASE_URL is guaranteed present for authenticated public deployments
- 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
- Set DATABASE_URL in the deployment manifest/secret store for every authenticated public environment
- Verify env/secret mounts in CI before rollout
- Understand dev allows embedded PGlite but authenticated public deployments intentionally refuse it
- Allow the supervisor to restart once staged config lands — do not weaken the refusal
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.
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- ${label} has existing tables but no migration journal. Run m
- PAPERCLIP_OPENCODE_RUNTIME_DIR is required
- Unknown config key ${warning.path}; did you mean ${warning.s
- Unknown config key ${warning.path}; did you mean ${warning.s
- "configJson" is required and must be an object
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10).
Data as JSON: /api/errors/e8332dc9ced17bbd.
Report an issue: GitHub.