We built two things on one database: the public website, and a CRM at /admin that the venue is actually run from. Twenty-one screens, grouped the way the work happens rather than the way the tables are shaped.
Pipeline. Leads land from the website, the chatbot, walk-ins, referrals and social, and move through New, Contacted, Proposal, Won and Lost. The same six leads render either as a table or as a drag-and-drop board, because reviewing a list and working a pipeline are different jobs. A won lead becomes a booking; bookings and site tours share one calendar, colour-coded so a pending hold, a confirmed event and a tour that nobody has phoned back are distinguishable at a glance.
Ticketing, and the part with money in it. Public events sell through Stripe Checkout. Three properties are worth naming, because each one is a real failure mode we designed against rather than a feature:
- Only a signature-verified webhook mints a ticket. The handler reads the raw request bytes — parsing the JSON first reformats them and every signature check then fails, which is exactly the pressure that tempts someone into skipping verification. Without that check, anyone who learns the URL can post a fake paid event and print free tickets.
- Idempotency is a unique-constraint violation, not a lookup. Stripe retries until it gets a 2xx. Checking whether an event was already handled and then inserting leaves a window where two retries both mint a ticket. Letting the database reject the duplicate closes it.
- The last seat cannot be sold twice. A pending order holds no seat, so two buyers can both reach Stripe for the final ticket. The fulfilment transaction retakes a row lock and refunds the loser automatically rather than overselling the room. Choosing to refund a real customer is a worse outcome than a clean sale and a better one than a family arriving at a full hall.
Staff sell at the counter through the same records, so cash and card reconcile against one set of takings. At the door, a receipt code — typed or scanned — checks a guest in.
An AI chatbot the owner controls. The website chatbot answers from a plain-text brand knowledge field the owner edits in Settings. No prompt engineering, no redeploy: whoever runs the venue writes what is true, and the assistant says that. It also reads the venue's real availability rather than guessing at dates.
Content, without a developer. The headline numbers, catering menu, gallery albums, blog, site images and the notification address for enquiries all live in the CRM. Leave a number blank and the website hides that stat rather than printing a zero — the kind of small decision that stops a half-filled admin panel from embarrassing the business in public.
A help centre, written for staff. Thirty topics covering every screen in the menu, in the language of someone running a front desk rather than a developer. It also states plainly where the CRM cannot yet do something, so nobody discovers that with a customer standing in front of them.
The whole system is self-hosted on its own server: Docker, PostgreSQL, automatic TLS, nightly encrypted off-box backups.