RianTosm
All projects

Full Stack · 2026

Tosm Cafe

A multi-branch cafe platform spanning three coordinated apps — QR-code table ordering for customers, a POS and back office for staff, and the REST API tying both to one branch- and role-scoped database.

tosm-cafe.tosm.netlify.app

Overview

What it is, and why it exists

Tosm Cafe is three apps built end to end around one Express/MongoDB API: a Next.js ordering site customers reach by scanning their table's QR code, a React/Vite back office cashiers and managers use for POS, menu, and reports, and the API itself, which enforces a strict owner → admin → staff role hierarchy with per-branch data scoping on every request. The three apps share one color palette, font system, and set of conventions so they read as a single product despite being separate deployments.

My Role

Full stack developer across all three apps — customer ordering site, back-office admin panel, and REST API — including role/branch-scoped auth, database modeling, and reporting aggregations.

Problem

The problem

Small multi-branch cafes typically run dine-in ordering over paper or WhatsApp and staff operations on a spreadsheet or a generic single-location POS, with no shared view of stock, revenue, or performance across branches — and most off-the-shelf ordering platforms either assume one location or require an ongoing subscription.

Solution

The solution

I built Tosm Cafe as three apps sharing one API: a QR-ordering site customers reach by scanning their table's code (no signup, just a phone number to look up their order later), a POS and admin back office cashiers and managers use per branch, and a role-scoped REST API (owner → admin → staff) that keeps every branch's menu, stock, and order data in one place. Stock, revenue, and reporting are branch-aware from the schema up, so an owner gets a cross-branch view while admin and staff only ever see their own branch's data — enforced server-side on every request, not just hidden in the UI.

Key Features

What it does

  • QR-code table ordering — scan, browse the menu, add to cart, and pay via a mock QRIS flow with no account required
  • Order lookup by phone number, with a 10-minute payment countdown and automatic stock release on expiry
  • Branch-scoped POS for cashiers — cash or QRIS checkout, receipt printing, dine-in and walk-in orders
  • Role-based back office (owner → admin → staff) with per-branch access enforced on every API request, not just hidden in the UI
  • Menu & category management with Cloudinary image upload, per-branch availability toggles and stock tracking
  • First-order discount engine (percentage plus an optional cap) applied automatically at checkout
  • Profit/loss reporting — branch comparison, revenue trend, expense breakdown, top-selling menu, and a peak-hours heatmap
  • Branch, staff, member, salary, and expense management, each scoped to the caller's role and branch

Tech Stack

Built with

Technology choices made to fit the product's real constraints, not novelty.

Next.jsReactTypeScriptTailwind CSSRedux ToolkitViteExpress.jsMongoDBJWTCloudinary
Architecture

System architecture

Customer ordering site (Next.js 16 + React 19)
Back office (React + Vite)
REST API (Express.js)
Mongoose / MongoDB Atlas
JWT auth middleware (role + branch scoped)
Cloudinary (menu & category image storage)

Challenges

Challenges & solutions

Reserving stock through a 10-minute payment window without leaking it

Placing an order decrements per-branch stock immediately, but the order can still expire unpaid — and that stock must be restored exactly once, whichever of two paths gets there first: the customer's client calling an expire endpoint the moment its local countdown hits zero, or back-office staff lazily triggering expiry when they next load the order list. I made "expired" a real stored status (not a derived one) precisely because restoring stock is a side effect that has to fire once, while a separate displayStatus still derives "failed" from elapsed time client-side so the UI reflects expiry instantly instead of waiting on that call to land.

One role hierarchy, enforced identically in three apps

Owner/admin/staff needed to mean the same thing in the ordering site, the back office, and the API — and a role or branch change made by the owner had to take effect immediately, not at next login. The API reloads the current user fresh from MongoDB on every protected request rather than trusting the JWT payload, and both frontends revalidate against /user/me on every full app load, so a revoked or reassigned user loses or gains access on their very next request instead of waiting for a token to expire.

Gallery

A closer look

QR table ordering & menu

Cart & mock QRIS checkout

Back office POS

Profit/loss & branch reports

Result

Outcome

  • Ships as three coordinated apps — customer ordering site, admin back office, and REST API — sharing one Mongoose backend and one branch-scoped role model
  • A customer goes from QR scan to paid order with zero account creation, the whole session keyed by a phone number instead of a login
  • Back-office reports (profit/loss, branch comparison, revenue trend, top menu, peak hours) each read from a dedicated aggregation endpoint scoped to the caller's role and branch instead of shipping raw order history to the client