Full-stack prize competition platform for a UK automotive business — where every draw is transparent, verifiable, and cryptographically secure.

The UK prize competition market is crowded — and plagued by scepticism. Entrants routinely question whether draws are legitimate, and operators struggle to prove they are. For a new entrant like Procter Motor, launching into this space without a clear answer to “how do I know this is fair?” was a non-starter.
Procter Motor came to us with a clear objective: build a competition platform for high-value automotive giveaways that people could actually trust. Not through branding or reassurance copy — through verifiable proof.
The platform needed to:
Off-the-shelf solutions couldn't deliver on the core requirement. They rely on basic random number generation that can't be audited, lack the concurrency controls needed for popular competitions, and treat regulatory compliance as an afterthought. A custom build was the only path.
Trust was the central design constraint, so we started with the draw engine.
The winner selection system is built directly into the database layer using PostgreSQL's pgcrypto extension — the same cryptographic library used to generate SSL/TLS certificates. The draw logic never touches application code, eliminating any possibility of manipulation at the API layer.
When an admin triggers a draw, four things happen in sequence:
Cryptographic seed
A 256-bit cryptographic seed is generated using hardware-backed random number generation sourced from the server's entropy pool.
SHA-256 hash
The seed is combined with the competition ID and a precise timestamp, then hashed using SHA-256 — the algorithm that underpins the Bitcoin network.
Winner selection
The hash is converted to an index that maps to a specific ticket in the pool. The winning ticket is selected.
Proof stored permanently
The complete cryptographic proof — public seed, SHA-256 hash, verification hash, winning index, and algorithm identifier — is stored permanently alongside the result.
A public verification endpoint allows anyone to independently recalculate and confirm the result. No login required. Enter the competition ID, and the system re-derives the hash from the stored seed and metadata, confirms it matches the original, and proves the winner was selected correctly.
No trust required — just maths.
256-bit hardware entropy
Combined with competition ID + timestamp
Hash mapped to ticket index
Seed, hash, index — all persisted
Anyone can re-derive the hash and confirm the result — no login required
The entire draw process runs inside the database — the application layer never touches it
Popular competitions can see hundreds of users purchasing tickets simultaneously. A single duplicate ticket number or a failed allocation under load would undermine the fairness the draw system is designed to protect.
The platform pre-generates a complete ticket pool for each competition at creation time. When a user purchases tickets, the system uses PostgreSQL's row-level locking to atomically claim available tickets from the pool. Each allocation is isolated — concurrent transactions skip locked rows and claim the next available tickets, guaranteeing uniqueness even under heavy load.
If any part of a transaction fails, the entire allocation rolls back cleanly. No duplicate ticket numbers. No gaps in the pool. No race conditions.
Concurrent transactions skip locked rows — no duplicates, no race conditions
The checkout flow was designed around a single principle: no user should ever be left in an ambiguous state. A failed payment should never result in allocated tickets lingering in limbo, and a successful payment should never leave a user without their ticket numbers.
The system achieves this through a staged transaction model:
Pre-allocation
Tickets are reserved from the pool and an order is created in a pending state before the user enters payment details.
Payment processing
Stripe handles the transaction with full PCI compliance.
Confirmation
On successful payment, the order is marked complete, tickets are activated, and a confirmation email is dispatched.
Rollback
On failure, the pending order is cancelled and tickets are released back into the available pool automatically.
This guarantees the ticket pool remains consistent at all times, regardless of what happens during checkout.
No user is ever charged without tickets — no tickets are ever stranded without payment.
Reserve tickets, create pending order
Stripe processes transaction
Order complete, tickets activated
Order cancelled, tickets released
UK Gambling Commission regulations require prize competitions to include a skill element to distinguish them from lotteries. This isn't optional — it's a legal requirement.
The platform enforces this at the API level. Each competition can be configured with a skill-based question that participants must answer correctly before they can proceed to checkout. The validation happens server-side — a user cannot create a payment intent without a verified answer on record.
All answers are stored and auditable, giving the operator a clear compliance trail without adding noticeable friction to the purchase flow.
Compliance enforced at the API level — not through policy documents.
Customer communication spans the full competition lifecycle, handled by six email templates built as React components and rendered server-side:
The draw reminder system runs as a Deno-based edge function on a cron schedule, identifying upcoming competitions, grouping ticket holders by user, and dispatching personalised emails.
Swipe for more →
Next.js 16 / React 19 / TypeScript / Tailwind CSS / Radix UI
Next.js API Routes / React Server Components
Supabase (PostgreSQL + pgcrypto + Row-Level Security)
Supabase Auth (JWT-based)
Stripe Payment Elements + Webhooks
Resend API + React Email
Supabase Storage (S3-backed CDN)
Supabase Edge Functions (Deno runtime)
Vercel (Edge Network)
The winner selection function runs as a PostgreSQL stored procedure using pgcrypto. The cryptographic operations happen at the database level, inside a transaction, with no exposure to the application layer. Even if the API were compromised, the draw logic and its randomness source remain untouched.
Every ticket for a competition is created at launch time rather than generated during purchase. This eliminates numbering conflicts, simplifies concurrency, and means the pool is always the single source of truth for availability and sales progress.
Rather than a simple pay-then-allocate flow, the system pre-reserves tickets before payment begins and rolls back atomically on failure. No user is ever charged without tickets, and no tickets are ever stranded without payment.
Competition stats — tickets sold, percentage filled, available tickets — are pre-computed in a PostgreSQL materialized view. This avoids expensive aggregation queries on every page load and keeps the frontend responsive even as ticket volumes grow.
SHA-256 cryptographic proof with public verification endpoint
Atomic allocation with row-level locking under load
Pre-allocate → pay → confirm/rollback cycle via Stripe
Skill question enforcement at API level
Parallel competitions with independent ticket pools and draw dates
Live sales stats via PostgreSQL materialized views
Competition management, one-click draws, winner management, CSV exports
Purchase tickets across competitions in a single Stripe checkout
Six templates covering the full competition lifecycle — purchase to draw result
Image uploads via Supabase Storage with edge delivery
Responsive design built with Radix UI primitives and Tailwind CSS
Row-level security policies enforced on every table
Whether you need cryptographic security, high-concurrency systems, or regulatory compliance built in from day one — we'd love to talk.
Get in Touch →