03/2026 – present · Opezey.com
Snooker Cafe Management System
Cross-platform venue management — a React Native app for staff, a Next.js platform for owners, kept in real-time sync over a Socket.io event layer.
A venue management system for a snooker café business: staff run tables, orders, and kitchen tickets from a React Native app on the floor, while owners monitor bookings and daily operations from a Next.js admin platform.
Both clients subscribe to the same Socket.io event layer, so a booking made at the counter appears on the owner's timeline the moment it exists — no refresh, no polling.
The system this replaced modeled reservations as static slots. A slot grid can answer "is 7pm free?" — it cannot answer "which tables are running long right now, and what does that do to the next hour?" In a venue where sessions extend and walk-ins happen, a grid is wrong within minutes of opening.
The naive fix — every client polling the server for fresh state — makes staleness a tuning knob: poll fast and the server pays for it, poll slow and the staff app and the admin panel show different floors. Neither side should ever be looking at a different truth than the other.
How it fits together
A single Node.js backend owns all booking and table state. Clients mutate state through REST endpoints; every accepted mutation is broadcast as a typed Socket.io event to all subscribed clients — the staff app and the admin platform hold local Redux stores that are written to by events, not by refetching.
The timeline view renders straight from that store: tables as rows, reservations and live sessions as ranges on a time axis. Because the store is event-fed, the timeline is as current as the last broadcast, on every screen, at the same moment.
The admin platform ships as a Progressive Web App, so owner and staff devices get install-free access without an app-store release cycle.
Solo build — greenfield. Nothing here was inherited; every layer below is mine to defend.
- System design: the decision to make the event layer the source of UI truth, and the REST-mutate / event-distribute split.
- The React Native staff app — table operations, orders, kitchen flow.
- The Next.js admin platform and its live reservation timeline.
- The Node.js backend, the Socket.io event contracts, and the PWA build.
- Deployment: hosting and domain configuration, and post-launch debugging with the client.
Reservations
- Live timeline-based reservation view — table availability and booking details at a glance, replacing static slots.
- Real-time propagation: bookings, extensions, and cancellations appear on all connected clients as they happen.
Floor operations
- Table lifecycle management from the staff app.
- Order and kitchen flows tied to the table a session belongs to.
Access
- Separate surfaces for separate jobs: a phone-first app for staff, a dashboard for owners.
- PWA build for install-free access across devices.
- React Nativestaff work standing up, on phones — the floor tool had to be an app, not a responsive page
- Next.jsthe admin platform and the PWA build share one codebase
- TypeScriptthe event contracts cross three runtimes; types are what keep them honest
- Redux Toolkitone client store that Socket.io events write into — the UI never asks twice
- Socket.iothe headline decision: push state changes instead of letting every client poll
- Node.jsbaseline choice
rationale is given only where the choice wasn't the obvious one.
Where it got difficult
Two clients, one truth
- symptom
With each client fetching on its own schedule, the staff app and the admin panel could legitimately disagree about the same table — not through a bug, but by design: polling makes staleness a per-client property.
- investigation
The options were to poll faster — cost scales with clients × frequency, and the gap never actually closes — or to invert the flow entirely and have the server announce changes.
- root cause
Booking state lived in one place but was consumed via N independent snapshots. The architecture, not the code, was what made the views diverge.
- fix
A Socket.io event layer as the only path by which state reaches a screen. Clients mutate over REST; the server validates, commits, and broadcasts; every connected store applies the same event. The timeline is a projection of the event stream, not a cached query.
- tradeoff
Persistent connections are a liability you carry: reconnection needs a resync path (snapshot fetch, then resume events), and the server now owns delivery, not just data. Accepted — sync correctness was the product.
A timeline is a harder UI than a grid
- symptom
The slot grid it replaced was trivially easy to render — and wrong. A timeline has to show reservations and in-progress sessions as ranges against real clock time, on a phone screen, while the data underneath it moves.
- investigation
The hard cases are visual, not computational: a session running past its booking, a reservation starting mid-view, updates landing while a staff member is mid-scroll.
- root cause
Time is continuous and slots are discrete — the entire reason the grid failed is the same reason the timeline is harder to draw.
- fix
Render ranges from the event-fed store against a time axis, and let updated events restyle in place rather than re-layout, so the view stays stable under live changes.
- tradeoff
More rendering logic living on the client, and a component that needs real care on small screens. The grid was cheaper; it just wasn't true.
- Push-based sync doesn't delete the hard part, it relocates it: the complexity you remove from polling reappears in the reconnect path. Budget for it on day one.
- On a solo project the scarce resource is attention. Every boring infrastructure choice — managed hosting, one database — was a deliberate purchase of attention for the timeline, which is the part users judge.
- Version the Socket.io event payloads — right now a client and server must deploy in step, and that coupling will bite as the surface grows.
- Persist an event log server-side for replay and audit, which would also simplify the reconnect/resync path.
- Load-test the socket layer against the venue's peak concurrency before scaling to more locations, rather than after.