The wifi died
at the door.
People still got in.
Cackle sells your tickets online, then checks them at the door on an ordinary phone. When the venue's internet drops — and one night it will — the queue keeps moving.
Try it. Then switch the internet off.
This is the real decision the scanner makes, wired to the same rules as the app. Press scan a few times. Then turn off the venue wifi, turn off your server, and press it again — nothing about the answers changes. That is the whole product in one control panel.
A faithful illustration of the decision ladder in web/src/lib/scan-decision.js, which mirrors Go's scan.DecideWithBundle check for check. The switches change the status line and the sync queue. They change nothing about whether the door opens — that is the entire point.
Four things happen. Only two of them need the internet.
You need a connection to sell a ticket, and you need one afterwards to see what happened. In between — the part where four hundred people are standing in a field waiting to get in — you need nothing at all.
You sell tickets
Make an event, add the tiers you actually sell — early bird, general, VIP — with their own prices, caps and cut-off times. Publish it. People buy, and each one gets a QR code on their phone.
Each phone downloads once
Before doors open, every scanning phone pulls one small file: the event, who is allowed in, and who has already come through. A few hundred kilobytes. It is the last thing the phone has to ask anyone for.
The internet dies. Doors keep moving.
Scan, answer, admit. Every check happens on the phone itself, against the file it already holds — so a dead router, a dead uplink or a dead server changes nothing at the front of the queue.
Everything catches up
When signal comes back, each phone uploads what it did and the gates merge into one record. Nothing overwrites anything: every device's answer is kept exactly as it was given, so the numbers can be argued with.
Your guests see three screens. You see the rest.
Every picture below is a real capture from the running app on a seeded database, in whichever theme you are reading this page in. Nothing here is a mockup.
Pick a ticket, pay, done
Priced in the event's own currency, checked against what is actually left. No account to create before they can hand you money.
The ticket is the QR code
That square is the whole ticket. It proves itself to the scanner — nothing has to be looked up anywhere for it to be believed.
Still there next week
Everything they have bought from you, in one place, ready to show at a gate. Screenshot it if they like — the gate will still catch a copy that has already walked in.
What sold, who came in
The organiser's home page. Your next events, the money taken, and the count at the door, without opening a spreadsheet.
One event, in numbers
Sales per tier and admissions per gate for a single night — the two numbers you get asked about at eleven o'clock.
Everyone, and what the door said
Every ticket holder with the answer each phone gave, kept word for word — including the awkward ones. This is the list you open when somebody disputes being turned away.
Look at any other screen
Four answers, readable at arm's length.
Door staff have three seconds, a queue, bad light and a phone held at hip height. So the screen floods with one colour, one icon and one instruction — and the phone buzzes a different rhythm for each, because colour alone fails exactly the people and the conditions a gate has to work for.
The one thing to plan around: two offline doors cannot talk to each other.
If someone tries the same ticket at your north door and your south door while both are offline, both gates will open. Cackle shows you that it happened once the phones reconnect — it cannot stop it at the second door. Cross-gate double-scan is detected, not prevented.
That is not a gap in this particular software; it is what "offline" means. Two devices that are not talking cannot agree about anything at the moment a ticket is presented, and no clever merging invents a conversation that did not happen.
What Cackle does instead is refuse to hide it. Every phone's own answer is kept word for word, and the conflicts report — GET /api/events/{id}/admission-conflicts — lists every ticket that got in more than once and how many extra people that let through. Re-downloading the file mid-event narrows the window. Linking two nodes narrows it further. Neither closes it, and nothing here will ever tell you it does.
If you need a hard cap on bodies through a door, you need a door that is online — or you accept the trade knowingly, which is what most festivals in most fields already do.
The money goes from your buyer to you.
Cackle never holds your funds. There is no escrow, no platform wallet, no waiting period and no cut. It creates the order, hands the payment to whichever method you chose, and writes down what came back — the money moves between the buyer and your account, never through one of ours.
Cash, transfer, at the door
Built in everywhere and impossible to switch off. No keys, no accounts, no network call: they pay you however you already get paid, and you mark the order as settled. This on its own is a complete way to run an event.
- no keys
- no network
- no account
Cards and local providers
Paystack is built in, including payouts. Around twenty more processors — Stripe, Adyen and the rest — arrive through patala, the shared payment layer this suite uses, on an opt-in build.
- paystack
- stripe
- adyen
- +17 via patala
Stablecoins and Lightning
A built-in watcher settles orders by watching an address that belongs to you. BTCPay and lnbits come through the same opt-in build. The keys stay yours throughout — Cackle only ever reads confirmations.
- stablecoin
- btcpay
- lnbits
No favourite country or currency
Currency is set per event and defaults from your organisation. Run events in EUR, INR and JPY at the same time and each one keeps its own. Cackle was built in South Africa and used to read that way; that framing was wrong and has been taken out of the code as well as the copy.
Amounts are whole numbers, always
Every price is stored as whole cents plus a currency code — never a decimal that can drift. Yen has no cents and Kuwaiti dinar has three; getting that wrong overcharges someone a hundred times over or sells your night out for one percent of its price, so Cackle refuses to guess at a currency it does not know.
Run one test payment before you sell anything through a card or crypto provider.
None of the card or crypto connectors have been run against a live merchant account or a provider sandbox from this repository. They are ported from tested code and covered by unit tests that prove they handle the documented shapes and refuse a bad signature, a wrong amount or a timeout — that is not the same as proving they talk correctly to the live provider. Put one real transaction through end to end first. The cash-and-transfer method needs no such check, because it makes no call at all.
One file. Your laptop, or your cloud.
Cackle is a single program with the database and the whole website already inside it. There is nothing else to install, nothing to sign up for, and nobody to ask. Copy the file onto a machine and start it.
A laptop in the venue
The machine most resistant to your venue's network is the one standing inside it. Run Cackle behind the box office and the gates reach it over the local network — or over nothing at all, which is the design.
A small cloud box
One modest VM, one disk, one process. The database file is the whole thing: every event, ticket, key and order. Back that one file up and you have backed up everything.
Two of them admissions only
A venue machine and a cloud machine can share their door records with each other, so the conflicts report reads the same wherever you open it and each side learns sooner about admissions at the other.
Only door records travel. Events, tickets, orders and keys do not. It is not a backup, and it does not prevent a double admission either.
# try it, fully seeded, zero setup git clone https://github.com/vul-os/cackle.git cd cackle && make build ./cackle --demo # → http://localhost:8080 # or run it for real docker run -d -p 8080:8080 \ -v cackle-data:/srv/data \ -e CACKLE_BASE_URL=https://tickets.example.com \ vulos/cackle
Your signing keys stay locked
The keys that sign your tickets are encrypted in the database. Without the unlock material the server refuses to start rather than quietly running without them. There is no fallback mode.
Every event gets a page
A real public page from the moment the event exists. Organisers can supply their own, or take the public data and render the event on their own website instead.
Open source, both licences
MIT or Apache-2.0, your choice. Releases are signed, and a script with a fail-closed test matrix checks them — a tampered download does not verify.
It never calls home
No tracking, no analytics, no licence check, no default connection to anything — including anything of ours. Running it on a machine with no internet at all is a supported way to run it.
A ticket is a signed capability, not a database lookup.
Everything above is true in plain language. Here is the same thing in the words an engineer evaluating this will want, because the trick is not complicated and it should be possible to check.
-
The QR carries the whole claim
Three parts:
cackle, a payload, and an Ed25519 signature over that payload. Nothing is stored elsewhere that the gate needs. - Each event has its own key pair There is no global signing key. A token names the key that signed it, so rotating one does not invalidate tickets already issued, and a leaked key is one event's problem rather than the platform's.
- The scanner pins the public key It arrives in the bundle before doors, so verification is a local signature check against material the device already trusts — no lookup, no round trip, no server in the room.
- A signature alone is not enough It only proves the ticket was issued once. The bundle also carries the set of tickets currently valid, so a refunded or voided ticket is refused offline, and a cancelled event admits nobody. That set is a snapshot as of the pull — re-pull at shift changes and it gets fresher.
-
Two verifiers, one frozen corpus
Go's
scan.DecideWithBundleand the browser'sscan-decision.jsare independent implementations held to the same conformance vectors in CI, so the two cannot quietly disagree about who gets in.
The token shape
Specified precisely enough to write a third verifier from, and documented in the ticket format chapter.
The decision ladder
Signature, then event, then revocation, then duplicate — in that order, all against the bundle already on the device. The demo above walks the same rungs.
Admissions are appended to a local log and uploaded when there is signal. Every device's verdict survives the merge verbatim, next to the reconciled one.
Nothing is lost quietly
A duplicate that got through is a row you can find, not a number that silently disagrees with another number. That is the difference between a system you can audit and one you have to trust.
If a phone is dropped in a puddle you lose that device's log. You never lose the ability to run the door.
Every claim on this page, with its status attached.
A venue plans its staffing and its refunds around what a system promises, so a marketing page that rounds "detected" up to "prevented" costs somebody a real evening. Here is the whole list — including the things that are not built, and the one thing that never can be.
cackle.<payload>.<sig>, Ed25519. Specified precisely enough to write a third verifier from, with a frozen conformance corpus two implementations are held to in CI.manual rail — cash, transfer, door Built--demo mode Built