The Problem
Commercial perimeter security — motion sensors, cameras, monitoring contracts — is expensive, and most of it is hardware you don’t own tied to a subscription you can’t leave. Almost everyone already owns a device with a camera, microphone, GPS and accelerometer: a phone. The brief was to turn spare Android phones into proper sensor nodes, with a real admin dashboard, live video, and alerting — without hardware, app store distribution, or a hosting bill.
Architecture: Phone as Node, Edge as Backend
The node itself is a Chrome PWA, not a native app — pas_node.html
running under Chrome’s Wake Lock, camera, microphone, GPS and
DeviceMotion APIs, installable straight from the browser with no app
store review. Everything behind it runs on Cloudflare: Workers for the
API, D1 (SQLite at the edge) for storage, and a Durable Object per
node acting as a stateful signaling room:
[ Android PWA Node ] → HTTPS/WSS → [ Cloudflare Edge ]
Workers (API)
D1 (SQLite)
Durable Objects (signaling)
→ WebRTC P2P → [ Admin Dashboard ]
Live video never touches the Worker at all — the Durable Object only negotiates the WebRTC handshake (SDP offer/answer, ICE candidates) between node and dashboard, then gets out of the way while video streams peer-to-peer. Snapshots on trigger events go to imgBB’s free API instead of R2, which keeps storage off the paid tier entirely.
Auth: Two Token Scopes, One Signing Key
Admins and nodes both authenticate with JWTs signed via Web Crypto HMAC-SHA256, but they carry different scopes — an admin token can manage every device, while a node token is bound to a single device ID and can’t reach admin routes at all. Pairing a new phone works like a lot of IoT onboarding: the dashboard generates a short-lived pairing token as a QR code, the node scans it, and the Worker exchanges it for a permanent device-scoped token.
Config Sync With a Version Counter
Each device’s device_config row carries trigger thresholds (motion,
sound in dB, geofence radius, impact in g-force), alarm behavior
(torch pattern, siren duration), and a config_version integer. Rather
than pushing config over the signaling socket, the node polls and
compares versions — a stale local config means a fresh pull is due. That
keeps the config path decoupled from the real-time signaling path, so a
node with a flaky WebSocket connection can’t fall permanently out of
sync with its trigger settings.
Webhooks: HMAC-Signed, Retried, Filterable
Outbound webhooks are matched against a per-webhook events filter, and every delivery is HMAC-signed with a per-webhook secret so a receiving endpoint can verify the payload actually came from PAS. Failed deliveries are tracked in their own table — status, response code, attempt count, last-attempted timestamp — which is what makes retry logic and delivery debugging possible after the fact instead of disappearing into a fire-and-forget POST.
What This Project Demonstrates
- Durable Objects used for what they’re actually good at — a small amount of coordination state (a signaling room) per entity, not as a database substitute
- WebRTC P2P kept off the Worker’s request path entirely, so live video bandwidth costs nothing at the edge
- Two-scope JWT auth (admin vs. per-device node tokens) instead of one flat permission model
- A version-counter pattern for config sync that tolerates an unreliable real-time channel
- HMAC-signed, retry-tracked webhooks — signed delivery and delivery history, not just a webhook URL and a hope
Live dashboard: https://pasv3.pages.dev/demo.html Node PWA: pasv3.pages.dev/pas_node.html GitHub: pasv3