From 1e417eccb11931f28137bbbda183b2aa12794ee6 Mon Sep 17 00:00:00 2001 From: "groombook-ci[bot]" Date: Sun, 29 Mar 2026 00:37:38 +0000 Subject: [PATCH] fix(api): add FOR UPDATE lock to super user claim transaction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CRITICAL race condition: two concurrent POST /api/setup requests could both read "no super user exists" before either acquired a lock, allowing two super users to be created. Added .for("update") to the staff SELECT query inside the transaction. PostgreSQL FOR UPDATE serializes concurrent claims — the second transaction blocks on the lock until the first commits, then sees the existing super user and returns 409. Co-Authored-By: Paperclip --- apps/api/src/routes/setup.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/api/src/routes/setup.ts b/apps/api/src/routes/setup.ts index 1cc56c6..c299afa 100644 --- a/apps/api/src/routes/setup.ts +++ b/apps/api/src/routes/setup.ts @@ -38,11 +38,13 @@ setupRouter.post("/", zValidator("json", setupSchema), async (c) => { .from(businessSettings) .limit(1); - // Check if any super user already exists (race condition guard) + // Lock super user rows to prevent concurrent claims + // FOR UPDATE serializes concurrent claims: second transaction blocks until first commits const [existingSuperUser] = await tx .select({ id: staff.id }) .from(staff) .where(eq(staff.isSuperUser, true)) + .for("update") .limit(1); if (existingSuperUser) {