Case Study · Design & Development
GigCaravan puts a touring act's season on one board, with a shared catalog of venues and presenters and a private workspace per act. The pipeline only offers the next legal move from prospect to booked.
Touring acts run booking out of spreadsheets, inboxes, and memory. A festival deadline lives in one tab, the venue contact in another, and the actual status of the application in someone's head. The room an act plays and the presenter who books them are often different people. A spreadsheet treats them as the same row.
GigCaravan is our booking pipeline for that work: find the opportunity, save it, move it from prospect to booked, and keep the pursuit private to the act.
GigCaravan is a deployed, multi-tenant web app at app.gigcaravan.quest, with a public marketing site at gigcaravan.quest. An act signs up, creates a workspace, and browses a shared catalog of venues and presenters, seeded with 31 festivals to start. The act saves targets to a private list and tracks each engagement on a four-column board: Prospect, In Progress, Booked, Closed. It is in private beta, and early.
The harder engineering is tenancy and the pipeline. Catalog facts can be shared. An act's targets and applications cannot leak to another tenant, and neither can its strategy. Status cannot skip a step because someone posted the wrong string.
How it's built
The isolation and the state machine are underneath the board.
Every tenant-scoped query runs inside a transaction that sets the current tenant as a transaction-local GUC. Postgres RLS then filters the rows. The tenant comes from the authenticated session, never the request body. Session-level SET is forbidden: under connection pooling it would leak one tenant's context onto the next request. Isolation is proven at the connection layer, including the reused-pool case that used to raise instead of failing closed.
Venues, presenters, and contacts carry a nullable tenant_id. NULL means public catalog, written only by the platform seed. A set tenant_id means private to that act. Other tenants' private rows are invisible at the database. Python does not filter them after the fact. Search and browse go through that policy.
A venue hosts. A presenter books. They are different records, linked at the edition or engagement. The booking contact lives on the presenter. A festival is a presenter that runs a dated edition. That split lets a club sit in the same pipeline as a series or a festival.
Ten statuses from prospect to booked, with declined, withdrawn, and passed as terminals. The database CHECKs the set. The service layer owns the legal transitions. An illegal move is a 422. The board collapses those ten into four buckets so the season is readable.
The app is Next.js 16 on Vercel. The API is FastAPI on a Contabo box. It talks to Postgres over a single pooled DATABASE_URL. Cross-origin session cookies plus CSRF, CORS locked to the app origin. Moving the database is an env change.
Day and night palettes, Space Grotesk and IBM Plex Sans, a CONFIRMED stamp on booked gigs, and the four-column board as the primary screen. The look is part of the product.
Transaction-scoped Postgres RLS · dual catalog · venue/presenter split · 10-status machine · Next.js 16 + FastAPI · 31-festival seed.
What this shows
A lot of client work is multi-tenant SaaS: a pipeline of items, deadlines, statuses, and a hard wall between customers. Tenancy is in Postgres. The state machine is in the service layer. The frontend is a board an act can run a season on.
We designed and built GigCaravan: data model, API, auth, the Roadbook UI, and the deploy. Private beta today.
We design and build multi-tenant products, status machines, and the Postgres isolation they need, from one studio.