Systems built for people who pay for them — outside the group, across four industries, on iOS, Android and the web. Delivered with handover documents, and with what is still open written down rather than buried.
The first system built and sold through my own company. Five serviced apartments in one building: guests get a signed check-in link, the door code appears only during their stay, and it disappears at checkout. Staff codes rotate themselves weekly. Local event demand feeds the pricing.
Encrypted at rest, revealed only inside the stay window, gone at checkout
Every door code is encrypted with AES-256-GCM before it reaches the database. A stolen database dump contains ciphertext and nothing else.
Guest check-in links are HMAC-signed tokens carrying the booking and an expiry — not guessable, and not editable to point at another booking.
Staff passwords are scrypt-hashed with a per-user salt; webhooks are rejected unless the signature matches the raw body.
Neither key is ever sent to a browser. The guest page asks the server what it is allowed to see and gets back a code or nothing.
Rotating the signing secret invalidates every outstanding link at once — which is also the emergency lever if one ever leaks.
Two decisions that come from operations, not code
Codes are issued at booking confirmation, not at check-in. The PIN is written to the lock days ahead and lives in the lock’s own memory, so a broadband fault on arrival day cannot lock a guest out of the building at eleven at night.
Rotation writes the new code before retiring the old one. If the lock is unreachable the old code keeps working and the failure is logged — the opposite order would leave a cleaner standing outside a flat they can’t enter.
Staff codes rotate on a seven-day cycle because they have no natural end. A code that leaks with a departed cleaner, a contractor or a photographed keypad stops working within the week without anyone remembering to change it.
Built before the hardware existed
The lock provider sits behind an interface with a mock implementation, so every path — issue, reveal, revoke at checkout, rotate — runs for real against a simulated lock. One environment variable swaps the mock for the live provider when locks are installed. No application code changes.
Messaging works the same way: dry run is the default. With no API keys set, every email and SMS is composed, stored and shown on the dashboard but nothing is sent — so the whole checkout flow is testable for free, without texting anyone.
Pricing from local demand
Three event sources refreshed daily and merged into one table — a venue what’s-on page, a ticketing API and a fixtures feed — because the trade shows that actually drive midweek demand never appear in ticketing APIs.
The scraper runs once a day, identifies itself with a real contact address, uses conditional requests so an unchanged page costs a few bytes, takes facts only, and stops on a 403 rather than retrying — a bad fetch leaves yesterday’s events in place rather than emptying the calendar.
Long-running exhibitions are capped in the weighting: a three-month immersive experience is background footfall, not a spike.
The demand weightings are documented as informed guesses to be replaced by the client’s own occupancy data after a season, rather than presented as fact.
What’s written down as not done
Two-factor for staff, and secrets moved out of environment files into a managed store.
Automated encrypted backups with one tested restore — untested is not a backup.
A data processing agreement with the client, ICO registration, and a retention job deleting guest personal data on a schedule.
The live lock adapter is written but has never been run against hardware; a day is budgeted to shake out field issues.
Handed over as a checklist the client can hold me to, rather than a system described as finished.
App Store · Jul 2026Second group, two venuesClient work
White-label staff app, second group
The staff platform rebuilt and shipped for a different pub group — two venues, its own database, its own App Store listing. Proof the first one was a product rather than a one-off: 41 Swift files, 19 migrations, approved by Apple as an unlisted app.
Tabs appear per venue from the data — a third pub needs no code change
Swift / SwiftUISupabasePostgreSQL + RLSCoreLocationOpenClose
What I built
41 Swift files, around 15,000 lines, 19 numbered migrations, across two venues with separate checklists, draught lines and staff.
Geofenced clock-in with the live distance shown on screen, breaks recorded separately, and a rota builder with draft-then-publish so staff only see finished weeks.
Holiday accrued live at the UK statutory 12.07% of hours worked for hourly staff, calculated from completed timesheets so it is never stale, on a 1 April holiday year.
Peer-to-peer shift swaps with an admin gate — only swaps both parties have agreed reach the inbox, and approving reassigns the shifts in the database rather than leaving someone to edit the rota by hand.
End-of-day sheets, income by category and cash-ups, with variance shown green, amber under £5 and red at £5 or more.
Documents in private storage served through signed URLs valid for one hour.
The cellar ordering model
Every draught line carries a par level — what should be on site immediately after the weekly delivery. Count the cellar and the order is simply par minus counted.
Par is derived from sales rather than guessed: a week of average demand plus a safety margin of 1.65 standard deviations, which sets a 95% service level — the line runs dry no more than once in twenty weeks. Sellable pints allow around 7% loss to line cleaning and ullage.
The app stores the average and the standard deviation per line, recomputes what par should be, and flags any line where the stored figure has drifted — so the quarterly review surfaces itself. Each submitted count snapshots the par used, so recalculating later never rewrites an old count sheet.
What went wrong
A full outage during the build, caused by one missing clause. Row-level security policies created without an explicit role clause don’t apply to the role the app actually uses, so every query failed with a permission error — which the login screen reported to staff as “Invalid PIN”. The symptom pointed nowhere near the cause.
Daily checklists weren’t resetting overnight. The view was merging today’s completions and this week’s into one lookup, so a daily item ticked on Monday still read as done on Tuesday. The data was never wrong — only the display.
Two Supabase projects with near-identical names in the same dashboard; running SQL against the wrong one fails silently with “relation does not exist”. Documented in the handover as the single most likely mistake for whoever comes next.
Handed over honestly
One venue’s GPS coordinates are an estimate that was never verified on site — written down, with the on-screen distance reading given as the way to check it.
One account still on a placeholder PIN, and a possible duplicate staff record, both flagged rather than left to be discovered.
Push notifications scoped and not delivered — the only substantial feature promised and not built. CSV export, reports and photo attachments were considered and deliberately dropped.
Security rests on PIN-gating in the app rather than per-user policies. Stated as a deliberate trade-off for a small team, with the cost of changing it spelled out rather than presented as a finished security model.
The handover covers adding a third pub end to end: a SQL template, a collision check, and two in-app steps. No code changes, no release.
Submitted to App ReviewThird iOS appClient work
Cleaning company operations app
An iOS app for a commercial cleaning company: cleaners see the jobs assigned to them, clock in on site, work the checklist, capture before-and-after photos and take a customer signature. Forked from the pub staff platform rather than started from scratch — and then rebuilt where the domain differs.
Sign-in, the application form, and a site record with its access instructions
Swift / SwiftUISupabasePostgreSQL + RLSStorageCoreLocationOpenClose
The access decision
A cleaning company holds alarm codes, gate codes and key details for every site it cleans. That is the most sensitive data in the business, and it sits in a database a phone app talks to directly.
Cleaners can read a site’s access codes only if they are assigned to a job there, and only in the 24 hours before that job starts. Outside that window the row simply isn’t returned.
The rule lives in row-level security, not in the app. A leaked key, a modified client or a direct API call still can’t read a code it isn’t entitled to — the database refuses, not the interface.
Role checks run through helper functions rather than being repeated in every policy, so there is one definition of what an admin is and it can’t drift between tables.
Reused deliberately, isolated deliberately
The app was forked from the pub staff platform rather than started from scratch. Authentication, the clock-in flow, GPS handling, notifications and the component library carried across untouched; customers, sites, jobs, photos, signatures and hazards were rebuilt, because that domain has nothing to do with pub shifts.
The database is a separate project, not a shared one with a tenant column. No shared data, no shared auth, no shared outage — one client’s problem stays theirs. Code reuse doesn’t have to mean infrastructure reuse.
What I built
Fifteen domain tables with row-level security on every one, plus role helper functions and update triggers throughout; storage buckets all private with per-bucket size and type limits.
Sign-up as an application rather than an open door: someone requests access, an admin approves or rejects with a reason, and approval issues their credentials. Nobody self-serves into a system holding other people’s alarm codes.
The cleaner’s day: today’s jobs, site detail with entry instructions and hazards, GPS clock-in, mandatory before-and-after photos, checklist, consumables, customer signature, upload on completion.
Auto-exposure of new tables switched off and automatic row-level security switched on at the project level — so a table added in a hurry is private by default rather than public by default.
App Review
Submitted, rejected three times, and resolved each time before approval. One of those was account deletion: Apple requires any app with sign-up to let a user delete their account from inside the app, not by emailing support.
Built as a type-to-confirm sheet that spells out the consequences and calls a database function to do the deletion properly, rather than a button that just signs you out. The requirement was met as a feature rather than as a formality.
Scope, held
The full specification runs to a customer portal, automated invoicing, inventory with low-stock alerts, AI photo comparison, offline mode and a KPI dashboard.
Version one ships the cleaner-facing app only, with administration done through the database console. Everything else is written down and dated for v2 and v3 — a scope list is only useful if something is actually on the other side of the line.
Google Play · Sept 202616 propertiesClient work
Rota, wages & profit app for a cleaning company
An Android app for a domestic cleaning company. It schedules recurring cleans across sixteen properties, pushes a reminder at 7am for anything due, records hours per cleaner, and works out wages, invoice value and profit as the hours are typed in. Built in React Native, distributed privately through Play internal testing.
Rates mean what I charge now; snapshots mean what applied then
Rates stored on a property or a cleaner mean what I charge or pay now. Copies stamped onto the completed job and the logged hours mean what applied then.
Put a client up next spring, or give someone a pay rise, and last autumn’s figures stay exactly as they were. Without it, every historic report silently rewrites itself the moment a rate changes — and the numbers in the app stop matching the numbers people were actually paid.
Rules that protect a record of money
The database refuses to delete a cleaner who has logged hours, because those hours are what somebody was paid. Not a warning in the interface — a foreign key that says no.
Deleting a property would cascade and destroy its wage history, so the app checks for completed cleans first and offers to pause the property instead.
A unique index allows only one open clean per property at a time — which is what stops the rota forking or stacking duplicates when a clean runs late.
Every completion records which job generated it, so a mistake can be undone precisely rather than patched over.
The calculation
Three hours at a cleaner’s cost rate against a property charged by the hour gives wages, invoice value and the margin between them — updating live as the hours are typed, before anything is saved.
The same arithmetic runs in two places: once on the phone for the live figures, once in the database on save. Two copies of a money rule is exactly how figures drift apart, so both were written to identical logic deliberately, and that’s recorded in the handover.
What went wrong, and what it taught
Built with bottom tabs first, then moved to a drawer. Android’s gesture bar sits in the same strip and taps were landing on the wrong control.
Push notifications need two separate halves on Android — one file to let the app receive, a service key to let the server send. With only the first you get a valid token and a silent credentials failure.
A bulk insert with on conflict do nothing needs a unique constraint to have anything to conflict with. Without one it inserted a duplicate — which is how there came to be two of the same cleaner.
A free-text date field gets handed half-typed values; a partial date crashed the scheduler mid-keystroke. Fixed by deleting the text field rather than validating it — a calendar cannot produce an invalid date.
Reminders are sent server-side rather than scheduled on the phone, because Android kills background work aggressively and a reminder that might not fire is worse than no reminder at all.
Written down as not done
Cleaner logins. The database is ready — roles, the link to a cleaner record, and access policies written and dormant. Only the interface is missing, so turning it on needs no migration.
Photos on completion, payment tracking (profit currently assumes everyone pays), and a monthly wage export for payroll.
Over-the-air updates, so a text change still costs a store round trip.
Also flagged honestly: the store screenshots are from the old build and show mostly empty screens. Noted as worth replacing once there is a week of real data behind them.
Delivered Jul 2026ConsultancyClient work
Operations platform review, aviation catering
An independent review of the software running day-to-day operations at an aviation catering business — meals produced in a kitchen and delivered to private aircraft at UK airports. Every screen and module walked through, then two documents: a plain-English user guide, and a costed modernisation proposal.
Findings banded by effort, sequenced to prove value before asking for investment
Systems analysisProcess mappingData quality auditAI strategyTechnical writingOpenClose
What I delivered
A system overview and user guide written to be read before anyone logs in for the first time, so that when they do, every screen already makes sense. Covers the business, the vocabulary, the order lifecycle, every module, and the workflows someone new will hit in their first week.
An improvement analysis and modernisation proposal — around forty findings grouped into seven themes, each with a business reason, an implementation approach and an effort estimate, then banded into a twelve-month roadmap.
A separate strategic section on where AI actually fits, with five concrete use cases grounded in data the platform already holds.
Leading with what already works
The proposal opens by being explicit about what the existing system gets right — a well-modelled six-stage order lifecycle, correct domain vocabulary rather than forced generic language, proper handling of amendments, recipe composition, consolidated invoicing, and a deep accounting integration.
The purpose is not to criticise the incumbent product. A review that opens with forty faults gets defended against; one that opens with what to preserve gets acted on. The one-line finding was that the bones of a very good system were already there — roughly fifteen medium improvements plus one strategic overlay, not a rebuild.
Findings worth naming
No single view of where an order is. An order sits in exactly one stage at a time, but nothing shows its journey — so staff assemble the timeline mentally, every time.
Data quality drifting quietly. Duplicate part numbers, placeholder descriptions never replaced, test records mixed into live data, and stock levels showing zero across the board — meaning either the module isn’t used or it is silently broken. Worth establishing which.
Integration failures visible but passive. A warning icon appears on the row, but with no retry action, no filter for unsynced records and no health indicator anywhere. Knowing something failed is not the same as being able to fix it.
Reporting stops at quantity and value. No margin, no period-on-period comparison, no customer profitability — despite both cost and sale price already living on the record. The data to answer commercial questions was there; nothing asked them.
Desktop-first throughout. Drivers airside, chefs on the pass and account managers on the road were all second-class users of a system built for a desk.
Where AI actually fits
Kept deliberately separate from the general improvements, and argued from the shape of the business rather than from the technology: orders arrive by email and PDF and get re-typed, amendments happen constantly, the data is structured and repetitive, and margins are thin enough that errors are expensive.
Highest-return case identified as email-to-order parsing — a monitored inbox reading incoming orders and dropping a draft into the system for a human to review and confirm. It removes the single largest manual bottleneck, and it keeps a person in the loop rather than letting a model book a flight’s catering unsupervised.
How the roadmap was sequenced
Quick wins sit at the front deliberately, and the document says why in plain terms: they are not the highest-value items. The strategic overlay is.
They go first because they build momentum, prove the working relationship, and clear the technical debt that would otherwise make the bigger changes harder. Fix the obvious, deliver the visible, then invest in the strategic.
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.