From 95525a02595a3f0cd2a27b23ace8bca66c86d0b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Tue, 3 Nov 2026 20:32:53 +0100 Subject: [PATCH] fix(jobs): let pg-boss create its schema only when it is missing Postgres checks the database's CREATE privilege before it checks whether the schema exists, so CREATE SCHEMA IF NOT EXISTS fails for a role that may not create schemas even when the schema is there. The worker now asks first and passes createSchema: false when it exists. --- src/lib/jobs/boss-instance.ts | 18 ++++++++++++++++-- src/lib/jobs/reminder-worker.ts | 9 ++++++++- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/lib/jobs/boss-instance.ts b/src/lib/jobs/boss-instance.ts index ef3fa2121..169021e24 100644 --- a/src/lib/jobs/boss-instance.ts +++ b/src/lib/jobs/boss-instance.ts @@ -88,8 +88,22 @@ function databaseOptions( } as const; } -export function createWorkerBoss(connectionString: string): PgBoss { - return new PgBoss(databaseOptions(connectionString, "worker")); +/** + * The worker's pg-boss. `createSchema` says whether pg-boss's first install + * may run `CREATE SCHEMA IF NOT EXISTS pgboss`. Postgres checks the database's + * CREATE privilege before it checks whether the schema exists, so under the + * app's restricted role (`scripts/db-app-role.mjs`), which may not create + * schemas, that statement fails even though the boot already created the + * schema for it. The caller passes false when the schema is there. + */ +export function createWorkerBoss( + connectionString: string, + options: { createSchema?: boolean } = {}, +): PgBoss { + return new PgBoss({ + ...databaseOptions(connectionString, "worker"), + ...(options.createSchema === false ? { createSchema: false } : {}), + }); } function producerOptions(connectionString: string) { diff --git a/src/lib/jobs/reminder-worker.ts b/src/lib/jobs/reminder-worker.ts index 715ce80b2..72991a313 100644 --- a/src/lib/jobs/reminder-worker.ts +++ b/src/lib/jobs/reminder-worker.ts @@ -11,6 +11,7 @@ * Usage: Run as a standalone process or call startReminderWorker() from a * custom server setup. In dev, use: npx tsx src/lib/jobs/reminder-worker.ts */ +import { prisma } from "@/lib/db"; import { markWorkerStarted, recordError } from "@/lib/jobs/worker-status"; import { createWorkerBoss, setGlobalBoss } from "@/lib/jobs/boss-instance"; import { reportJobsCutOffAtBoot } from "@/lib/jobs/job-observer"; @@ -47,7 +48,13 @@ export async function startReminderWorker() { return; } - const boss = createWorkerBoss(DATABASE_URL); + // The queue schema exists once a worker has started, and on every boot + // the restricted database role runs under (the entrypoint creates it for + // that role); only a fresh database without either needs pg-boss to + // create it. + const [{ present }] = await prisma.$queryRaw<{ present: boolean }[]>` + SELECT to_regnamespace('pgboss') IS NOT NULL AS present`; + const boss = createWorkerBoss(DATABASE_URL, { createSchema: !present }); boss.on("error", (error: unknown) => { workerLog("error", "boss emitted error", error);