theme rss
visitor@fireatwill:~/projects$ ↑ history · enter to run
~/projects/0x0103
fireatwill ¬ 0x0103 ¬ projects2026.07

Kislings

author Sofus Bech status in development stack pocketbase · react native · expo repo github.com/sofus-bech/kislings

A café's whole digital counter in one app — a two-track loyalty card (a stamp per cup, free coffee at 10; a stamp per 250 g bag, free beans at 6), plus what's on the grinder, brew recipes, news, events and menus. A PocketBase backend the owner edits from an admin UI, and a React Native app that updates in real time.

pocketbasereact-nativeexporealtime

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:

Caddyfile
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:

loyalty
# 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

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.