Cackle is events and ticketing that keeps working when the internet doesn't. An organiser creates an event, sells tickets, and staff scan them at the gate — and the gate keeps admitting people even if the venue's network, or the server itself, goes down mid-event. It runs as one program you can start on a laptop, with no cloud account and nothing else to install.
Getting started · Docs · How it works · Roadmap
Warning
This is experimental — not ready to run your real event on yet. Cackle is a ground-up rebuild of an earlier ticketing platform, and the pieces (the API, the database, the ticket format itself) are all still moving — expect breaking changes. Most payment options are built against the provider's own published docs and tested against a fake version of that provider, but have not been proven against a real one yet — see docs/PAYMENTS.md for exactly which ones, and don't take real money through an unverified one. In short: kick the tyres, don't stake a box office on it yet.
Picture the worst version of doors-open at a real event: the venue's wifi buckles under three hundred phones at once, or the server hiccups, and every other ticketing app goes quiet right when the queue is longest. Cackle is built so that moment is a non-event. Each scanner downloads what it needs once, before doors open, and after that it doesn't ask anyone's permission to let someone in — it checks the ticket itself, on the device, with no network required.
Under the hood: a Cackle ticket isn't a row in a database that a scanner has to look up. It's a small piece of data, cryptographically signed by the event's own key, that a scanner can check entirely on its own — the same way you can check a wax seal without phoning the person who made it. The Cackle server still handles selling tickets and reporting on sales, but on the night, it's optional. If the venue's wifi drops, or the machine running Cackle dies outright, every gate that already grabbed the event's data keeps admitting people, keeps its own record of who came in, and catches the server up once the network is back. (The engineering name for that little piece of signed data is a capability, and the exact format is specified byte-for-byte in docs/TICKET-FORMAT.md, for anyone evaluating this closely.)
Cackle doesn't assume you're in any particular country or that you use any
particular payment company. Currency is set per event, and the built-in
default way to take payment — called manual — is simply: the organiser
records that the money arrived (bank transfer, cash at the door, an invoice,
mobile money) and marks the order paid. No payment account to sign up for,
no API key, works anywhere in the world, and Cackle never holds your
money — it hands off to a payment provider (or to you, for manual) and
just keeps the record. Real processors — Stripe, Paystack, BTCPay, and about
twenty others — plug in as optional extras if you want them. See
docs/PAYMENTS.md for the full list and, importantly,
which of them are actually proven safe to use with real money today.
New here? docs/GETTING-STARTED.md walks the whole thing end to end — install, your first event, a ticket sold, a ticket scanned at the door — with a screenshot at every step.
| 🎟️ Offline-verifiable tickets | Every ticket is an Ed25519-signed capability, cackle.<payload>.<sig>, verified with a pure function — no database, no network, no clock but the one you hand it. This is what lets a gate keep admitting people with no connection. The format is specified as a wire format with frozen conformance vectors that both shipped verifiers (Go and browser) are held to in CI. See docs/TICKET-FORMAT.md. |
| 🚪 Offline gate scanning | A scanner pulls one scan-bundle while online (event details, issuer public keys, and a ticket index of what's still valid) and can then run the whole event unplugged. Admission dedupe is local and append-only — first scan wins, duplicates are recorded, never overwritten. It is per device: a second scan on the same scanner is refused at the door, while two offline scanners at two entrances only reconcile once they sync. See docs/OFFLINE-GATES.md for exactly what that does and doesn't stop. |
| 🏟️ Events & ticket types | Organisations own events; events own ticket types with pricing, quantity caps, sales windows, and per-order limits. Draft → published → cancelled lifecycle. |
| 🛒 Orders & checkout | Cart-style checkout against live ticket-type availability, integer minor-unit accounting in the event's own currency (money is never a float), per-order item breakdown. |
| 💳 Pluggable payments | A small Provider interface (Begin / Verify / Webhook / Capabilities) behind every charge. manual is the always-on default (no API key, works anywhere); 20+ optional adapters (Stripe, Paystack, BTCPay, LNbits, and more) are off by default and enabled per deployment via CACKLE_PAYMENT_PROVIDERS. Webhooks verify signatures and fail closed. Cackle never holds funds. See docs/PAYMENTS.md. |
| 👥 Org roles | owner / admin / scanner per organisation, checked server-side on every event/org route — including the ones an incumbent platform forgot to guard. |
| 📈 Live stats | Sold, revenue, admitted count, and a per-ticket-type breakdown per event. |
| 📦 One binary, one file database | modernc.org/sqlite (pure Go, no cgo) plus the built React app embedded via embed.FS. docker run -p 8080:8080 vulos/cackle is the whole install. |
Full gallery, including dark mode, in docs/SCREENSHOTS.md.
The condensed version — docs/GETTING-STARTED.md has the same steps with what to expect after each one, a troubleshooting table, and a full first-event walkthrough afterward.
git clone https://github.com/vul-os/cackle.git
cd cackle
docker build -t vulos/cackle .
docker run -d --name cackle -p 8080:8080 -v cackle-data:/srv/data vulos/cackle
# open http://localhost:8080Or build the real single binary yourself (make build builds the frontend,
embeds it into the Go binary, and outputs ./cackle) and run demo mode —
fully seeded, zero setup:
make build
./cackle --demo
# open http://localhost:8080go build -o cackle ./cmd/cackle also compiles (useful for fast
backend-only iteration), but without the embed_frontend build tag it
serves a bare dev fallback instead of the real UI — use make build (or
make build-backend if you specifically want the no-UI binary) to get the
one Go embeds the whole frontend into. See the Dockerfile
"EMBED CONTRACT" comment for why.
--demo boots with a seeded organisation, a published event, ticket types,
and the stub payment provider so you can buy a ticket and scan it end to
end without touching a real payment processor. See
docs/GETTING-STARTED.md for a real (non-demo)
setup, and docs/SELF-HOSTING.md for running it for a
real event.
Cackle ships no curl | sh installer — you download a binary from
Releases and run it, so the
verification step is yours to run. scripts/verify.sh
does it, and needs only curl and sha256sum/shasum:
curl -fsSLO https://raw.githubusercontent.com/vul-os/cackle/main/scripts/verify.sh
bash verify.sh --tag v0.3.0 --attest cackle-linux-amd64 # fetch + verify
bash verify.sh --dir ~/Downloads cackle-linux-amd64 # already downloadedIt fails closed. A missing, empty, HTML or malformed checksums.txt, a
manifest with no entry for the asset you asked for, a truncated download or a
digest mismatch each exit non-zero with their own diagnostic, and the partial
download is deleted. There is deliberately no --skip-verify and no path where
an absent manifest means "nothing to check" — a verifier that shrugs at a 404
prints a line that looks like verification while checking nothing, which is
worse than no verifier because it turns "I don't know" into "it's fine".
Every release also carries a sigstore build-provenance attestation, signed with
a short-lived certificate minted from the release workflow's OIDC identity —
no long-lived key, no secret, nothing to rotate. --attest checks it (needs
the gh CLI); without it the script prints that provenance was not checked
rather than letting a pass imply more than it checked.
The container image is attested by digest, which is what a mutable tag like
:latest cannot vouch for on its own:
gh attestation verify oci://ghcr.io/vul-os/cackle:v0.3.0 --repo vul-os/cacklemake release-guards (part of make check) runs the verifier's 24
synthetic-origin failure cases, so its refusals are exercised rather than
assumed.
Everything that matters for admission happens without the server: an event's issuer key is fetched once while a scanner is online, and every ticket after that verifies locally.
flowchart LR
subgraph online["online — happens once per event"]
direction LR
organiser["Organiser"] -->|creates event + ticket types| cackle["Cackle server<br/>Go + SQLite"]
buyer["Attendee"] -->|buys a ticket| cackle
cackle -->|"Ed25519-signs with the<br/>event's own issuer key"| capability["Ticket capability<br/>(the QR code)"]
cackle -->|"scan-bundle:<br/>issuer pubkey + ticket index"| scanner["Gate scanner"]
end
subgraph offline["at the gate — no network required"]
direction LR
capability -->|presents QR| scanner
scanner -->|"Verify(token, pubkey, now)<br/>pure function, no DB, no network"| admit{"valid?"}
admit -->|"this device's local admissions table<br/>first scan wins, duplicates recorded"| result["admitted / duplicate / invalid"]
end
scanner -.->|"sync admissions back<br/>once online again"| cackle
internal/tickets owns the capability format and is the one place signature
and expiry logic lives; internal/scan owns admission and the offline
dedupe table. Full write-up: docs/ARCHITECTURE.md.
Env-first, every key prefixed CACKLE_. Flags mirror the env vars; nothing
requires a config file.
| Variable | Default | Description |
|---|---|---|
CACKLE_ADDR |
:8080 |
HTTP listen address |
CACKLE_DB |
./cackle.db |
SQLite file path |
CACKLE_BASE_URL |
— | Public base URL, used in links and payment callbacks |
CACKLE_SESSION_SECRET |
auto-generated, persisted | Session signing secret |
CACKLE_PAYMENT_PROVIDERS |
unset (every registered provider enabled) | Comma-separated allowlist of optional payment providers for this deployment, e.g. manual,stripe,paystack. manual is always enabled regardless. |
CACKLE_DEMO |
false |
Boot fully seeded with demo data (same as --demo) |
Each optional payment provider has its own CACKLE_<PROVIDER>_* secrets
(e.g. CACKLE_STRIPE_SECRET_KEY, CACKLE_PAYSTACK_SECRET_KEY) — see
docs/PAYMENTS.md for the full per-adapter list and
docs/CONFIGURATION.md for the complete reference.
Full reference: docs/CONFIGURATION.md.
| Document | What it covers |
|---|---|
| docs/GETTING-STARTED.md | Clone to first ticket scanned: build, configure, run |
| docs/ARCHITECTURE.md | Layout, request flow, the contract behind every package |
| docs/CONFIGURATION.md | Every env var / flag, defaults, and what requires a restart |
| docs/API.md | The full HTTP API the frontend (and any client) codes against |
| docs/TICKET-FORMAT.md | The ticket capability wire format, precise enough to implement from, and why offline verification works — read this first |
| docs/ticket-format-vectors.json | Frozen conformance vectors for that format — what any new implementation must reproduce |
| docs/OFFLINE-GATES.md | Running a gate with no network: scan-bundle, allocations, sync |
| docs/FEDERATION.md | What two organisers' boxes may share, why tickets are always bought from the publisher's own box, and the discovery/central-index features this project will not build |
| docs/PAYMENTS.md | The payment provider seam, the full adapter table + verification status, manual by default, why Cackle never holds funds |
| docs/SCREENSHOTS.md | Full screenshot gallery and how to regenerate it |
| docs/SELF-HOSTING.md | Running Cackle for a real event: Docker, backups, TLS, scaling the gate |
| ROADMAP.md | What v1 ships, and what's deliberately deferred |
| CHANGELOG.md | Version history |
Prerequisites: Go 1.25+, Node 20+.
# Backend — note: NOT bare `./...`. web/node_modules can contain a stray
# vendored .go file (e.g. a nested npm package's Go port); scope to the
# real Go packages.
go vet ./cmd/... ./internal/...
go test ./cmd/... ./internal/...
# Frontend
cd web && npm install && npm run dev
# Frontend tests — the browser ticket verifier against the same frozen
# conformance vectors the Go one is checked against. node:test, no extra
# dependency; needs `npm install` in web/ first for @noble/curves.
cd web && npm testmake check runs the same gate CI does (gofmt + lint + tests + full build)
in one command.
# Regenerate screenshots (Playwright, builds the binary, boots --demo on :8087)
# — run from the repo root, where the screenshots script + its devDependency live
npm install && npx playwright install chromium
npm run screenshotsContributions are welcome. See CONTRIBUTING.md for the dev-environment setup, branch conventions, and what we say yes and no to. See SECURITY.md to report a vulnerability.
The mark in brand/ is the source of truth. Every icon this repo
ships — favicon, PWA and app icons, the mark in the README and on the site — is
rendered from brand/logo.svg rather than redrawn, so there is one approved
drawing and no second copy to drift.
Copy it outward, never edit a derived copy, and never edit brand/ to match
something downstream.
MIT OR Apache-2.0 — © VulOS. Cackle is a VulOS project; source and issues at github.com/vul-os/cackle.
![]()
vulos — open by design



