Skip to content

Latest commit

 

History

8 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Panic

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.

What it does

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.

Architecture

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 on adapter-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 shared PanicCore Swift 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.

Design decisions worth knowing

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.

Running it

Requires Node 24+, Wrangler 4, and — for the clients — Xcode with XcodeGen.

npm install
npm run check
bash scripts/demo.sh

scripts/demo.sh applies local D1 migrations, starts a signed recipient stub, and exercises the definition of done against a local gateway.

The apps

cd clients && xcodegen generate && open Panic.xcodeproj

By 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.xcconfig

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

Deploying

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 deploy
cd 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 deploy

Two things that will bite you:

  • Replace the seeded recipient secrets. gateway/migrations/0001_initial.sql seeds demo recipients with placeholder secrets. Rotate them before any real deployment.
  • Worker-to-Worker calls need a service binding. Same-account workers.dev subrequests return 404. The receiver reaches the gateway through a GATEWAY service binding, not a plain fetch.

Status and scope

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.ts and wired in, but it stays inert unless both an EMAIL binding and PANIC_ALERT_FROM are configured. Cloudflare Email Sending requires a domain you own.
  • The canary — needs a public-read R2 custom domain for its status objects.

Security notes

  • 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.xcconfig is git-ignored; Worker secrets go through wrangler 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.

Licence

MIT. See LICENSE.

About

Emergency alerting POC on Cloudflare Workers: one press routes your location, identity and live audio to a responder, then destroys itself. Durable Objects, D1, R2, iOS + Apple Watch.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages