A local café doesn't have a developer on call. Whoever owns it needs to change the opening hours, swap the coffee on the grinder, and post tonight's event — from their phone, between orders — and have it show up in the customer's app a second later. So the real design problem wasn't the app. It was making a backend the owner could run themselves without ever seeing code.
The problem
A paper loyalty card gets lost. A spreadsheet has no history. And a loyalty card that lives on a server in someone's home closet goes dark the moment the home internet does — which, for the one feature customers touch every single day, is unacceptable. It had to be boring, hosted somewhere with its own uptime, and editable by a non-technical owner.
Content the owner edits; code the customer never sees.
How it works
The backend is PocketBase — one Go binary, SQLite, and a built-in admin UI — pinned to a fixed version and run under systemd on a small VPS, chosen over the homelab precisely so a power cut at home can't take down the card. Content collections (coffees, recipes, news, events, menus) are public-read; stamps are staff-only. The app opens a single realtime stream, and any change the owner makes re-fetches instantly. The one config line that keeps that stream instant, behind the reverse proxy:
pb.kislings.dk {
reverse_proxy 127.0.0.1:8090 {
flush_interval -1 # don't buffer — keep the SSE realtime stream instant
}
}
The loyalty count is never a stored number. Each track's balance is derived — the stamps you've earned since your last redeem on that track — so you get a full audit trail for free and the two tracks can't bleed into each other:
# count = stamps on a track since the last redeem on that track kaffe ▓▓▓▓▓▓▓░░░ 7 / 10 # free cup at 10 bønner · 250g ▓▓▓░░░ 3 / 6 # free bag at 6
What I'd do differently
- The design handoff assumed iOS-only and said "SwiftUI is the natural choice." Because the UI is fully custom-drawn with no native chrome, React Native + Expo got me Android for free — but I nearly took the handoff at its word and boxed myself in.
- PocketBase's migration API shifts between versions; writing a migration from memory bites you. Pinning the version early and reading that version's docs is not optional.
- Danish content, English code — a clean rule, but I set it late and had to go back and un-mix a few seed strings.
Roadmap
Push notifications when a new coffee hits the grinder, wired to the realtime stream that's already there. Then the slow work: order-ahead, and letting the owner theme the app from the same admin UI.
Thanks for reading. Found a bug or have a sharper idea? get in touch — or run cat 0x0104 above for the lending-desk tool.