Group event scheduling for scheduler.snoking.app. Pick a set of candidate days, share one link, and let everyone mark the days that work for them — no accounts, no email. Built on Cloudflare Workers + D1 with a no-build static frontend.
Three distinct roles, each identified by a secret bearer key/id stored in localStorage:
| Role | Page | Can do |
|---|---|---|
| Admin | /admin |
Create and delete organizers (mint/revoke organizer keys). |
| Organizer | /organizer |
Create, edit, and delete their own events; see the availability matrix; pick the final date. |
| Attendee | /e/:id |
Mark each candidate day as Works / Could work / Doesn't work; edit anytime until finalized. |
Keys are GUIDs. Only their SHA-256 hashes are stored server-side — the raw key is shown once and never again. The same person can hold both an admin key and an organizer key (the admin console's "Open as this organizer" button bridges the two on one device).
Admin creation is gated by the ADMIN_BOOTSTRAP secret and fails closed: if the secret is not
set, bootstrapping is unavailable (no "first admin free" path), so a public deployment can never hand
admin to the first anonymous caller. Set the secret before the first deploy, then on /admin
enter the bootstrap code to mint an admin key for your device.
# set/change the bootstrap code (required before bootstrapping any admin)
echo "your-secret-code" | npx wrangler secret put ADMIN_BOOTSTRAPadmins(id, name, created_at)—id= sha256(admin key)organizers(id, name, created_by, created_at)—id= sha256(organizer key)events(id, organizer_id, title, location, slots, organizer_prefs, questions, chosen_date, …)—slots= JSON[{id,date,start,end}];questions= JSON[{id,text}];chosen_dateholds the chosen slot idattendees(id, name, …)—id= attendee GUID from their deviceresponses(event_id, attendee_id, day, status)—dayholds a slot id;status∈works | maybe | noanswers(event_id, attendee_id, question_id, answer)— attendee free-text answerspush_subs(endpoint, organizer_id, p256dh, auth, created_at)— organizer Web Push subscriptions
Installable PWA (manifest.webmanifest, push-only sw.js, calendar-emoji icons). The Worker injects a
build-id auto-reload script into every HTML page (/api/version + CF_VERSION_METADATA), so the app
picks up JS/CSS changes automatically — including warm-resumed home-screen apps. run_worker_first lets
the Worker serve/inject HTML.
Organizers can enable Web Push (VAPID, RFC 8291 aes128gcm) to get notified when someone responds. On iOS this only works once the PWA is added to the Home Screen. Keys:
# private VAPID key (JWK); public key + subject live in wrangler.jsonc [vars]
echo '<VAPID_PRIVATE_JWK_json>' | npx wrangler secret put VAPID_PRIVATE_JWKnpm install
npm run db:local # apply schema to the local D1
npm run dev # wrangler devnpm run db:remote # apply schema to the remote D1 (first time / on changes)
npm run deployThe custom domain scheduler.snoking.app is wired up via the routes block in wrangler.jsonc.
.github/workflows/deploy.yml deploys the Worker + assets on every push to master
(and via manual "Run workflow"). It uses two repo secrets:
CLOUDFLARE_API_TOKEN— token with Workers Scripts:Edit (+ D1 if migrating)CLOUDFLARE_ACCOUNT_ID
CI runs wrangler deploy only. Schema migrations are not run by CI — apply
schema.sql (or one-off ALTERs) manually with npm run db:remote. Worker secrets
like ADMIN_BOOTSTRAP live in Cloudflare and are untouched by deploys.