Cackle.
Ticketing you run yourself

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.

0network calls needed to let someone in
1file to copy onto a machine — that is the install
0%of your ticket money. Cackle never touches it
A phone at a gate with no signal, still reading ADMIT Night at a venue door. Light spills from an arched doorway and three people wait inside it, behind a rope drawn as a ticket perforation. To the right stands a phone whose screen has flooded green and reads ADMIT, General Admission, no signal needed. A badge in the corner shows the wifi symbol struck through and labelled NO SIGNAL. NO SIGNAL ADMIT General Admission no signal needed
The door is not asking anybody's permission. Everything the phone needs to answer was downloaded before the first guest arrived.


Made for a music venue a conference a church hall a night market a school fête

At the door

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.

Gate 1 — North EntranceKaroo Rock Revival Online

Point it at a ticket. Try switching the venue wifi and the server off first.
Scanned here: 0 Waiting to sync: 0 Reporting live

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.

/admin/scanner
The Cackle gate screen: every event the organiser can work, each with a Download button to cache its scan bundle and a Scan button that opens the camera once it is cached.
And this is the real screen it starts from, captured from the app: every event downloaded once, then scanned with the network off. Tap "Scan" and the camera and verdict flood above are what opens.

A night, start to finish

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.

Coupon 01

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.

Needs internet
Coupon 02

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.

Needs internet · once
Coupon 03

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.

Needs nothing
Coupon 04

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.

Needs internet · afterwards

What it actually looks like

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.

/checkout
Cackle checkout: the chosen tickets, the total in the event's own currency, and the payment options the organiser turned on.
They buy

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.

/ticket/{id}
A buyer's ticket page: a large QR code, the event name and date, and which tier the ticket is for.
They hold

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.

/tickets
A buyer's ticket wallet listing every event they hold tickets for, each with its date and venue.
They keep

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.

/admin
The organiser dashboard: upcoming events, what has sold, and how many people have come through the gate.
You run it

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.

/admin/events/{id}/stats
Per-event statistics: sales by ticket type and admissions counted at the gate.
You check

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.

/admin/events/{id}/attendees
The attendee list for an event, showing each ticket holder and what happened when their ticket was scanned.
You settle it

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

/admin/scanner 01 / 12


The answers

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.

gate 1 · 21:04
LET THEM IN
Real ticket, right event, nobody has used it here yet.
one short pulse
gate 1 · 21:06
ALREADY SCANNED
A real ticket that has been through a door before. The screen tells you which one — this gate, or another.
two quick taps
gate 1 · 21:09
DO NOT LET THEM IN
Faked, edited, expired, refunded — or made up entirely.
one long buzz
gate 1 · 21:12
WRONG EVENT
A genuine ticket — for something else. They are at the wrong door, not holding a fake.
two long buzzes

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.


Getting paid

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.

Always on · the default

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
Optional · cards

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
Optional · crypto

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.


Yours to run

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.

your terminal
# 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.


How it actually works

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 anatomy of a Cackle ticket A drawn ticket with a dashed perforation down the right side. The main body carries the event name and tier; the stub carries the QR code that is the ticket itself. ADMIT ONE Karoo Rock Revival Sat 14 Mar · doors 19:00 General Admission THE TICKET
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. Two verifiers, one frozen corpus Go's scan.DecideWithBundle and the browser's scan-decision.js are 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.

cackle.eyJ2IjoxLCJ0aWQiOiIwMUpG….3Rk9mQ2xhaW1TaWc…

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.


Honest status

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.

Built in the tree, wired to a surface, covered by a test Note built, with a limit worth knowing Planned designed, not built Never impossible; will not be claimed
The gate
Offline admission, no network Built
Signature, event, revocation and duplicate checks all run on the device against the bundle it already holds.
Signed ticket capability Built
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.
Per-event signing keys, rotatable Built
No global signing key exists. Tokens name their key, so rotating one does not invalidate tickets already issued.
Keys encrypted at rest Built
Boot fails closed without the unlock material. A half-sealed database is never produced.
Refunded / voided tickets refused offline Built snapshot, not live
The bundle's valid-ticket set is authoritative as of when it was pulled. A refund issued after that reaches a gate only when it re-pulls.
Cross-gate double-scan detected Built
Every device's verdict is stored verbatim; the conflicts endpoint reports each ticket admitted more than once and the extra admissions it caused.
Cross-gate double-scan prevented Never
Two partitioned gates cannot coordinate at the moment of a scan, so both admit. No merge rule, CRDT or replication changes this, and nothing in the product or its docs claims otherwise.
Delegated capacity for sub-issuers Planned
A signed grant letting a disconnected device mint up to N tickets. The code is written and tested but wired to nothing — no route issues one and no bundle carries one. Plan your event as though it does not exist, because operationally it does not.
Selling
Events, ticket types, caps, sales windows Built
Draft → published → cancelled, with per-type pricing, quantity caps and sales windows.
Orders and checkout Built
Checked against live availability, priced in the event's own currency.
Money in minor units Built
Integers plus an ISO-4217 code, with the real exponent table. Never a float, never an assumed two decimals.
manual rail — cash, transfer, door Built
On in every deployment, cannot be disabled, makes no network call.
Paystack, native Built not sandbox-verified here
Includes the payout and banking calls that have no equivalent in the shared substrate.
Stablecoin watcher, native Built not sandbox-verified here
Settles by watching an address you control. Non-custodial.
~20 processors via patala Built opt-in build · not sandbox-verified here
Reached through the shared payment substrate on a build flag, not in the default binary — which needs no cgo and makes no payment call.
Cackle holding your money Never
No escrow, no platform wallet, no cut. There is no code path in which funds pass through Cackle.
Running it
Single binary, embedded database and UI Built
Pure-Go SQLite and the built frontend compiled in. No cgo, no external services.
--demo mode Built
Seeds an organisation, twelve events, ticket types, real sales and admissions, and an auto-settling provider.
Multi-node admission replication Built admissions only · not a backup
Operator-configured peers, no discovery and no default endpoint. Events, tickets, orders, users, keys, images and payments do not replicate; two nodes only converge on an event they both already hold.
Organiser host pages Built
A real default page per event, an optional structured page document rendered at its own URL, and the public JSON to render the event on your own site instead.
Host-uploaded HTML Never
One origin serves the organiser app, the API and the session cookie, so script in host content is account takeover, not a defaced page. You get typed blocks and a public API instead.
Signed releases, fail-closed verification Built
A verify script with a test matrix covering the ways a tampered or truncated artefact must fail.
Production-ready Planned
Cackle is experimental. The pieces above are built and tested, and it has not been run at scale by anybody. Treat a first event as a trial.
Telemetry or a call home Never
No analytics, no licence check, no default network call to any third party.