diff --git a/src/server/db.ts b/src/server/db.ts index 1cc1a5c..c3ede2b 100644 --- a/src/server/db.ts +++ b/src/server/db.ts @@ -406,7 +406,7 @@ export function dbForTenant(tenantId: string) { args, (a) => delegate[operation](a), ); - }); + }, TX_DEFAULTS); }, }, }, @@ -415,6 +415,12 @@ export function dbForTenant(tenantId: string) { export type TenantDb = ReturnType; +/** + * Interactive transaction limits. Prisma defaults (maxWait 2 s, timeout 5 s) fail under parallel load + * (pool exhausted while notifications/mails run inside a transaction) → wider but still bounded. + */ +const TX_DEFAULTS = { maxWait: 10_000, timeout: 20_000 } as const; + /** * Run `fn` atomically for one tenant. ALWAYS use this (or `inTransaction` in services) * instead of `db.$transaction(...)` for multi-step writes — atomic in both modes: @@ -431,7 +437,7 @@ export async function tenantTransaction( ): Promise { if (!RLS_ENFORCED) { if (isTransactionClient(db)) return fn(db); - return db.$transaction((tx) => fn(tx as unknown as TenantDb), options) as Promise; + return db.$transaction((tx) => fn(tx as unknown as TenantDb), { ...TX_DEFAULTS, ...options }) as Promise; } const active = tenantTx.getStore(); if (active) { @@ -441,7 +447,7 @@ export async function tenantTransaction( return appBase!.$transaction(async (tx) => { await tx.$executeRaw`SELECT set_config('app.tenant_id', ${tenantId}, true)`; return tenantTx.run({ tenantId, tx }, () => fn(db)); - }, options); + }, { ...TX_DEFAULTS, ...options }); } /** Interactive transaction clients have no $transaction method. */