QR ticketing

A ticket is a signed promise on the holder's phone — the gate's job is to verify the signature and spend it exactly once.

The idea

When you buy an event ticket, the system mints a small token, signs it with a secret key, and renders it as a QR code on your phone. At the gate, a scanner reads the code, checks the signature, and asks one question: has this ticket been used yet?

The two hard parts are authenticity (only the issuer can mint a valid ticket) and single use (one paid ticket admits one person, even if the image is screenshotted and shared). A signature solves the first; an atomic "mark as redeemed" solves the second.

Issuer signs with key Phone QR empty Gate scanner idle admit already used Redeemed set { }
A ticket starts life as a signed token, not a row anyone can forge.

How it works

The issuer never trusts the phone to be honest. It packs the ticket fields, appends an HMAC over them with a server-only key, and that signature is what makes the QR un-forgeable. Redemption is a single atomic write, so two scans of the same code can't both win.

def mint(ticket):
    body = f"{ticket.event}|{ticket.seat}|{ticket.id}"
    sig  = hmac_sha256(SECRET_KEY, body)      # only the issuer can produce this
    return base64(body + "." + sig)           # encode this string as the QR

def redeem(scanned):
    body, sig = split(decode(scanned))
    if sig != hmac_sha256(SECRET_KEY, body):
        return "INVALID"                      # forged or tampered
    # atomic: add_if_absent returns False if the id was already there
    first_time = redeemed.add_if_absent(body.id)
    return "ADMIT" if first_time else "ALREADY USED"

Note what is not here: no database lookup is needed to prove the ticket is real — the signature does that offline. The only shared state the gate needs is the small set of already-redeemed ids.

Trade-offs

ChoiceBuys youCosts you
Signed token, offline verifyGates authenticate without a live DB callCan't revoke one ticket without a deny-list
Central redeemed-setStrong single use across all gatesEvery scan needs a round trip; gate is online
Rotating, time-boxed QRScreenshots expire fast, harder to sharePhone must refresh; clock skew can reject valid holders

Watch out for

Worked example

Ticket SEAT-12B is minted: body show7|12B|9f2 plus its HMAC. A friend screenshots it and arrives first — the gate verifies the signature (valid), inserts 9f2 into the redeemed set (new), and admits them. When you arrive with the same image, the signature is still valid, but 9f2 is already in the set, so the atomic insert fails and the gate shows already used. One paid seat, one entry — exactly the guarantee we wanted.

Check yourself

Two gates scan the same QR at the same instant. What keeps both people from getting in?

Coach note: the signature proves the ticket is genuine, but only the single-write check enforces single use. Take another pass if the distinction feels slippery — it's the heart of the design.