A restaurant POS sounds simple until you're three days before launch and a manager says, "We need to split a table's bill by items, not just by percentage." That single sentence added two days of work. Hospitality software is deceptively complex — it sits at the intersection of human workflow, fiscal compliance, inventory management, and real-time kitchen coordination.
The fiscal day model
The most fundamental architectural decision in a hospitality POS is how you model time. In most software, a day is a calendar day. In a restaurant, a "day" starts when the manager opens the system and ends when they run the closing reports — which might be at 2 AM the next morning.
We built a fiscal_days table. Every transaction — every order, every payment,
every void — belongs to a fiscal day. A fiscal day is opened explicitly and closed explicitly.
Reporting runs against fiscal days, not calendar dates. This sounds like a small distinction
until you try to reconcile a late-night Saturday service using midnight-boundary reports.
CREATE TABLE fiscal_days (
id INT AUTO_INCREMENT PRIMARY KEY,
branch_id INT NOT NULL,
opened_by INT NOT NULL,
opened_at DATETIME NOT NULL,
closed_by INT NULL,
closed_at DATETIME NULL,
status ENUM('open','closed') DEFAULT 'open'
);
Kitchen display coordination
The kitchen display screen (KDS) is a monitor in the kitchen showing active orders. When a waiter fires an order, items appear on the KDS. When a chef marks an item ready, it disappears from the KDS and the waiter gets a notification.
I initially built this with page refreshes on a 5-second interval. It worked, but felt
laggy during service. The improved version uses PHP SSE (Server-Sent Events) for the
kitchen screen — a persistent HTTP connection that the server pushes events through.
No WebSocket server needed, no additional infrastructure. Just PHP, Apache, and the
browser's native EventSource API.
Recipe-level stock deduction
This is where most simple POS systems stop and where ours had to go further. When a customer orders a burger, you don't just decrement "burger" from stock — you don't stock burgers. You stock beef patties, burger buns, lettuce, tomatoes, and special sauce. Each sold item should decrement its recipe components from stock.
The recipes table links menu items to their ingredients with quantities.
Every sale triggers a recipe lookup and a batch of stock decrement operations within
a single transaction. If any decrement fails (negative stock), the transaction rolls back
and the manager gets an alert.
Multi-branch: the hardest part
When the client added a second branch, the complexity multiplied. Stock is per-branch. Fiscal days are per-branch. But menus, users, and pricing structures are shared. And the owner wants a single dashboard showing both branches' performance.
We kept everything in one database with a branch_id column on every
transactional table. The shared-resource tables (menu items, recipes, user accounts)
have no branch_id. The application layer filters by branch on every query.
It's simpler than separate databases and easier to report across.
What I got wrong first
Voids. I assumed voiding an order item would be rare and built it as a soft delete. In practice, voids happen constantly — wrong item ordered, customer changed their mind, chef ran out of an ingredient. A void needs to reverse stock, reverse the KDS, reverse the billing subtotal, and leave an audit trail. Implementing that properly on a soft delete with no event log required a significant rewrite two months in.
Build your void model from day one. Every mutation in a POS is potentially reversible, and the reversal logic is always more complex than the original operation.
In hospitality software, the edge cases aren't edge cases. They're Tuesday night service.
You can explore the Novara Restaurant App at novara.co.zw/demo.