Sajjad Mahessar Good Merch Group Free audit
Selected work

Systems & product

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 2026 Systems & 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.

GUEST · QR EDGE FNORDER STRIPE WEBHOOK BAR SCREEN · REALTIME 0:48 1:52 4:11 0:12
Order flow — guest to Edge Function to bar screen
PostgreSQL/SupabaseStripeEdge FunctionsRealtime OpenClose

What I built

  • 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

Stripe payments list showing successful live card payments and one refunded order
ORDER TIMER · BAR SCREEN 0:002:004:006:00 ACCEPTED CHIME REPEAT FULL ALARM ACTIONS AVAILABLE THROUGHOUT ACCEPTDELIVEREDSOLD OUT REFUSE + REFUND
GUEST CARDORDER FN STRIPEPAYMENTINTENT WEBHOOK · 4 EVENTS SUCCEEDEDFAILEDCANCELED REFUNDED RECONCILEDAGAINST ORDER
Timer bands — chime at 2:00, escalating at 4:00 and 6:00
Live since Aug 2026 Five rooms, five URLs Systems & 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.

FIVE LETTING ROOMS · FIVE CODES · FIVE URLS ROOM 1ROOM 2ROOM 3ROOM 4 ROOM 5 ITS OWN GUIDE ROOM TYPE · ORDER LINK ALREADY CORRECT NOTHING FOR THE GUEST TO CHOOSE, NOTHING TO GET WRONG
One code per room — the guide already knows which room the guest is standing in
Single-file HTMLNetlifyPython (segno)PlaywrightDNS / SSL OpenClose

The decision that matters

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.
Live Systems & 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 call flow showing keypad routing to three departments with answer detection
INBOUND GATHERKEYPAD 1 · RESERVATIONS 2 · KITCHEN 3 · MANAGEMENT V/MAIL+ TEXT PRESS 0
The published flow — three departments, each with its own answer detection
Twilio StudioFunctionsTwiMLSendGrid OpenClose

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 removed Systems & 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 3.1C -18.4C 75.2C -- -- PROBE · LOGGED TARGET
Daily compliance sheet and probe logging
SupabaseMobileCompliance logging OpenClose

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 removed Systems & 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.

The App Store listing for the staff app, showing four screens
Live on the App Store — venues and staff names anonymised
SwiftUISupabaseAPNsEdge Functions OpenClose

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.
  • Cross-site stock transfers, time-locked daily compliance sheets, timed toilet checks, weekly cleaning checklists.
  • 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

Three phones showing clock-in, rota and break screens
Clock-in screen showing site and clocked-in status
Break screen showing on-break status and end-break action
Rota screen showing shifts across both sites
STAFFMANAGEROWNER CLOCK IN / OUTROTA VIEWHOLIDAY REQUEST ROTA BUILDERSTOCK TRANSFERPAY DATA CROSS-VENUE PAY DATA SEPARATED AT THE DATA LAYER, NOT THE UI
MONTUEWEDTHUFRISATSUN A. K.M. P.J. R.S. B.L. T. SWAP REQUESTED · MANAGER SIGN-OFF · PUSH SENT
Geofenced clock-in, with the site confirmed on screen
Live since Jul 2026 ~£1/month to run Systems & 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.

The escalation section of the knowledge base listing categories the agent must never auto-answer
EVERY TEN MINUTES 1 · FETCH · NEW MAIL SINCE WATERMARK 2 · PRE-FILTER · NO AI · DROPS OTA, SUPPLIER, NOREPLY 3 · UNDERSTAND · ONE MODEL CALL · RETURNS JSON 4 · GUARD · NO AI · CAN VETO ANYTHING PROPOSED 5 · ACT · CREATE DRAFT IN THE ORIGINAL THREAD 6 · LOG · DECISION AND TOKEN COST THE MODEL PROPOSES · ORDINARY CODE DECIDES WHAT IS ALLOWED OUT
Two of the six stages contain no AI at all — that's the design
Python (stdlib only)Microsoft GraphProvider-agnostic LLMSQLitelaunchd OpenClose

The guard layer

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 progress Systems & 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.

PUSHTO TALK SD CARD BLE LOCAL ONLY DAYSUMMARY LLMWHENONLINE NEVER ALWAYS-ON · CAPTURE ON PRESS ONLY
Push-to-talk capture, local-first sync
ESP32-S3BLESpeech-to-textLLM OpenClose

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 removed Systems & product

Table booking system

Full schema and booking engine built into the group website, replacing a paid reservations platform. 395 historic bookings migrated across.

AVAILABILITY GRID MONTUEWEDTHUFRISATSUN PAST SLOTS FILTERED NOTICE PERIOD ENFORCED 395 HISTORIC BOOKINGS MIGRATED
Availability grid — notice and past-slot rules
PostgreSQL/SupabaseEdge FunctionsDatabase webhooksRLS OpenClose

What I built

  • 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

The five-step guest booking flow on the group website
The admin booking diary showing a week of bookings across both sites
SLOT PICKEDVALIDATEDINSERTED DB WEBHOOK EDGE FN · EMAIL GUEST INBOX LARGE PARTIES AND PRIVATE HIRE DIVERT TO AN ENQUIRIES QUEUE 21 THREADS FOUND WITH NO CONFIRMATION — THE FAULT THIS REPLACED
FOUND AND FIXED IN ROLLOUT GENERATED-COLUMN IMMUTABILITY BLOCKING INSERTS CDN SERVING ES MODULE WHERE UMD EXPECTED VARIABLE COLLISION WITH A CDN GLOBAL RLS BLOCKING ANONYMOUS INSERT VIA TRIGGER SELECT-AFTER-INSERT FAILING UNDER RLS TIMEZONE SHIFT MOVING UK DATES BACK A DAY PAST-TIME BOOKINGS PASSING DATE-ONLY VALIDATION
Booking states from enquiry through to confirmation
Live Cert valid to Sep 2027 Systems & 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 Wallet pass on a phone, showing the wordmark and the scannable code
The pass on device — artwork as a strip image, Wallet drawing its own scannable code beneath
PassKitOpenSSL / CMSPython (segno)NetlifyBash OpenClose

What I built

  • 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 2026 150 single-use codes Systems & 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 voucher print sheet, ten cards to an A4 page with cut guides
TWO STAFF · SAME CARD · SAME INSTANT UPDATE ... SET redeemed_at = now() WHERE redeemed_at IS NULL 1 ROWPOURED 0 ROWSUSED POSTGRES PICKS THE WINNER UNDER ROW LOCK · NO READ-THEN-WRITE GAP
The check and the write are one statement, evaluated under row lock
PostgreSQL / SupabaseRLS + SECURITY DEFINERSingle-file HTMLPython (ReportLab) OpenClose

Making it bulletproof

  • 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.
No charge

I’ll find the money before you pay me anything.

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.