World Vibe Web
The distributed app store for vibe-coded projects. Browse, discover, and install open-source apps from across GitHub.
Sell event tickets for £50 a year or £5 a month with no per-ticket platform fees. Includes payments, QR check-in, refunds, reporting and a hosted booking site.
CommerceTypeScript · GherkinGitHub @chobbledotcomOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
Chobble Tickets: Minimalist and private ticket sales on Bunny's Edge Scripts - a "no per-attendee fee" alternative to BookItBee, Eventbrite, Seetickets, etc
Translation pending
Taken from the repository README
Loading the full README. The saved excerpt is shown below.
Chobble Tickets is a reservation system that runs on Bunny Edge Scripting (or
any Deno environment) with libsql, which encrypts all PII at rest and handles
free and paid listings with Stripe, Square, or SumUp.
It is developed by Chobble CIC - a community interest
company, which means the assets are locked to the community and cannot be sold
off.
This is not "open core" - every feature is available under
AGPL-3.0-only with no proprietary add-ons. If you prefer not to host
it yourself, I offer hosted instances at
tix.chobble.com for £5/month or
£50/year.Read the full README ↗Chobble Tickets is a reservation system that runs on Bunny Edge Scripting (or any Deno environment) with libsql, which encrypts all PII at rest and handles free and paid listings with Stripe, Square, or SumUp.
It is developed by Chobble CIC - a community interest company, which means the assets are locked to the community and cannot be sold off.
Website: tickets.chobble.com
This is not "open core" - every feature is available under AGPL-3.0-only with no proprietary add-ons. If you prefer not to host it yourself, I offer hosted instances at tix.chobble.com for £5/month or £50/year.
This is the recommended way to deploy it. Fork the repo, connect it to Bunny, and deploy via GitHub Actions - you can pull in upstream changes and push your own customisations on your own schedule.
Fork or clone this repository
Create a Bunny Database in the Bunny dashboard - note the database URL and token
Create a Bunny Edge Script using your repository as the linked source
Add secrets to the script in the Bunny dashboard:
| Secret | Description |
|---|---|
DB_URL | Your Bunny database URL |
DB_TOKEN | Your Bunny database auth token |
DB_ENCRYPTION_KEY | 32-byte base64-encoded AES-256 key |
SCHEDULED_TASK_KEY | Unique 32-byte base64url maintenance key |
Add GitHub Actions secrets to your repository: BUNNY_SCRIPT_ID and
BUNNY_ACCESS_KEY
Pushes to main trigger the deploy workflow automatically. The database schema
auto-migrates on first request. Visit /setup/ to set your admin password and
currency.
For image uploads, also add STORAGE_ZONE_NAME and STORAGE_ZONE_KEY as Bunny
secrets. Configure an external monitor using the
scheduled maintenance guide. See the
CONFIG_KEYS reference
for all optional variables.
/ticket/listing1+listing2), one form, one checkout/ticket/:slug/qr) for posters and printed materials/pay/:token
link to pay off what they owe, no logindeno task verify:models checks the production-derived VAT machine with
Apalache. It checks the initial state and an unrestricted inductive step.
The arithmetic domain uses representative minor-unit amounts. These checks do
not prove database atomicity or provider behaviour. They do not establish legal
tax compliance.
/t/:token) with a QR code/t/token1+token2).pkpass files with listing details and barcodeAccept-Language headerI18N_REPLACEMENTS): substring replacements (for example
ticket|booking) that rewrite copy without touching HTML, links, or
interpolated values, applied once at loadeditor (edits
listings/site but holds no data key, so attendee PII stays undecryptable), a
delivery agent (locked to its own run sheet), and a door-only scanner
(checks people in and out, sees the doors page only)/admin/privacy): plain-language data-minimisation guidance,
automatic/manual purging of orphaned attendee records, and GDPR erasure of a
contact's recognition record by email or phoneDEMO_MODE=true): replaces all user-entered text with generated
sample data so demos hold no real PII/admin/guide) with FAQ for all featuresagent role): an auto-generated run sheet of today's
and tomorrow's drop-offs and collections with per-leg done toggles, walled off
from the rest of admin/renew
flow<script> and <iframe> snippets to drop the booking form onto an
existing site/order.js widget turns any data-add-listing link on the operator's own
site into a live multi-item cart, with per-origin CORS/custom.css for restyling public pages/feeds/listings.ics) for calendar apps/feeds/listings.rss) for feed readers/caldav/events.ics) of bookings for staff calendars,
filtered per agent so a driver sees only their jobscurrency (formats amounts) and pluralizeBOTPOISON_PUBLIC_KEY/BOTPOISON_SECRET_KEY and
submissions must also pass server-side verification/sitemap.xml./page/:slug link remains public and returns
X-Robots-Tag: noindex, nofollow./api/listings, /api/listings/:slug,
/api/listings/:slug/availability, /api/listings/:slug/book)Authorization: Bearer YOUR_API_KEY.
Owners create keys on the API Keys page under Users/admin/api-keys/docs for the complete generated reference, with request
and response examples for every endpointgroup_ids, or which groups include hidden ones — are answered in the
built-in admin guide's Admin API section (/admin/guide)source){
"address": "42 Oak Lane, Bristol, BS1 1AA",
"amount_owed": 0,
"business_email": "[email protected]",
"currency": "GBP",
"email": "[email protected]",
"name": "Alice Smith",
"notification_type": "registration.completed",
"payment_id": "pi_3abc123def456",
"phone": "+44 7700 900000",
"price_paid": 3000,
"source": "public",
"special_instructions": "Wheelchair access needed",
"ticket_url": "https://tickets.example.com/t/A1B2C3D4E5",
"tickets": [
{
"date": "2025-08-20",
"listing_name": "Summer Workshop",
"listing_slug": "summer-workshop",
"quantity": 2,
"ticket_token": "A1B2C3D4E5",
"unit_price": 1500
}
],
"timestamp": "2025-08-20T14:30:00.000Z"
}
Prices are in the smallest currency unit (for example pence, cents). The
source field says how the booking was made: public is a booking the person
made online, admin is a booking an admin added by hand, and unknown means
no stored contact history holds the answer. For
multi-listing bookings the tickets array contains one entry per listing and
the ticket_url combines tokens with + - one webhook, not several.
processed_payments
table.internal, and raw IPs rejected)These measures aim to raise the cost of common attacks. They do not guarantee security against all scenarios - proper operational practices (key management, access control, monitoring) are equally important.
The production platform is Bunny Edge Scripting (see above). One prebuilt OCI
image serves every other target. devenv.nix defines it as
outputs.tickets-image. The image carries the pinned Deno runtime, built static
assets, and server module cache. It runs as an unprivileged user on x86-64
hosts.
The Create Release workflow calls .github/workflows/publish-image.yml after
the GitHub release succeeds. Image publication requires main in
chobbledotcom/tickets. Each image uses the release run's exact commit and has
a sha-<commit> tag. Only GitHub's latest release can replace
ghcr.io/chobbledotcom/tickets:latest. Ordinary merges to main do not publish
images.
If image publication fails, the GitHub release remains available. Retry the
failed image job from the release workflow run. A superseded release can publish
its commit image, but it cannot replace latest.
GitHub creates the registry package as private on the first publish. A
repository admin must make it public once. Open the tickets package in the
chobbledotcom organisation on GitHub. Select Package settings, then Change
visibility.
docker run -p 3000:3000 \
-v tickets-data:/data \
-e DB_URL="file:/data/tickets.db" \
-e DB_ENCRYPTION_KEY="your-base64-key" \
ghcr.io/chobbledotcom/tickets:latest
The image uses a local SQLite file by default - set DB_URL and DB_TOKEN to
point at a remote Turso database instead if you prefer.
Deploy to: DigitalOcean | Render | Koyeb
Each platform's config in this repository (fly.toml, render.yaml,
.do/deploy.template.yaml) points at the published image. You can also deploy
with Fly.io (fly launch) or any Docker host.
DigitalOcean App Platform does not watch GHCR images. It redeploys a latest
image only when you ask it to: open the app, choose Actions, then Rebuild &
Deploy — or run doctl apps create-deployment <your-app-id>.
For the current repository layout and path conventions, see
REPO_STRUCTURE.md.
The screenshot runner starts a throwaway local site, completes setup, and fills
it with sample listings and attendees. It captures a 390 by 844px mobile
viewport at 2x device scale, producing retina PNG files under
screenshots/<theme>/.
deno task screenshot:attendees-list
deno task screenshot:listing
deno task screenshot:all
deno task screenshot attendees-list,listing --theme forest
deno task screenshot all --theme all --output ../tickets-site/images/screenshots
deno task screenshot listing-attendees --element 'form[action$="/attendee"]'
deno task screenshot --scenario ../tickets-site/scripts/screenshots/charity-events.js \
--output ../tickets-site/images/screenshots
Themes are applied through the site's custom CSS form. The choices are
default, forest, sunset, and ink; use --theme all to capture every
selected page in every theme. Set CHROMIUM_EXECUTABLE when Chromium is not in
Playwright's normal browser cache. On NixOS, devenv shell provides Chromium
and sets this variable for you.
Form scenes such as listing-form and add-attendee-form hide the rest of the
page, trim the image to the visible content, and add 32px of the page background
around it. Use --element with any CSS selector to make an element-only version
of another scene.
External scenario modules own one specific screenshot's data, custom CSS, and
browser steps while reusing this runner's temporary site and high-resolution
capture. Pass their path with --scenario; the scenario's name sets the PNG
filename directly inside --output. A scenario can set its own viewport when a
wider layout is part of the screenshot. Set adminSetup: false when the
scenario needs to capture the site before setup. The runner skips admin setup,
Stripe, and the saved theme, then applies the scenario's CSS after its browser
steps.
Pass --layers with an external scenario to also write transparent background,
controls, and text layers. They are named <scenario-name>__layer-<layer>.png
next to the normal screenshot.
deno task screenshot --scenario ../tickets-site/scripts/screenshots/charity-events.js \
--layers --output ../tickets-site/images/screenshots
Pass --social (one of facebook, instagram-landscape, instagram-portrait,
instagram-square, a comma-separated list, or all) to also write a
social-media-sized copy of each screenshot next to the original. The runner
takes the page background colour and extends one side of the canvas until the
image reaches the target's aspect ratio, then downscales to the target's pixel
size if the result is larger. Variants are named <theme>/<name>__<target>.png
(or <scenario-name>__<target>.png for scenarios). For example,
dashboard__facebook.png is the Facebook share size (1200 by 630px) of the
dashboard screenshot.
deno task screenshot all --social facebook,instagram-portrait
deno task screenshot dashboard --social all
deno task specs:evidence runs only Cucumber cases that declare evidence. It
runs them one at a time and captures each declared final page before that
scenario's test database is removed. Normal specs and test runs do not start
Chromium. Commit local changes before running it: the task refuses a dirty Git
worktree so the manifest cannot name a commit that differs from the captured
code.
The task writes a versioned reports/evidence/manifest.json and PNG files under
reports/evidence/assets/. The manifest uses the authored story, rule, case,
and capture IDs. It includes the app commit, image hash and dimensions, browser
profile, viewport, and presentation type. Raw Cucumber messages and reports are
not part of this evidence folder. The spec-evidence workflow is manual: an
operator starts it from the Actions tab. It checks the captures and uploads this
folder as the stable tickets-evidence artifact with GitHub's 35-day retention.
The Tickets website imports that artifact into a reviewed pull request and keeps
its ordinary site build offline.
# Enter the development shell (installs the pinned Deno and the other tools),
# then run all checks
devenv shell
deno task precommit
# Run locally
DB_URL=libsql://your-db.turso.io DB_TOKEN=your-token \
DB_ENCRYPTION_KEY=your-base64-key deno task start
# Or run against a throwaway in-memory database, with no Turso account
DB_URL=:memory: DB_ENCRYPTION_KEY="$(openssl rand -base64 32)" deno task start
# Same, restarting whenever a source file changes. An in-memory database is
# emptied by each restart, so use a file to keep your setup across edits:
# DB_URL=file:./local.db
DB_URL=:memory: DB_ENCRYPTION_KEY="$(openssl rand -base64 32)" deno task dev
# Run tests (stripe-mock downloaded automatically)
deno task test
On first launch, visit /setup/ to set admin credentials and currency. Payment
providers are configured at /admin/settings.
deno task start # Run server
deno task dev # Run server, restarting on source changes (static assets build once, so editing style.scss or bundle inputs needs the task restarted)
deno task test # Run tests
deno task test:coverage # Tests with coverage report
deno task lint # Format and lint code with Biome
deno task lint:ci # Strict read-only lint (what precommit runs everywhere)
deno task typecheck # Type check
deno task build:edge # Build for Bunny Edge
deno task deploy:edge <script-id> # Build, upload, and publish to Bunny Edge using BUNNY_ACCESS_KEY from .env
deno task backup # Dump the database out-of-band (uploads to storage; --out <path> for a local .zip)
deno task restore <backup.zip> # Restore the database at DB_URL from a local backup using DB_TOKEN from .env
deno task snapshot --out <path.sqlite> # Copy the complete remote database to a local SQLite file
deno task migrate:turso # Copy a remote database into a new Turso database
deno task precommit # All checks (typecheck, lint, cpd, build:edge, test:coverage)
| Variable | Required | Description |
|---|---|---|
DB_URL | Yes | libsql database URL |
DB_TOKEN | Yes* | Database auth token (*remote databases) |
DB_ENCRYPTION_KEY | Yes | 32-byte base64-encoded AES-256 key |
Optional:
| Variable | Description |
|---|---|
ADMIN_EMAIL_ADDRESS | Enables a superuser recovery account and the owner-only Support page (/admin/support). The email local-part (before @) must be a valid username: 1–64 characters (the SMTP limit), letters, numbers, hyphens, and underscores only. Email delivery must be configured before the superuser can be enabled. |
SUPPORT_PAGE_TEXT | Optional markdown for the Support page (requires ADMIN_EMAIL_ADDRESS). Use literal \n for line breaks. The form beneath it delivers to ADMIN_EMAIL_ADDRESS and needs a business email, like the contact form. |
SUPPORT_FORM_NAG_DAYS | Optional positive integer (default 7): how long the Support page shows a "you last submitted this form …" notice after a submission, to discourage duplicates. |
BOTPOISON_PUBLIC_KEY | Optional Botpoison public key. When set with BOTPOISON_SECRET_KEY, adds proof-of-work spam protection to the contact form (which otherwise works without it). |
BOTPOISON_SECRET_KEY | Optional Botpoison secret key. Used server-side to verify contact form submissions when Botpoison is enabled. |
UPTIME_KUMA_URL | Uptime Kuma 2.4 or newer URL for built-site maintenance monitors. Requires CAN_BUILD_SITES=true, UPTIME_KUMA_USERNAME, and UPTIME_KUMA_PASSWORD. A public host must use https. Cleartext http is allowed only for a local network address (loopback, private, CGNAT, link-local, or IPv6 unique-local). |
UPTIME_KUMA_USERNAME | Uptime Kuma username. Requires UPTIME_KUMA_URL and UPTIME_KUMA_PASSWORD. |
UPTIME_KUMA_PASSWORD | Uptime Kuma password. Requires UPTIME_KUMA_URL and UPTIME_KUMA_USERNAME. |
UPTIME_KUMA_INTERVAL_MINUTES | How often built-site monitors run. Any positive whole-minute interval is allowed. Defaults to 15. |
TURSO_API_TOKEN | Turso platform API key used to create databases. deno task migrate:turso asks for it when it is not set. |
Database maintenance: pruning of expired sessions, rate-limit rows, payment
records, and optional orphan attendees runs automatically while serving
requests. For quiet sites, configure an external monitor to send an
authenticated POST /scheduled on the schedule you need. Each site needs its
own key. New managed Uptime Kuma monitors default to every 15 minutes. A builder
with Uptime Kuma configured can inspect and add these monitors from each built
site's Scheduled maintenance tab. See the
scheduled maintenance guide for setup and CDN
rules.
Backups: every table is dumped to a single .zip, with table reads
keyset-paginated so no single response trips libsqld's "Response is too large"
payload cap (the server limit behind Bunny's databases). Backups run
out-of-band, not inside the migration: a full dump of a ~31-table schema
cannot fit alongside a migration within one edge request's
50-subrequest budget, so migrations
just migrate, and a backup is taken by GitHub Actions (or deno task backup)
beforehand. To enforce that, /admin/update and the per-site update button
refuse to deploy unless a backup of that database was taken in the last hour.
Restores run out of band because realistic imports exceed Bunny's per-request
subrequest limit. Put the target database's DB_URL, DB_TOKEN, and
DB_ENCRYPTION_KEY in .env, then run deno task restore <backup.zip>. The
task checks the backup, shows its available table, row, statement, and schema
details, and asks for a typed confirmation before deleting any data. It reports
each restore step and shows the backup's recorded commit when one is available.
Local SQLite snapshots: developers can copy a remote database into one
complete SQLite file using the same DB_URL and DB_TOKEN as the app:
mkdir -p .local-data
deno task snapshot --out .local-data/site.sqlite
The task reads DB_URL and DB_TOKEN from .env. Values in that file take
priority over existing shell variables. It then uses libSQL page-level sync,
moves all WAL data into the main file, and checks the finished database before
publishing it. It refuses to replace an existing file. The temporary replica is
created beside the output and removed on success or failure. Local .sqlite
files and .local-data/ are ignored by Git. Run this only on a development
machine; it needs a local filesystem and the native libSQL client. The file
contains the database exactly as stored, including encrypted personal data, but
it does not include environment secrets or files held in Bunny Storage.
Migrate to Turso: run deno task migrate:turso to copy a remote libSQL
database into a new Turso database. The task asks for the source database URL,
its password or token, and the new database name. It uses TURSO_API_TOKEN from
.env or the shell when available. Otherwise, it asks for the API key without
showing it. It uses TURSO_ORGANIZATION and TURSO_GROUP when set. Otherwise,
it discovers the available choices and asks only when there is more than one.
The task checks that the destination name is free before downloading the source.
It creates a verified standalone SQLite snapshot, then streams that file through
Turso's native database upload API. It does not rebuild the database one SQL
statement at a time. If token creation or upload fails, it deletes the
incomplete Turso database. On success it prints the new DB_URL and DB_TOKEN.
Keep using the source DB_ENCRYPTION_KEY; that key is not stored in the
database file.
The deploy workflows back a site up (via POST /instance/site-credentials)
before deploying to it (the staging push-to-main trigger is the one exception
— see below):
.github/workflows/backup.yml (manual) — backs up the main instance's own
database with DB_URL / DB_TOKEN / STORAGE_ZONE_NAME / STORAGE_ZONE_KEY
repository secrets..github/workflows/deploy-clients.yml (manual) — upgrades the built
client sites that accept the published tier. It asks the main instance for the
DB credentials of the sites on that tier (each site's own full-access token —
the workflow only reads, to take backups), backs each up to the builder's
storage, then deploys. The tier run input (alpha default, then beta
or release) is the release channel being published: each site carries its
own updates channel and only the sites at the deploy's tier or more eager
are returned — a release deploy reaches every site, beta reaches beta +
alpha sites, alpha only alpha sites. The default is alpha (the smallest
blast radius), so you ship a risky build to alpha sites first and reaching the
whole fleet is the deliberate release choice. For a canary (alpha/beta)
the workflow fails closed unless the main instance echoes the tier back in
its response, so running it against a master that predates tier filtering
(which would ignore the filter and return the whole fleet) cannot silently
deploy to everyone — upgrade the master first. It needs the MAIN_INSTANCE
URL secret plus STORAGE_ZONE_* and BUNNY_ACCESS_KEY; CDN-enabled builds
additionally need the five optional CDN secrets below. The MAIN_INSTANCE_KEY
that authorizes the credentials endpoint is pasted in as a run input each
time, never stored (set the same value as the main instance's
MAIN_INSTANCE_KEY env). BUNNY_SCRIPT_DATA is no longer needed — the script
ids come from the endpoint..github/workflows/bunny-deploy.yml (staging) and
.github/workflows/production-deploy.yml — same flow narrowed to the
single site whose script id matches BUNNY_STAGING_SCRIPT_ID /
BUNNY_SCRIPT_ID (the deploy fails unless exactly one fleet site
matches), via the shared backup-site action. They take MAIN_INSTANCE_KEY
as a run input but fall back to the stored secret. Only manual runs back
up: the staging push-to-main trigger (a merge) deploys without a backup,
so it needs no / /
secrets — just and , the
repository variable (the staging site's https origin,
which the update-mode wait polls), plus the optional CDN secrets when CDN
publishing is enabled. A manual run backs up by
default (needing those backup secrets, or the key pasted in) and can untick
to skip it — the escape hatch for
when the backup itself is broken. Production's update-mode wait polls the
secret (the production site's https origin) when the run
has no script id. The script id falls back to the same
way. A run with an overridden script id looks that site's URL up on the
main instance.Optional static CDN builds: set all five repository secrets CDN_URL,
CDN_BUNNY_STORAGE_ZONE_NAME, CDN_BUNNY_STORAGE_ZONE_KEY,
CDN_BUNNY_STORAGE_HOST, and CDN_BUNNY_PULL_ZONE_ID to publish
site-independent browser bundles and image-codec WASM to a dedicated Bunny
Storage/Pull Zone during the build. CDN_BUNNY_STORAGE_HOST is the Storage API
hostname shown on the zone's Bunny Storage Access page, such as
storage.bunnycdn.com or uk.storage.bunnycdn.com; it has no default because
it must match the zone's primary region. The existing BUNNY_ACCESS_KEY secret
purges that pull zone after every successful upload. Each upload carries its
SHA-256 checksum, and the build then fetches every public URL and verifies its
bytes and media type before emitting a script that depends on it. The generated
script contains immutable, content-addressed CDN URLs and the matching CSP
origin; these values are not runtime Bunny script secrets. Configure the pull
zone's browser cache override for a long lifetime because these paths change
whenever their content or media type changes. Site-bound assets such as the
embed bootstrap and dynamic order widget remain in each script. If all five CDN
secrets are absent, the build remains fully self-contained. A partial
configuration fails the build. Historical restore builds intentionally receive
none of these secrets and therefore retain their embedded assets.
Tune BACKUP_PAGE_SIZE (default 500 rows per read) via env if a single page
ever approaches the payload cap.
Restoring to a point in time: the running build records the git commit it
was built from into its own settings table (current_script_commit, written
on boot; cleared if a build ships without one, so it never goes stale), so every
dump carries the commit the site was running when the backup was taken.
Restoring a backup rolls back the data and surfaces that commit (the full
SHA); to roll the code back to match, run the
.github/workflows/restore-deploy.yml workflow with that commit and the
target script id. It builds the edge bundle fresh from our repository at that
commit (git is the per-commit source of truth) and deploys it — so a restore
only ever redeploys a commit from our own history, never code carried inside an
(untrusted) uploaded backup file. For safety the workflow validates the commit
is a full SHA and deploys via the external Bunny action pinned to an immutable
SHA, so the production API key is never handed to a deploy wrapper loaded from
the checked-out (possibly old) commit. (The rebuilt old code reports the rebuild
time as its build version, so the in-app updater may not surface the latest
release afterwards — redeploy latest via the normal deploy workflow to return to
it.)
See the CONFIG_KEYS reference for all optional variables (email providers, Apple Wallet, image uploads, and more).
AGPL-3.0-only - developed by Chobble CIC, a community interest company.
TURSO_ORGANIZATION | Turso organization used for new databases. The migration task discovers or asks for it when it is not set. |
TURSO_GROUP | Turso group used for new databases. The migration task discovers or asks for it when it is not set. |
DEBUG_KEY | Optional diagnostic key. GET /health returns a plain Up :) by default; a request carrying a matching X-Debug-Key header gets a small JSON payload (build commit, build timestamp, server time). Unset ⇒ the verbose response is disabled. |
MAIN_INSTANCE_KEYMAIN_INSTANCESTORAGE_ZONE_*BUNNY_STAGING_SCRIPT_IDBUNNY_ACCESS_KEYSTAGING_SITE_URLMAIN_SITE_URLBUNNY_SCRIPT_ID