The first visible result of CrowdRelay ticketing is a QR code. That is also the least interesting part.

A QR code can contain any string. It does not prove that a ticket was paid for, issued to the right person, still valid, assigned to this event or not already used five minutes ago at another entrance. Treating the image as the ticket would move every important decision into the scanner — the least trusted and most chaotic part of the system.

The ticket is a state machine

The actual ticket is an admission pass with a lifecycle.

An order may be created and never paid. A payment webhook may arrive after the browser has closed. A pass may be issued, delivered, revoked, replaced or checked in. Staff may scan it several times because the connection was slow and nobody knows whether the first tap worked. Every one of those situations needs a real state, not another boolean called used.

Payment first, pass second

Payment confirmation is the beginning. The success redirect from the checkout page is not proof, because anybody can open a URL and call it success. CrowdRelay waits for the verified payment event, records it idempotently and issues the pass from durable order state. A repeated webhook returns the same result instead of creating a second ticket.

The QR carries less than you think

The QR contains an opaque pass reference and signed material, not private fan data. The scanner sends it to a staff endpoint, and the backend decides whether it belongs to the event, whether the pass is active and whether the current staff session has permission to check people in.

Staff is not admin

I separated scanning from administration too. The person standing at the entrance needs to validate and redeem passes. They do not need access to campaigns, exports, fan profiles or ticket configuration. “Staff” is not a smaller spelling of “admin”.

The door needs one atomic answer

The final check-in is atomic. Two devices may scan the same pass at almost the same moment. Reading unused, showing a green screen and only then updating the database would allow both through. The state transition therefore happens in one database operation: valid and unused becomes checked in, exactly once. The second scanner receives the existing result and the time of the first check-in.

That response matters operationally. “Invalid” and “already used at 19:42” are different situations at a door. So are “wrong event”, “revoked”, “not active yet” and “scanner not authorised”. The system should help staff make a decision, not turn every failure into a red rectangle.

The same boundary protects the website. The public Virya frontend can display a fan's pass through a private signed session, but it never receives CrowdRelay admin credentials. Staff tools use their own route and permissions. The browser presents the interface; PostgreSQL remains the authority.

Delivery can fail. The pass cannot disappear.

Delivery is asynchronous like the rest of CrowdRelay. Issuing a pass should not disappear because an email provider had a bad minute. The pass exists first, the outbox records that it needs to be delivered and a worker handles the external channel. A fan can still recover it from the private wallet.

I may eventually add more convenience around the door: better device handling, richer staff views and controlled degraded modes. None of that changes the core rule.

The QR code is a pointer.

The ticket is the state transition the backend is willing to defend.