Ten things I designed, built and shipped into live service — apps, payment platforms, agents, guest-facing products, and the databases underneath them. Every one is running now.
Projects
Ten
Status
All in service
Earliest
Jan 2024
Latest
Aug 2026
Live since Aug 2026Systems & product
QR order-and-pay platform
At-table ordering and card payment across 43 locations — 20 inside tables, 18 outside, 5 letting rooms. Guest scans, orders and pays; the order lands on a bar tablet in realtime. Runs on its own subdomain, independent of the venue EPOS.
Schema from scratch — orders, items, catalogue, locations, settings, refunds, audit — with all 43 locations seeded against unguessable tokens.
Stripe live payments with webhook reconciliation across four event types, and three Edge Functions covering order creation, webhook handling and refunds.
Realtime bar screen: per-order timers, a chime at two minutes escalating at four and six to a full-screen alarm, accept and delivered actions, refuse-with-reason, live sold-out toggle. Refuse-and-refund proven end to end in live service.
Food ordering with allergen data enforced at schema level, so no dish can go live without allergen text.
Admin console for pricing, menu toggles and service hours, plus a QR print module generating error-correction-level-H cards.
What went wrong
Closed a token-enumeration vulnerability before launch by replacing direct table reads with a security-definer lookup returning at most one row.
Diagnosed a gateway-level CORS rejection blocking every guest request before the function ever ran.
Traced a payment failure to a mangled key value rather than an account problem, by reading the raw error instead of the dashboard.
Schematics
Timer bands — chime at 2:00, escalating at 4:00 and 6:00
Live since Aug 2026Five rooms, five URLsSystems & product
In-room digital guest guides
A QR card in each letting room opens a mobile guide for that specific room — wi-fi, arrival and departure, an order button for food and drink, local information and the history of the building. One continuous scroll, no app, no login. Built as a product: the venue is configuration, not code.
One code per room — the guide already knows which room the guest is standing in
Every comparable product sells one guide per property. The guest arrives, opens it, and has to work out which bits apply to them.
One code per room instead. The room type, the arrival instructions and the ordering link are already correct when the page loads, because the code the guest scanned was printed for that room. There is nothing to choose and nothing to get wrong — and it costs nothing extra, because the room is a five-line config object.
What I built
Five self-contained mobile guides, one per letting room, covering wi-fi, arrival and departure, the room itself, food and drink, safety, house rules, getting there and the history of the building — on one continuous scroll with a jump menu, no app and no login.
An ordering button inside the guide, wired to the QR order-and-pay platform, so a guest can send food and drink to the room from the same page that tells them the wi-fi password.
A rendering engine with sixteen block types driven entirely by a content array, and a design system inlined in every file — no build step and no dependencies beyond two web fonts.
Print-ready QR cards generated in Python, two per A4 page with trim lines, at error-correction level H so a scuffed card in a bedroom still scans. Every code was machine-decoded from the finished PDF and verified to resolve before printing.
Custom subdomain with SSL, pointed at the guides without delegating the main domain — the group website stays where it is.
Decisions in the detail
Cards print black on white rather than on the site’s cream — office printers band solid pale fills, and a banded QR is a QR that sometimes fails.
The fire notice on the back of the door is pointed to, not duplicated. Two versions of a safety notice is one version too many, and the door is the one that gets checked.
The guide names a nearby shop and a deli but deliberately omits the competing food business next door — a guest guide that sends people elsewhere for dinner is working against the kitchen it sits above.
The room guides carry the building’s own history, taken from notices the venue already had and had no way to put in front of anyone.
Built as a product, not a one-off
The engine is content-agnostic. A second venue is a new config object and a new content array — delivery is content entry rather than development.
Deployed at one site as the reference build, with a credit line at the foot of every guest’s screen acting as the distribution channel for the next one.
LiveSystems & product
Automated call routing
Inbound phone system replacing manual call handling. A keypad menu routes callers to three departments with independent answer detection; no-answer calls fall through to voicemail with a transcription emailed out.
The published flow — three departments, each with its own answer detection
Twilio StudioFunctionsTwiMLSendGridOpenClose
What I built
Three-department keypad menu with a press-zero replay loop, and voicemail-on-no-answer delivering transcription by email including the caller's number for callback.
Serverless function handling delivery through a verified sender domain.
Controls: a ten-minute per-call cap, monthly spend alerting, and UK-only dialling permissions with every other geography disabled.
What went wrong
The initial business profile was rejected on an email-domain mismatch. Diagnosed it, corrected the registered contact, secured an approved bundle.
When the geographic-number route stayed blocked, pivoted to a national number to get live and kept the original line in service via divert.
Callers heard the greeting cut off mid-sentence. The raw TwiML and flow logs showed the gather was accepting speech alongside keypad on a one-second timeout, so line noise triggered a barge-in. The platform makes speech language a required field, so it couldn't be disabled — I restructured the flow to move the greeting outside the gather entirely.
Live£1,000/mo removedSystems & product
Kitchen & food safety app
In-house replacement for a paid kitchen compliance platform charging £500 per site per month, distributed directly to kitchen staff.
Daily compliance sheet and probe logging
SupabaseMobileCompliance loggingOpenClose
What it does
Digital temperature logging, daily compliance sheets, cleaning schedules and timed area checks.
Completion data feeds directly into the weekly management report — something the bought platform never surfaced.
Replaced a £1,000/month subscription and came out with better reporting than the product it replaced.
App Store · v1.4+£800/mo removedSystems & product
Staff management iOS app
A full workforce management product shipped to the App Store, in daily use by 15+ staff across the group, replacing a paid platform. Packaged as a white-label reseller product with a complete handover pack.
Live on the App Store — venues and staff names anonymised
SwiftUISupabaseAPNsEdge FunctionsOpenClose
What I built
Three-tier role model — staff, manager, owner — with a hard five-tab cap per role, and PIN authentication routed through a server-side Edge Function.
Geofenced clock-in and clock-out, a rota builder with push notifications, holiday accrual and approval workflows, shift swaps requiring manager sign-off.
Guest check-in with extra-night extension billing, and a cross-venue manager dashboard.
A PII protection layer separating pay data from general staff records.
Schematics
Geofenced clock-in, with the site confirmed on screen
Live since Jul 2026~£1/month to runSystems & product
AI email agent
An agent that answers the venue inbox. It polls every ten minutes, classifies each message, and writes a reply into the Drafts folder for a human to read and send. It has never sent an email and cannot. Everything it isn't certain about goes to a person instead.
Two of the six stages contain no AI at all — that's the design
The model proposes a reply. Then ordinary code decides whether it is allowed to exist. A prompt is a request; code is a rule — and the model cannot talk its way past a rule.
No bank details, ever. Every draft is scanned for sort-code, account-number and IBAN patterns. The highest-value rule in the system, because invoice-interception fraud targets hospitality inboxes specifically.
Price whitelist. Every currency figure in a draft must already appear in the knowledge base. An invented price cannot leave the building.
Force-escalate keywords read from the incoming email, not the reply — refunds, chargebacks, solicitors, environmental health, allergy, coeliac, anaphylaxis. These escalate regardless of what the model concluded.
Link whitelist, competitor blocklist, a length ceiling, and a block on any draft that discloses being automated.
A confidence floor: below 0.75 it escalates whatever it said.
A blocked draft isn't discarded silently — it's logged and escalated, so you can see what it tried to say and why it was stopped. The failures are visible rather than invisible.
What I built
A six-stage pipeline where stages two and four contain no AI at all. A deterministic pre-filter drops booking platforms, suppliers, marketing and anything from a no-reply address before a single token is spent; the guard layer sits between the model and the mailbox.
An 18-section knowledge base as one editable file per venue, read fresh on every run. Changing a price needs no redeploy, and if a fact isn't in the file the agent doesn't know it and escalates.
Application-level mail permissions clamped to a single mailbox by an Exchange Application Access Policy, rather than granting tenant-wide access. Verified both ways: the intended mailbox granted, another explicitly denied.
Every decision logged to SQLite with its token cost. Measured, not estimated: £0.019 for twenty messages, around £1 a month at real volume.
Built multi-venue from the start — a second site needs a folder and a filled-in questionnaire, not a code change.
What went wrong
The first version over-escalated badly. The classifier treated any mention of a room as an availability enquiry, so questions about hairdryers, breakfast and luggage went to a human. The distinction is whether they're asking if a room is free — everything else the knowledge base covers is just a question. Fixed by splitting the intent taxonomy with explicit examples.
Documented rather than hidden: the schedule only runs while the machine is awake. Acceptable while a human sends every draft, and a blocker that must be cleared before auto-send is ever switched on.
What the analysis found
Exporting twelve months of mail was meant to size the problem. It answered a different question: the inbox gets around 195 genuine guest threads a year, roughly four a week, already answered 80% of the time. The gain was never hours saved — it was replies going out in minutes instead of days, including at 11pm on a Sunday, and being right every time.
It also surfaced two things nobody was looking for. 21 of 150 sampled guest threads had booked and never received a confirmation email — spread evenly across six months, so a persistent fault rather than a blip, generating 14% of all guest mail. And 76 separate tour operators and travel agents had made contact in a year, a commercial channel sitting in the same pile as "do you do Sunday lunch".
Build in progressSystems & product
Wearable voice assistant
A push-to-talk voice-capture pendant. Worn through the day and spoken to on a button press; at end of day it summarises what was said and done, updates a to-do list and surfaces who needs calling back.
Push-to-talk capture, local-first sync
ESP32-S3BLESpeech-to-textLLMOpenClose
Decisions
Push-to-talk only, never always-on — a deliberate privacy choice, not a battery one.
Validated the premise cheaply first, running phone-recorded audio through transcription before committing to hardware, then went straight to custom hardware rather than a phone-plus-Bluetooth-button prototype.
Device captures to onboard SD, pairs to a phone app over BLE, the app holds data locally, and AI summarisation runs when connectivity is available.
Contacts kept as a user-managed list read by the model at name-resolution time, rather than baked into a static prompt.
Live£100/mo removedSystems & product
Table booking system
Full schema and booking engine built into the group website, replacing a paid reservations platform. 395 historic bookings migrated across.
Role-permissioned admin panel with a separate enquiries queue for large parties and private hire.
Transactional guest confirmation email fires from a database webhook, not from the page. The booking row is written first and the email is triggered by that write — so a confirmation cannot be missed because a browser closed or a request timed out. That was exactly the failure mode of the platform this replaced, which silently dropped 21 confirmations across six months.
Availability logic covering notice periods and past-slot filtering.
A full integration handover specification for surfacing bookings inside the iOS app.
What went wrong
Generated-column immutability blocking inserts.
CDN serving an ES module where UMD was expected, and a variable name collision with a CDN global.
RLS blocking anonymous inserts through a trigger, and select-after-insert failing under RLS.
A timezone bug shifting UK dates back a day, and past-time bookings passing validation because only the date was checked.
Schematics
Booking states from enquiry through to confirmation
LiveCert valid to Sep 2027Systems & product
Apple Wallet business card
A signed Apple Wallet pass that sits alongside the payment cards on my phone. Someone scans the code off my screen and lands on this site — no app to install, nothing to hand over, and it works offline once added.
The pass on device — artwork as a strip image, Wallet drawing its own scannable code beneath
Registered a Pass Type ID and generated the CSR and private key from the command line rather than the usual keychain route, which wasn’t available on this machine.
Assembled the full signing chain — pass certificate, Apple WWDR intermediate, and a CMS signature over a manifest holding a SHA-1 digest of every file in the bundle.
A build script that rebuilds the manifest, re-signs, zips and refreshes the deploy folder in one command. Design changes cost seconds; the certificate work is never repeated.
Artwork rendered as a strip image on a storeCard pass at 1×, 2× and 3×, so Wallet draws its own scannable code underneath rather than competing with an embedded one — the structure a loyalty card uses.
Back of the pass carries phone, email, portfolio, LinkedIn and education as labelled fields, so the details are readable without scanning anything.
Hosted on its own site with a _headers rule setting the pkpass MIME type. Without it the browser downloads a file instead of handing it to Wallet.
The decision that matters
The code’s contents are inside the signature. Point it straight at a profile and changing that target later means rebuilding the pass and re-adding it to every phone holding it.
So it points at a destination I control and can change without touching the pass. The signed artefact never needs revisiting; the certificate runs to September 2027.
What went wrong
iOS rejected the first passes outright, and Mail wouldn’t open them either — which ruled out the browser and pointed at the signature. The cause was -noattr stripping the authenticated attributes from the CMS signature. It verifies locally either way, which is exactly why every local check passed. Removing the flag and moving to a SHA-256 digest fixed it.
AirDrop delivers a .pkpass to Files rather than Wallet, so a working pass looked like a broken one. Email attachment works; so does serving it over HTTPS with the right content type.
A drag-and-drop deploy overwrote an existing site rather than creating a new one. Restored from the previous deploy and separated the two projects permanently.
Researched Wallet NFC before committing and established it was a dead end — the entitlement is gated to reader protocols, and there is no reader to tap. Ruled out rather than built toward, since the phone-to-phone case is already handled by the operating system for free.
Live since Aug 2026150 single-use codesSystems & product
Partner voucher redemption
A single-use discount voucher scheme run with a canal boat hire operator moored beside one of the sites. They hand 150 printed cards to their customers; we never deal with those customers directly. Each code works exactly once, ever — and that's enforced in the database, not by the paper.
The check and the write are one statement, evaluated under row lock
Redemption is a single conditional update, not a read-then-write. The redeemed_at IS NULL condition is evaluated by Postgres as part of the write, under row lock. Two staff scanning the same card in the same instant cannot both succeed — one gets the row, the other gets ALREADY USED. A check-then-write would have left a race window open on exactly the busy Saturday it matters.
There is no insert, update or delete policy on the table at all. Not even a signed-in member of staff can write to it directly. Every change goes through one security-definer function, which is what makes the audit trail trustworthy: no path exists that sets redeemed_at without also setting who did it.
The anonymous key baked into the page has been revoked from the table entirely, so a guessed code hits a wall before it reaches any data. Lookups return zero rows for a code that doesn't exist rather than an error, so probing reveals nothing — and only after getting past a login anyway.
redeemed_at being null is the unused state. No separate status column, so a voucher marked used with no timestamp is not a state the schema can represent. Cancellation is a separate void flag, so a card lost in the post doesn't look like a customer used it.
A unique index on upper(code) makes uniqueness case-insensitive, so two codes differing only in case can never coexist.
Total exposure is capped at 150 discounted food bills. Even in the worst case — the entire code list leaking publicly — the scheme cannot cost more than what was already agreed to give away.
Decisions in the room, not the code
The staff device scans the customer's card, never the reverse. The single most important rule in the design — a green screen on a customer's own phone proves nothing and is a screenshot waiting to happen. The guest-facing page deliberately reveals nothing about whether a voucher is still valid.
Two-tap redeem, because the action is irreversible and misfires behind a busy bar are inevitable. And no un-redeem in the interface at all — reversing one requires database access, so it has to come to me. Staff can't quietly undo a mistake.
The code alphabet omits I, O, 0 and 1 — the characters people misread off a scuffed card — leaving 32 characters across 8 positions, around 1.1 trillion combinations against 150 live codes. The format is chunked so it can be read down the phone without losing your place, and the input auto-formats as it's typed.
Cards generated at business-card size with error-correction-level-H QR codes, so a card can take a beer ring and still scan. The scanner regex-matches the code anywhere in the payload, so it works whether the QR carries a full URL or a bare code.
The card is taken off the guest and kept. Not handed back, not photographed — a card in the till drawer cannot be passed to the next table, and a photo or screenshot is refused outright.
Staff are told to read the screen, not the card: the screen states 15% or 20%, and it is the screen that decides. Redeem is tapped only once the guest has ordered, because it cannot be undone.
Deliberately not built: no EPOS integration (weeks of work to save four seconds per transaction), no email delivery (a card can be taken off the guest at the point of use, an email can't), no minimum spend (a rule staff have to police, for no real protection).
What went wrong
A drag-and-drop deploy created a new site rather than adding to the existing one. Left that way on purpose — the other project runs live Stripe card payments, and keeping the voucher page out of that folder means never redeploying a payments site to change voucher wording.
The custom domain 404'd on first visit. The single-file deploy had been promoted to index.html, so the page had been live at the domain root the whole time.
A certificate provisioning failure that looked like a fault but was expected and self-correcting — the host can't validate a domain that isn't pointing at it yet.
The database editor's 100-row default silently truncated the code export, losing an entire batch of 50 with no error shown. Caught by counting the output against what was seeded, then worked around by exporting each batch separately.
Written up with its open items rather than signed off clean: a shared venue login identifies the site but not the individual, and the two batches are visually indistinguishable, so they have to stay physically separated or be sorted one lookup at a time.
Send me your software subscriptions and I’ll come back with what’s replaceable and what it’s worth — free, and yours to keep either way. How it works →
Contact
Want the same done to your cost base?
Open to operations, automation and technical operations roles — and to talking with anyone running a multi-site business who suspects they're paying for software they could own instead.