Home › Our Work › VenueHarbour
A complete restaurant operating system built from scratch for La Mar restaurant in Marbella — covering guest bookings, real-time floor plans, kitchen orders, table-side payments, staff attendance with facial recognition, and loyalty programmes. Designed to licence to other venues as a SaaS product.
69
Database Tables
~100
Vue 3 Components
60+
Service Classes
15+
Sprint Cycles
The Challenge
La Mar, a restaurant and bar in Marbella, ran on disconnected phone bookings, paper rotas, and separate payment systems. They needed a unified platform where guests book online with Stripe card holds, kitchen staff work from a live display, floor managers see real-time table states, and the whole operation connects from booking through to fiscal compliance.
Nothing off-the-shelf could handle the combination of Spanish Verifactu tax law, multilingual guest flows, real-time Pusher-powered floor plans, and the ambition to licence the system to other venues under the VenueHarbour brand. Every component had to be engineered from scratch — purpose-built for the way La Mar actually operates.
What Was Delivered
Tech Stack
Backend
Frontend
Mobile
A separate Android staff app gives venue staff offline-first access to bookings, orders, checks, and the KDS from their mobile device. Built with React Native, Expo, and WatermelonDB — delta sync keeps local data current without a full download on every shift.
Database
Real-time
Payments
Biometric
Passes
Hosting
PDF / QR
Key Features
Staff tap their face at a tablet kiosk to clock in and out. Powered by Azure Face API with a 0.85 confidence threshold. Critically, only a person_id is ever stored — no raw biometric image or facial embedding touches the database. This is the hard architectural guarantee: biometric data stays inside Azure, and our system only ever sees an opaque identifier it cannot reverse-engineer.
A PIN fallback with bcrypt hashing and a 5-attempt lockout ensures access is never blocked if lighting or camera angle causes a failed match. Fully GDPR-compliant by design — not by policy — because there is simply nothing biometric to breach.
Subscription passes are issued as native .pkpass files (Apple Wallet) and Google Wallet JWT tokens. The QR code inside regenerates every 60 seconds using TOTP (RFC 6238 — the same standard as two-factor authentication apps like Google Authenticator).
A screenshot of someone else's pass is worthless 60 seconds later. Replay attacks are blocked by the lm_pass_totp_log table which records every scan with a timestamp — if the same TOTP window is presented twice, it is rejected regardless of how fresh the code appears.
Managers build their table layout with a drag-and-drop builder. Staff see a live view where table tile colours shift from red → green as KDS tickets are completed — giving a visual pulse of how busy each table is and where they are in their meal. Powered by Pusher private channels, updating within milliseconds of a kitchen action.
Orders from the waiter app appear instantly on the KDS. A 4-state lifecycle (new → preparing → ready → completed) with compare-and-swap concurrency protection ensures two kitchen staff can't accidentally bump the same ticket simultaneously. An offline queue backed by Dexie.js handles network disruptions without losing a single order.
Bookings capture a card hold, not an immediate charge. The hold is released on cancellation or captured at arrival. Every EU transaction enforces 3D Secure. Idempotency keys (lm_pi_{booking_id}_{action}) prevent duplicate charges even if the Stripe webhook fires twice. Holds approaching the 7-day limit are automatically re-authorised via WP Cron.
Every sale writes an immutable record to lm_fiscal_records with a SHA-256 hash chained to the previous entry — satisfying Spain's AEAT Verifactu requirements. S0 mode (hash-only) is live in production. S1 SOAP transmission to AEAT is code-complete, pending the mTLS certificate from the tax authority.
Full guest profiles: complete booking history, dietary and allergy flags, VIP status, internal staff notes, tags, and loyalty balance. Post-visit NPS emails fire automatically 24 hours after arrival. A 4-star or above response triggers an automatic prompt to the Google Reviews link — converting satisfied guests into public reviews without any manual follow-up.
An immutable, append-only loyalty ledger records every earn and redeem event — rows are never updated, only inserted. SELECT FOR UPDATE in every redemption transaction prevents concurrent requests from creating negative balances — the database-level equivalent of a mutex. Earn rates are configurable per venue, decoupled from the booking flow via WordPress action hooks.
System Architecture
Guest browser → Cloudflare (CDN + DDoS + SSL)
→ Google Cloud VM (Nginx + PHP 8.2)
→ WordPress MU-plugin loader (lamar-loader.php)
→ 60+ Service Classes → $wpdb->prepare()
→ MySQL VM (dedicated, 10.154.0.11)
// Vue 3 front-end layer
Vue 3 AJAX → admin-ajax.php
→ check_ajax_referer()
→ Service class
→ MySQL
// Real-time layer
Pusher private channels
→ Floor plan · KDS · Checks · Messaging
// Payment layer
Stripe → SCA/3DS holds → Webhooks
→ Booking lifecycle state machine
// Biometric layer
Azure Face API → person_id only
→ lm_staff_faces → clock_events
// Wallet pass layer
Apple Wallet (.pkpass)
Google Wallet JWT
→ TOTP (60s rotating QR)
→ lm_pass_totp_log (replay prevention)
Engineering Decisions
No raw biometric data stored
Azure Face API person_id only — GDPR compliance by architecture, not policy. There is nothing biometric to breach.
Pusher fire-and-forget via wp_remote_post
Non-blocking — Pusher events are dispatched without waiting for a response, so real-time updates carry zero latency overhead on the request that triggered them.
SELECT FOR UPDATE on all loyalty redemptions
Database-level mutex — not an application lock. Concurrent redemption requests queue at the row level; no race condition can create a negative loyalty balance.
TOTP passes use RFC 6238
The same standard as Google Authenticator and Authy — a proven, time-tested algorithm for time-based one-time passwords with robust replay protection.
Stripe idempotency keys on every mutation
Safe webhook retry by design. If Stripe fires a webhook twice due to a timeout, the second call is rejected at the API level — the charge never happens twice.
PHP MU-plugins as application framework
WordPress is the infrastructure layer — database, auth, cron, admin. The 54 service classes are the application. No plugin activation required; the system loads unconditionally on boot.
Scale at a Glance
A genuinely large-scale PHP application — not a theme with some plugins on top.
69
Database Tables
2,400+
Translation Keys
90+
DB Migrations
5
Guest Languages (EN/ES/FR/DE/NL)