Apps · Turnstile

The door that fails honestly

At 19:00 twenty thousand phones arrive and the venue's internet falls over — which is the moment every cloud ticketing scanner stops. Turnstile puts the whole ledger on every phone at the door: a scan is decided in the time it takes to look up a code, a ticket admitted at Gate B is dead at Gate F a moment later, and a gate that loses everyone keeps scanning, counts what it could not sync, and names the collisions when it is back.

0 ms waiting on a server every gate holds the ledger, so the verdict is a lookup, not a round trip
unsynced: 34 said on screen, in large type a gate with no network keeps admitting and shows what nobody has acknowledged, readable from three metres
1 chain per gate, in the file the night's record can be added to and not rewritten; a verifier recomputes it and can fail
How it works

Three kinds of no, and one kind of later

A steward does a different thing for each answer, so the screen says which one it is.

  • Malformed. Every code carries a check character. A typo is refused as a typo — never mistaken for a fake.
  • Unknown. A well-formed code that is not one of this event's tickets. The box office hands the list to every gate, so nobody has to ask.
  • Duplicate. "Already admitted — Gate B, 19:02." The gate, the time, on the screen.
  • Collision, later. Two gates that were apart both admitted the same ticket. Neither could have known. When they meet again, both name it — oldest admission first, so the copy is obvious — and nothing is dropped to make the report look clean.
Gate C, 19:14network gone
  ┌──────────────────────────────┐
  │           ADMIT              │
  │  GNW5-CQE5-J · admitted at C │
  └──────────────────────────────┘

  admitted   at this gate   unsynced
    1 204        212           34

  NO NETWORK · still scanning

  …19:31, back online:
  1 collision found on rejoin —
  GNW5-CQE5-J: Gate C 19:14:20,
  then Gate B 19:14:22
What is being sold

Not the scanning. The file.

Every admission, every collision, what was still unsynced when the night closed, and one hash chain per gate.

Closing the nightone tap
Entriesticket, gate, steward, time — from every gate, set-union, nothing reconciled away.
Collisionsthe admissions the gates could not have caught while apart. Six duplicates is something a door manager can act on; a clean report that was never true is not.
Chainsderived from the entries, one per gate. Edit one entry or remove one and the verifier says which gate and which link.
Receiptthe file's hash written to a signed, append-only chain on the platform when it is reachable — so the file cannot quietly change after the night either.

The sentence to say in the first meeting

The mesh survives your network dying. It does not survive your steward putting the phone in their pocket. When either happens we will tell you exactly what we could not catch — which is more than anyone currently selling you a clean-looking report will do.

Weakest point, stated

Not the ledger. The scan loop. Continuous barcode reading through a mobile browser, at night, on a three-year-old Android, for four hours — autofocus, thermal throttling, battery. Turnstile uses the browser's own barcode reader where it has one and the keyboard everywhere else, and says so rather than promising a native scanner it is not. And v1 carries gate-to-gate traffic through the box-office gate rather than a full mesh: a gate that loses that one gate queues and reconciles on return.

Pricing

What it costs

Commercial — per event

A licence per event, checked at the gate. Without one the gates scan exactly the same and the night's file is stamped licensed:false — a trial whose file looked identical to a paid one would be worth less than no trial.

How it's built

The part that matters, in the open

The rules live in a file with no browser and no network in it, so the collision case is a test, not a story.

js/turnstile-core.js
Ledger.prototype.merge = function (entries) {
    // Set union, keyed by (ticket, gate): the same ticket from a
    // different gate is KEPT — that is a collision, and collisions are
    // reported, never resolved by dropping one.
    var fresh = [];
    for (var i = 0; i < entries.length; i++) {
        var k = entryKey(entries[i]);
        if (this.entries[k]) continue;
        this.entries[k] = entries[i];
        fresh.push(entries[i]);
    }
    return fresh;
};

Taken from this app's source, not written for the page. Built on the Messaging Platform SDK.