
UPDATE ... WHERE ticketsIssuedCount + qty <= capacity inside a single Postgres statement, no SELECT-read-then-write, no SELECT-FOR-UPDATE deadlock window, no overticketing. See ticketing.ts L36-47.idempotencyKey on every reserve call, backed by Reservation.idempotencyKey UNIQUE — double-clicks, 4G retries and retry storms never double-charge. See schema.prisma L107.(userId, eventId, ticketIndex) @@unique on Ticket + background reconciler. Tiers 1 and 2 stop the known races; tier 3 stops refactor bugs; tier 4 heals manual-DB-write drift. See schema.prisma L134.cleanupExpiredReservations atomically reverts sold count + marks reservation EXPIRED + appends audit log. See ticketing.ts L49-50 and L164-189.TICKET_SECRET in qr.ts. Forgery fails offline in the door scanner before any DB lookup; the signature check is cheap and runs in the browser worker.TicketTransfer model with unique transferCode, pending/completed/cancelled status, receiver email, expiry window and claim flow. See schema.prisma L137-148 and the transfer + claim routes.USER, ORGANIZER, ADMIN, SUPER_ADMIN. Every mutating route re-checks the role from the httpOnly session cookie; ORGANIZER-owned resources additionally check organizerId === session.userId. See schema.prisma L14-19.AuditLog rows are created inside the same $transaction as the mutation they record; no UPDATE or DELETE on AuditLog is exposed anywhere in application code. See schema.prisma L150-159.Message model scoped by eventId, with read flags, unread-count endpoint, and per-conversation pages on both dashboard and organizer sides. See schema.prisma L161-173.verifyTicketToken locally before POST-ing; the /api/organizer/checkin endpoint re-verifies the signature and re-checks ORGANIZER ownership, then writes Ticket.status = USED + validatedAt + validatedBy. See qr.ts L19-32 and checkin/route.ts.ticketsIssuedCount, writes corrective UPDATE atomically, and appends a CAPACITY_RECONCILED AuditLog row with old/new values and reason. See ticketing.ts L216-266.DRAFT with approvedByAdmin = false; the admin dashboard's DeploymentRequests queue lists them with Approve / Reject buttons. Approve flips both flags and publishes; Reject closes the event. Both actions write AuditLog. See deployment-requests.tsx and action/route.ts.postinstall runs prisma generate, which reads DATABASE_URL from prisma.config.ts via env("DATABASE_URL") — the variable must exist in the shell that runs npm install, or prisma generate is skipped.UPDATE ... WHERE semantics, AuditLog.metadata is Json type, and the schema's datasource is provider = "postgresql".ticketing.ts. Never holds signing material in JS (httpOnly cookie). ticketing.ts (src/services/) Every mutation Only actual writer to Reservation/Ticket/counters. Wraps CAS + idempotency + inserts in one $transaction. PostgreSQL Every reservation / confirm / scan / expire UNIQUE indices are schema-level (idempotencyKey, ticketIndex pairs) — final enforcer, blocks duplicates even if app code is wrong. Writes AuditLog append-only. TTL reconciler (cleanupExpiredReservations → reconcileCapacity) After every expiry sweep + 5-min cron Reverts sold count for expired holds, detects drift, writes CAPACITY_RECONCILED AuditLog. Never issues new tickets. Browser door scanner Every entry Verifies QR ticket signature offline before POST-ing; cannot write directly. Writes only via /api/organizer/checkin with ORGANIZER ownership re-check.UPDATE ... WHERE ticketsIssuedCount + qty <= capacity statement; Postgres runs them serially and exactly one of them gets 1 row affected (or 0 if capacity is exhausted). There is no SELECT-FOR-UPDATE deadlock window because there is no separate read — the read is the predicate, inside the write. See ticketing.ts L36-47.idempotencyKey UNIQUE protects one buyer retrying the same reserve on flaky 4G: same key = same reservation returned, no new write. It does not protect against a code refactor that calls ticket.create twice inside confirm with different keys. That gap is what Ticket @@unique([userId, eventId, ticketIndex]) catches. Two different mechanisms, two different failure modes. See schema.prisma Ticket @@unique.event.ticketsIssuedCount is authoritative, not a derived COUNT(*), which means drift is possible. A DB admin writing a manual fix, a mid-sale admin action on the event row, or any future code path that issues a ticket without bumping the counter — all of them leave the counter wrong. The reconciler sums the real sources of truth (reservations + tickets), compares, and writes the correct value back with an AuditLog entry recording the delta and reason. It's not a sign of failure; it's the system fixing itself.TICKET_SECRET from the session's JWT_SECRET. Rotate one and the other is unaffected. Rotate TICKET_SECRET and every old unscanned QR token is immediately invalid. Operational playbook during rotation: sign new tokens with both secrets for an overlap window, verify against both in verifyTicketToken, then drop the old secret once every pending ticket has either been scanned or expired..sample.env to .env.local (or your deploy platform's env UI). Fill:DATABASE_URL + DIRECT_URL (from your Postgres provider)JWT_SECRET: generate with openssl rand -hex 32 (256-bit)TICKET_SECRET: generate with openssl rand -hex 32 (256-bit, different from JWT_SECRET)SESSION_COOKIE_NAME: unique per deploy so localhost/staging/prod cookies don't clobber each othernpx prisma migrate deploy — NOT migrate dev in production. migrate deploy runs the applied migrations only and never touches the shadow database or the Prisma client.ENABLE_SEEDING=true. Never on production after go-live: the seed script is built for local demo data and truncates if enabled.eventId, with idempotency keys riding the message key.ENABLE_SEEDING: Never enable in production. Demo seeding deletes existing user-facing data or duplicates it.JWT_SECRET / TICKET_SECRET leak: attacker can forge SUPER_ADMIN sessions and forge valid door-entry QR tickets. Rotate immediately, and for QR tokens write a dual-sign overlap playbook before you need it.approvedByAdmin = false — use it.Posted Sep 23, 2026
Developed a ticketing platform with zero double-bookings using Next.js, Prisma, and PostgreSQL.