One press. Your location, your identity, and live audio reach an emergency responder — then the whole thing destroys itself an hour later.
Panic is a proof of concept for emergency alerting built entirely on Cloudflare's edge. It proves a deliberately narrow path end to end: a press creates a one-hour alert, routes it to a responder eligible for that place and category, streams movement and chunked Opus audio, and destroys the alert data on schedule.
It is a POC, not a product. It is not connected to real emergency services, and nothing here should be relied on in an actual emergency.
| Press | A large button on iPhone or Apple Watch. Touch sends immediately; hold adds live audio. |
| Route | The gateway resolves country and state from the coordinates, then picks recipients registered for that area and category. |
| Deliver | An HMAC-signed webhook reaches the responder's endpoint. The app shows the delivery state, and says plainly that delivery is a server response — not a promise that help is coming. |
| Follow | Location updates stream at a cadence that adapts to movement and battery. Audio arrives as individual Opus chunks. |
| Destroy | Everything expires on a schedule held by the alert's own Durable Object. The signed live URL and the audio objects both stop existing. |
Four pieces, three of them on Cloudflare.
iOS / watchOS ──▶ panic-gateway (Worker) ──▶ panic-receiver (Worker)
PanicCore Durable Object per alert responder dashboard
D1 · metadata, routing D1 · alert index
R2 · Opus audio chunks
▲
└── panic-canary (Worker, scheduled)
gateway/— a plain TypeScript Worker. One jurisdiction-prefixed Durable Object per alert holds its state and its retry/expiry schedule. D1 stores installation, routing, and configuration metadata, a 60-second atomic idempotency locator, and payload-free delivery results. R2 stores the audio chunks. The Worker never logs bodies or bearer tokens.receiver/— a SvelteKit responder desk onadapter-cloudflare, backed by D1. It serves the signed webhook, a triage dashboard, the status page, and health. It stores only the expiring signed URL, an opaque recipient-scoped device ID, initial coordinates, and timestamps. Profiles, movement history, and audio stay at the gateway and disappear with it.clients/— a sharedPanicCoreSwift package plus iOS, Watch, and complication targets, generated with XcodeGen. The Watch talks to the gateway directly when it has a connection and falls back to the phone when it doesn't.canary/— a scheduled Worker that exercises the whole Lagos route every minute and writes independent status evidence to R2. Not currently deployed — it needs a public-read R2 custom domain.
Delivery is not rescue. The UI never implies that a delivered alert means help is on the way. delivered means one server accepted a signed request. Calling emergency services is presented as a separate action.
The profile is not an account. There are no user accounts. Name, phone, email, medical notes, and emergency contacts live on the device and ride along with an alert. The gateway passes them to the responder and forgets them when the alert expires.
Alerts expire whether or not anyone is watching. Expiry is the Durable Object's own alarm, and the receiver purges its index on a cron. Neither depends on a dashboard being open.
Idempotency is global. A phone and a Watch pressing at the same moment must produce one alert, not two. The gateway reserves the idempotency key in D1 and caches the response at the edge before returning, so the second press replays the first result.
Requires Node 24+, Wrangler 4, and — for the clients — Xcode with XcodeGen.
npm install
npm run check
bash scripts/demo.shscripts/demo.sh applies local D1 migrations, starts a signed recipient stub, and exercises the definition of done against a local gateway.
cd clients && xcodegen generate && open Panic.xcodeprojBy default the clients point at http://127.0.0.1:8787/v1 with no dev token. To point a build at a deployed gateway, copy the example config — it is git-ignored, so your values are never committed:
cp clients/Secrets.example.xcconfig clients/Secrets.xcconfigThe dev token is optional. It only unlocks the simulated Lagos origin and shortened demo TTLs, and must match the PANIC_DEV_TOKEN secret on your gateway Worker.
Each component has its own README with details. In short:
cd gateway
npx wrangler d1 create panic-gateway # put the id in wrangler.jsonc
npx wrangler r2 bucket create panic-data
npx wrangler d1 migrations apply DB --remote
npx wrangler secret put PANIC_DEV_TOKEN
npx wrangler deploycd receiver
npx wrangler d1 create panic-receiver # put the id in wrangler.jsonc
npx wrangler d1 migrations apply DB --remote
npx wrangler secret put PANIC_SHARED_SECRET # must match the recipient row in the gateway's D1
npx wrangler deployTwo things that will bite you:
- Replace the seeded recipient secrets.
gateway/migrations/0001_initial.sqlseeds demo recipients with placeholder secrets. Rotate them before any real deployment. - Worker-to-Worker calls need a service binding. Same-account
workers.devsubrequests return 404. The receiver reaches the gateway through aGATEWAYservice binding, not a plainfetch.
docs/implementation-status.md tracks exactly what is and is not implemented.
Deliberately not built: user accounts, Android, SMS, public maps, organisation controls, real Nigerian boundary polygons, attestation verification, and analytics.
Currently dormant:
- Emergency contact notification — implemented in
gateway/src/contacts.tsand wired in, but it stays inert unless both anEMAILbinding andPANIC_ALERT_FROMare configured. Cloudflare Email Sending requires a domain you own. - The canary — needs a public-read R2 custom domain for its status objects.
- Recipient webhooks are HMAC-signed with a per-recipient secret and carry a timestamp and idempotency key. An unsigned request is rejected before the body is parsed.
- Live alert URLs are signed and expiring. Recipient endpoints must be HTTPS.
- Device identity reaching a responder is an opaque per-recipient hash, so suppression can block a device without identifying a person.
- Alert rate is capped per install, per hour and per day.
- No secrets are committed.
clients/Secrets.xcconfigis git-ignored; Worker secrets go throughwrangler secret put.
The wrangler.jsonc files do contain the deployed workers.dev hostnames, which are public endpoints. Anyone can install against a deployed gateway and create alerts within the rate limits — fine for a POC, worth changing before anything real.
MIT. See LICENSE.