Skip to content
Cackle

Cackle

The wifi died at the door. People still got in.

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.

License: MIT OR Apache-2.0 Go React Offline-verified Build

Getting started · Docs · How it works · Roadmap


The Cackle homepage — offline-verified tickets, browse events, and featured/upcoming listings

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.

What is Cackle, in plain terms?

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.

Features

🎟️ 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.

Screenshots

Cackle organiser dashboard showing ticket sales and admission stats
Organiser dashboard — sales, revenue, and admissions for an event
Cackle gate scanner screen showing a QR scan result
Gate scanner — admits, dedupes, and works with no network
Cackle public event page with ticket types
Public event page — ticket types and availability
Cackle attendee ticket with QR code
Attendee ticket — the QR is the signed capability itself

Full gallery, including dark mode, in docs/SCREENSHOTS.md.

Quick start (standalone)

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:8080

Or 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:8080

go 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.

Verifying a release binary

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 downloaded

It 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/cackle

make release-guards (part of make check) runs the verifier's 24 synthetic-origin failure cases, so its refusals are exercised rather than assumed.

How it works

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
Loading

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.

Configuration

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.

Documentation

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

Development

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 test

make 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 screenshots

Contributing

Contributions 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.

Brand

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.

License

MIT OR Apache-2.0 — © VulOS. Cackle is a VulOS project; source and issues at github.com/vul-os/cackle.


vulos
vulos — open by design

About

Events and ticketing in a single Go binary — Ed25519-signed tickets your gate can verify with no internet.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages