Skip to content
Ballo Innovations2026 — present
In buildarchitecturepaymentsmobile

Octo

Event ticketing for venues where the network drops exactly when you need it

A ticketing platform for Zambian events — selling through mobile money, and admitting people through QR gates that keep working when the venue's mobile network saturates. Most of the engineering is in the two places where the network cannot be trusted: taking money, and letting someone through a door.

Octo interface

StatusServer-side sale, payment and reconciliation paths built; scanner app still a scaffold.

Built for
Ballo Innovations
My role
Architecture and backend implementation — payments, ticket integrity, and the offline scan reconciliation model.
When
2026 — present
  • TypeScript
  • tRPC
  • Hono
  • Prisma
  • PostgreSQL
  • Firebase Cloud Functions
  • DPO Pay
  • HMAC-SHA256
01Look & feel

Screens and demos

What it looks like in use — not just what it was designed to do.

  • Octo public ticketing landing page
    Public web — Lusaka365 Tickets
  • Trending events and ticket cards on Octo
    Event discovery
02The engineering

What made it hard

The problems worth reading about. Everything else in this system was ordinary work.

  1. 01

    Gate scanners have to admit or reject with no connectivity at all, so the server cannot assume it sees scans in the order they happened. Scans are re-sorted by the device's own clock and resolved first-scan-wins — but a decision a gate already acted on is never reversed, because a person who is already inside cannot be un-admitted by a database write.

  2. 02

    Nothing marks an order paid except a server-to-server confirmation with the payment gateway. A browser redirect is client-controlled, and the client is exactly who has the incentive to lie about having paid.

  3. 03

    A ticket carries its own proof — a payload plus an HMAC signature a scanner can verify offline. That requirement is also why the signing key lives in Postgres rather than a KMS: a wrapped key cannot be handed to a phone.

03Numbers

What it measures

Each figure says where it came from, so you can judge how much weight to give it. Some are measurements and some are chosen thresholds; the basis line tells you which.

0
Oversell under concurrent reservation

200 parallel single-seat reservations against a capacity of 100 yielded exactly 100 successes and 100 sold-out errors, 20 runs out of 20, measured against live Postgres.

3
Independent guards against double payment

An early already-completed check, an atomic conditional update on the order's status, and a final check for tickets already minted.

500
Maximum entries per sync batch

Schema bound on the sync request; sequential resolution is part of why it is capped.

Write-up

The longer version

The problem

Octo sells tickets for events in Zambia — open fields and large halls around Lusaka, where a few thousand people arriving at once will saturate or entirely sink the mobile network. That single fact drives most of the architecture. Payment happens on a connection that drops mid-checkout, and admission happens on devices that may have no connection at all for the length of the event.

Both are cases where the easy implementation is quietly wrong rather than visibly broken, which is what made them worth writing down.

What I built

The sale and payment paths, the ticket integrity scheme, and the server side of offline scan reconciliation. Tickets are minted only after the server has independently asked the gateway whether money arrived, and each one carries a signed payload so a scanner at the gate can verify it with no network call.

What is not built yet

The scanner application is a scaffold. The on-device database, the local signature verification, the clock discipline and the background sync queue are specified and still on the backlog, so what I can defend in detail is the reconciliation policy rather than a shipped end-to-end offline gate. Scan submission is also not yet authenticated: possession of a validly signed QR is currently the de facto authorisation.

There is one more open gap on the payment side. If the gateway's callback never arrives at all, the order sits pending forever — it has an expiry timestamp written to it and nothing reads that timestamp. The reconciliation sweep that fixes it is designed and not built, and it is the next thing I would do before this handles real volume.

Decision records

The close calls, written up

Where a choice was genuinely arguable, I wrote it up the way a team records a design decision internally: the constraint, the options I turned down and why, and what it would cost to reverse.

  • ADR-006acceptedPayments19 Jul 2026

    Trusting only server-to-server payment confirmation

    A payment gateway redirect is client-controlled and trivially spoofable, so the only thing allowed to mark an order paid and mint tickets is a server-to-server confirmation call that gets retried, deduplicated at three separate points, and still leaves one real gap I have not closed: nothing yet re-checks an order that the gateway never called back about.

    3 options evaluated

  • ADR-005acceptedTicket Integrity26 Jul 2026

    Where the ticket signing key lives

    A ticket is a plain payload plus an HMAC signature, and the obvious place to keep the signing key is a secrets manager. I put it in a database row instead, because the same key eventually has to be provisioned onto a scanner device, and a wrapped KMS key cannot be handed to a phone. That trade is real, I flagged it in the code the day I made it, and it is still open.

    3 options evaluated

  • ADR-003acceptedDistributed Systems27 Jul 2026

    Offline gate scan reconciliation

    Two gates scan the same ticket fifteen seconds apart, neither is online, and both let the holder through. When the batches finally reach the server they arrive in the wrong order. The server re-sorts by the device's own clock and resolves first-scan-wins — but it will not reverse a decision a gate has already acted on, because a person who is already inside cannot be un-admitted by a database write.

    3 options evaluated