House Pub Platform
The software behind the I-House pub, from the bar TVs and tender scheduler to an audited operator admin.
The I-House pub is run by residents who work a few shifts a week, so the software behind it has to keep itself healthy, explain failures in plain language, and let a new volunteer do the right thing without a training session. I built that platform, in partnership with AVI Foodsystems, the pub’s food-service operator. The audio, lighting, and song identification are covered in House Pub AV Control and Now Playing.
In plain words. Everything behind the bar that is not a drink: the pub’s website, the two TV screens, the schedule that tells volunteers when to work, and the control panel staff use to run it all. Its job is to keep working, and to say clearly what went wrong when it does not.
At a glance
- Status: in production.
- What it runs: a public website, two bar TVs, an iPad kiosk, a shift scheduler, and an operator admin.
- Built on: Cloudflare (a web backend, databases, and file storage), plus a Raspberry Pi in the room.
- Scale: over 12,000 automated unit tests and more than 100 permission keys.
See it
You can try the live page at pub.ihnyc.affectivetech.com.
How it works
- Patrons see a public website and two TV screens.
- Staff run the pub from an admin site, where each person can do only what they have been given permission to do.
- One backend serves both, and a small computer in the room reports what the audio and lights are doing.
flowchart LR
P[Patrons and staff] --> W[Pub backend]
TV[Bar TVs] <--> W
Pi[Room Raspberry Pi] <--> W
Cal[House Desk calendar] --> W
In technical terms, the backend is a Cloudflare Worker with static assets, plus a separate recovery Worker. It is backed by D1 databases (content, setlist, and recovery data), R2 for media, KV, and two Durable Objects: one for TV pairing state and one that relays lighting commands. House Desk is the system of record for events, so the pub reads a validated copy of the events shared with it, and their flyers, with a short-lived fallback cache.
Problems I had to solve
A TV goes dark and nobody knows why
A TV browser has long uptimes and gets power-cycled, so I made the screens report on themselves.
- Slot identity. A TV is bound to a named slot rather than to its address, and a replacement TV can adopt an orphaned slot.
- Diagnosing a dark screen. Each TV page leaves a note every 30 seconds and at every lifecycle event, and reports how the previous page ended. The backend classifies each outage as reloaded itself, browser dropped the page, browser closed, backgrounded, frozen then resumed, or stopped without warning, and reports how long the screen was dark. It cross-checks the TV maker’s cloud for power and input state, and flags outages that hit both TVs within two minutes as a shared cause such as power or Wi-Fi. Each TV also runs its own speed test nightly.
- Content rules that match how the bar runs. The left screen features the tenders on shift with a tip QR code and follows last call. The right shows specials, announcements, now-playing, and a pub QR code. Announcements can be limited to shifts, days, and times of day, and themes (brand colors, Pride, a team’s colors) can be scheduled.
- DJ mode. A projector video queue with patron photo moderation, and a transcoder on the Pi that prepares uploaded video.
Anyone could answer a scheduling form as anyone
Tender scheduling used to live in a chain of no-code automations. I replaced it with a weekly workflow inside the platform: a wizard to set up shifts, a form that tenders answer for availability, an assignment step, and the emails that send the result.
The original form link carried only a tender id, and ids were public, so anyone could answer as anyone. Links are now signed per tender and week, and answers are labeled “link verified” at read time. The change ran in a shadow mode first so real traffic could confirm it before it changed any result. It is now enforcing: an answer whose link does not verify is shown, but not counted. Last week’s links stop counting when the next week’s form opens, because the signature names the week.
The assignment step also gives each tender who offered a night a seat before anyone gets a second where it can. When a schedule is edited by hand, a summary lists each change, who made it and why, and anyone who offered a night but was left without a shift.
Different people need different powers
The admin is a TypeScript and React application with a route registry. Everything is behind Cloudflare Access, and on top of that the platform has its own permission model: over 100 permission keys grouped into scopes, assembled into bundles such as AV operator and AV engineer. A key means exactly one thing. When one key had quietly grown to gate several unrelated actions (for example one that also covered pairing screens), I split it so that granting one never silently grants the other.
Every non-read admin request writes an audit record with the actor, the normalized route, the required permission, the outcome, and a correlation id. Denied and failed attempts are recorded too, because the attempts that did not land are the ones that matter in an incident. In the audit view, repeated page views by the same person collapse into one counted entry; mutations never collapse, and nothing is merged server-side.
A change must not break the pub
- Tests. Over 12,000 unit tests, around 1,450 Python tests for the Pi services, and end-to-end tests that start the backend locally. A required build gate runs the full test and build verification before a merge deploys.
- Database changes. They go through an audited runner that requires a recent verified recovery set, prints a dry-run plan, and only proceeds when re-run with the exact confirmation string it printed.
- Recovery. Daily and monthly recovery sets cover the databases, Durable Object and KV state, media, live configuration, the exact release source, and the Q-SYS design. Daily sets also include encrypted Pi state. They carry hashes and retention evidence, and restore drills are documented.
- Releases. A release contract binds the Git revision, backend version, TV build, Pi revision, migrations, and settings together so a restore can reproduce a known state.
Also in the platform
- Sales reporting. Nightly sales reports from the pub’s point-of-sale system are validated and ingested into sales views, with nights that were only test rings excludable, and a weekly operations summary.
- An iPhone app, in development. The backend has a feature-flagged API for a native app, with reads first and writes behind a second flag, and the admin pages render in an embedded mode inside the app’s web view.
Tools
Cloudflare Workers, D1, R2, Durable Objects, KV, Access, TypeScript, React, Vite, Playwright, Vitest, Python on a Raspberry Pi.
Under the hood
The signed-link check (about 20 lines)
Why this matters: the signature covers the tender and the week, so a link cannot be edited to answer as someone else, and a stale link cannot be reused. The comparison takes the same time whether the first or last character is wrong, so it leaks nothing about the signature.
// worker/lib/pub-scheduler/availability-link.js (excerpt)
const SIG_LENGTH = 22; // 22 base64url characters = 132 bits of the HMAC
function message(tenderToken, weekOf) {
return `pub-availability:v1:${normToken(tenderToken)}:${weekOf}`;
}
async function sign(key, tenderToken, weekOf) {
const mac = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(message(tenderToken, weekOf)));
return base64url(new Uint8Array(mac)).slice(0, SIG_LENGTH);
}
function sameString(a, b) {
if (typeof a !== "string" || typeof b !== "string" || a.length !== b.length) return false;
let diff = 0;
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
return diff === 0;
}
Words used on this page
- Backend: the program that answers requests from websites and screens.
- Worker: a backend program that runs on Cloudflare’s network instead of one server.
- D1, R2, KV: Cloudflare’s database, file storage, and small key-value store.
- Durable Object: a small piece of state that stays consistent even when many requests arrive at once.
- Cloudflare Access: a login gate that sits in front of the admin.
- HMAC: a keyed fingerprint that proves a message was not altered.
- Audit record: a permanent note of who did what, and what happened.