Skip to content
Lumis Studios
All Work
Marketplace

RoaVa

Discover, book, and experience day-trips near Nairobi.

2026

Visit live site

Overview

Booking a local experience in Kenya means DMs, manual M-Pesa confirmations, and operators tracking guests on paper. RoaVa turns it into a two-sided marketplace: operators list experiences with recurring availability and self-manage their slots, while consumers discover, book a specific dated slot, pay via M-Pesa STK push, and receive a single-use QR ticket. It is built for the realities of the market — a mobile-first PWA tuned for low-end Android and metered 4G, bilingual English and Kiswahili, and a deliberately non-custodial payment model that collects through a licensed provider and disburses the operator's share, retaining only commission, so no funds are ever held in escrow.

Our Role

Designed and built the entire platform end-to-end — the Supabase data model and row-level security, the database-enforced money path, M-Pesa collections and operator payouts, signed offline QR ticketing, the operator self-serve tools, and the consumer-facing PWA.

Tech Stack

Next.js 16React 19TypeScriptSupabasePostgreSQLTailwind CSS v4IntaSendM-PesaPWA

Key Features

Atomic Slot Booking

The bookable unit is a slot — a specific date and time with its own capacity. Reservation is a single conditional UPDATE in PostgreSQL that only succeeds if capacity remains, so two people racing for the last seat can never both win. A database CHECK constraint makes overselling impossible even if application logic errs.

M-Pesa Pending-Until-Callback

Bookings collect via IntaSend M-Pesa STK push. Price is always recomputed server-side, capacity is held, and a pending booking is created — but it is confirmed only when the payment callback arrives, never on the request-accepted response. Failures release the hold automatically.

Idempotent Payments & Reconciliation

A single idempotent entry point handles webhooks, the reconciliation poll, and mock triggers alike, with idempotency enforced in the database via row locks and state guards. A daily cron sweep re-checks stuck payments and expires abandoned holds, so a dropped callback never orphans a paid booking.

Signed Offline QR Tickets

Each ticket is an HMAC-SHA256 signed payload rendered as an inline SVG and cached by the service worker, so it displays offline at the gate. Check-in is an atomic database update that marks the ticket used in one step and verifies the scanning operator owns the experience — blocking screenshot resale and double-entry.

Operator Self-Serve & Payouts

Operators onboard, create and edit experiences, manage images and recurring slots, view a guest manifest, and scan guests in. The non-custodial model's disbursement half mirrors collection: a per-booking payout ledger settles the operator's net share via M-Pesa B2C, confirmed only on the payout callback.

Discovery & Diaspora Gifting

Search and filter experiences by category, county, price, date, and party size, with reviews restricted to consumers' own completed bookings. A gift flow lets buyers purchase an experience for someone else, who claims it with a short shareable code that moves the ticket into their wallet.

How It Was Built

RoaVa is a Next.js 16 App Router PWA on Vercel, backed by Supabase for Postgres, Auth, and Storage. All money-touching logic is pushed into the database as SECURITY DEFINER Postgres functions — reserve_slot, confirm_booking_payment, check_in_ticket, initiate_payout, claim_gift — revoked from public roles and callable only by the service role, so atomicity and idempotency live in the database rather than application code. Row-level security is default-deny on every table, with non-recursive SECURITY DEFINER helpers and privilege-escalation guard triggers that stop a consumer from granting themselves admin or a verified badge; payout phone numbers are split into a separate non-public table since operator profiles are world-readable. Payments, payouts, and SMS each sit behind a provider interface with a mock implementation, so the full pending-to-callback flow runs locally without live credentials. Auth is passwordless — phone OTP, email OTP, and Google OAuth. Performance is treated as a feature: a low-end-Android and metered-4G budget, skeleton loaders, sharp-compressed images, service-worker offline tolerance, a rate limiter on booking initiation, and cookie-driven English/Kiswahili dictionaries read in server components.

Highlights

  • Atomic capacity reservation plus a database CHECK constraint — overselling a slot is impossible
  • M-Pesa via IntaSend with pending-until-callback confirmation and server-side price recomputation
  • Idempotent payment webhooks with a daily reconciliation cron so dropped callbacks never orphan bookings
  • HMAC-signed single-use QR tickets that work offline and verify operator ownership at check-in
  • Non-custodial pass-through model: collect, disburse the operator's net, retain commission — no held funds
  • Default-deny RLS with privilege-escalation guard triggers and money writes locked to the service role

Build with us

Want something like this for your business?

We build web apps, mobile apps, and cloud infrastructure for African businesses.