Ibrahim Shahid
← Projects

Al Madina Silent Art Auction

Al Madina School of Richmond · Nov 2025 – Mar 2026

A full-stack auction system built for a real fundraiser at Al Madina School of Richmond — live bidding with PostgreSQL row-level locking, automated auction lifecycle jobs, pgvector similarity search, and a complete admin operations dashboard.

0

race conditions in production bidding

100%

type safety, DB to UI

1024-dim

image embeddings for discovery

Problem

Al Madina School of Richmond runs a silent art auction as a real fundraiser — live bidders, real money, a hard event deadline. Off-the-shelf auction tools couldn't handle the combination they needed: anonymous public bidding, anti-sniping, Stripe payments, post-auction sales for unsold pieces, and pickup scheduling, all manageable by non-technical staff.

The hard part isn't the CRUD around artworks. It's correctness under concurrency: two people bidding on the same piece in the same second must never corrupt state, accept an invalid amount, or double-assign a winner.

Architecture

System architecturelive
Turborepo monorepo · Bun

Next.js 16 web

React 19 + React Compiler

Hono API server

oRPC · end-to-end types

type-safe RPC · Zod on every boundary

PostgreSQL

Drizzle ORM · row-level locking · pgvector

Cloudflare R2

artwork images · avatars

webhooks + cron (bearer-secured)

Stripe

Payment Intents · receipts

Lifecycle jobs

status sweep · winner sweep · self-healing

Replicate ImageBind

1024-dim embeddings on upload

Technical Decisions

Row-level locking for bids

Every bid runs inside a transaction that takes SELECT … FOR UPDATE on the artwork row, making each artwork a serialization point. Two simultaneous bids are forced into a strict order: the second one re-reads the post-commit state and is validated against the actual current high bid. This eliminates double-spend and invalid-amount races at the database layer instead of hoping application logic catches them.

oRPC + Drizzle for end-to-end type safety

A change to an API input type propagates through server, client, and UI with compile-time errors — no manually maintained API types, no drift. On a solo project with a hard deadline, this is a velocity decision as much as a safety one: entire classes of runtime validation bugs simply can't ship.

pgvector inside Postgres, not a separate vector DB

Artwork uploads generate a 1024-dimensional ImageBind embedding via Replicate, stored in the same Postgres database that holds the auction data. 'Similar artworks' is one cosine-distance query joined against normal relational filters. For a catalog of hundreds of pieces, a dedicated vector database would have added an operational dependency for zero benefit.

Cron jobs over real-time schedulers

Auction lifecycle (scheduled → live → closed, winner assignment) runs on idempotent cron sweeps that log to a job_runs audit table, including a self-healing pass that catches zombie auctions. Idempotent sweeps are boring and recoverable: if a run fails, the next one fixes it. An in-process timer that fires exactly once is the opposite.

Privacy enforced at the data layer

Public users see anonymized bids ('Bidder #42'); admins see full identity. The API shapes the response per procedure tier (public / protected / admin) so the anonymity guarantee doesn't depend on the UI remembering to hide a field.

Challenges

Anti-sniping that can't be gamed

Last-second sniping ruins silent auctions. Bids inside a configurable window (default 90 seconds) extend the auction end time — but extensions had to be monotonic, only ever moving the end time forward. The subtle bug class here is two near-simultaneous late bids computing extensions from different observed end times; because extension happens inside the same row-locked transaction as the bid itself, the second bid always sees the first one's extension.

Payments meeting the real world

Stripe webhooks arrive out of order, retry, and occasionally duplicate. The fulfillment handler verifies signatures, treats events idempotently, and marks artworks paid with receipt URLs only on confirmed payment. Winner-only payment pages enforce access control server-side — a URL leak can't let someone else pay for (or claim) a piece.

An admin suite for non-engineers

The school staff running the event aren't developers. The admin dashboard had to cover the entire operation — live revenue stats, artwork scheduling, drag-and-drop image ordering, bidder management, payment tracking, pickup appointments with enforced business rules (Mon–Thu, 9–11 AM, paid winners only). Building operational tooling people actually use ended up being as much work as the auction engine, and as important.

Profile queries without N+1

Bidder profiles aggregate active bids with live winning/outbid status, watchlists, wins, payment state, and appointments. Done naively this is a dozen queries per profile. CTEs and LATERAL joins collapse it into a small number of round trips, which kept profile loads fast even during the event-night traffic spike.

Stack

Next.js 16React 19HonooRPCDrizzlePostgreSQLpgvectorStripeBunTurborepoDockerCloudflare R2