You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
utility and backend services map aws_access_key_id / aws_secret_access_key (lowercase in .env) to uppercase AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY for boto3 compatibility
clientpython mounts /dev/video0 from the host for camera access
Session-based (browser SPA): Django session cookies. Public routes are whitelisted in JwtBearerAuthenticationMiddleware.
Bearer Tokens (devices): OAuth2 device flow tokens issued by django-oauth-toolkit. The HTTP middleware validates these as a fallback after Cognito JWT validation; DRF uses OAuth2Authentication to resolve request.user.
WebSocket Auth:
Device clients: OAuth2 Bearer token in Authorization header, validated against oauth2_provider.AccessToken
Browser viewers: Session cookie via AuthMiddlewareStack
OAuth2 Device Flow (RFC 8628):
POST /oauth/device-authorization/ — creates DeviceGrant + ClientDevice
Device polls POST /oauth/token/ with device_code
Utility auto-approves the grant and exchanges for tokens
Tokens written to shared volume (client_tokens)
WebRTC Streaming Flow
Device client (ClientPython or ClientiOS — same protocol either way) connects to /ws/live_stream/<device_id>/ with Bearer token
Viewer (React) connects to same WebSocket via session auth (or ?share_token= for anonymous shareable-link viewers, see shareable_links app)
Viewer creates RTCPeerConnection with recvonly video transceiver
ICE candidates exchanged bidirectionally via signaling server
Video stream flows device → peer connection → viewer <video> element
On viewer disconnect, device resets RTCPeerConnection (keeps signaling WS open) ready for the next viewer
On-screen display message (doorbell mode)
A display_message signaling message (viewer → device only) lets a session-authenticated viewer
remotely set/clear a message shown on the device's own screen (e.g. "We're not home right now") —
used by ClientiOS when mounted front-camera-out as a doorbell. Anonymous shareable-link viewers
are rejected server-side (live_stream/consumers.py's receive() checks scope["share_viewer"])
so guests can look but not control the device's display.
Billing (Stripe)
Single paid tier, 14-day trial for everyone, 5-device cap on all accounts. Full details and the
Stripe Dashboard/CLI setup steps live in APIServer/billing/README.md. The load-bearing ideas:
Signup creates a real Stripe subscription in trialing with no card required, so there is
no Checkout step anywhere — the Stripe-hosted Customer Portal is the only billing UI.
Invites flagged grants_comp_subscription comp the user with a 100%-off-forever coupon rather
than bypassing billing. Comped users get the same subscription, webhooks, and €0 invoices as
anyone else — which is the whole point: the free early-adopter cohort is how the billing code
gets exercised for real.
Entitlement is enforced, via billing.services.is_entitled: an unentitled user can't view
streams (checked on the device owner, so their share links die too) and can't approve devices.
Staff are exempt; no Subscription row means unentitled (fail-closed).
Test-mode and live-mode Stripe IDs are not interchangeable, so live keys must be in place
before the first real user signs up.
Notifications
notifications.Notification (backend) is the storage/API foundation for user-facing alerts —
separate from client_devices.DeviceTelemetryLog, which is a raw per-device event log that isn't
inherently user-facing. notifications.services.create_notification(...) is the single entry
point other code should call to create one (e.g. a future motion-detection feature would call
this, rather than constructing Notification objects directly). Currently backend-only
(model + REST list/mark-read endpoints, no live delivery channel or push notifications yet) —
real-time delivery to an open browser tab and push notifications for when nobody's looking are
both intentionally deferred follow-ups.
Key API Endpoints
Endpoint
Auth
Description
GET /api/v1/users/session/
Public
Check auth state
POST /api/v1/users/logout/
Required
Logout
GET /api/v1/devices/
Required
List user's devices (includes telemetry logs)
DELETE /api/v1/devices/{id}/
Required
Delete a device owned by the user (cascades telemetry logs)
POST /api/v1/devices/{device_code}/telemetry/
OAuth2 Bearer
Ingest device telemetry event
GET /api/v1/devices/oauth-app/
Required
Get the device OAuth app's client_id (used to build the iOS setup QR code)
POST /api/v1/shareable-links/
Required
Create an anonymous share link for a device
GET /api/v1/shareable-links/{token}/
Public
Validate a share link (used by anonymous viewers)
GET /api/v1/notifications/
Required
List the user's notifications (?unread=true)
POST /api/v1/notifications/{id}/read/
Required
Mark a notification read
POST /api/v1/notifications/mark-all-read/
Required
Mark all notifications read
GET /api/v1/billing/subscription/
Required
Current user's subscription status + device usage
POST /api/v1/billing/portal-session/
Required
Get a Stripe Customer Portal URL to redirect to
POST /api/v1/billing/webhook/
Stripe signature
Stripe subscription state sync
POST /api/v1/invites/validate/
Public
Validate invite code
POST /api/v1/invites/
Admin
Create invite
GET /api/v1/invites/
Required
List invites
POST /api/v1/invites/{id}/revoke/
Admin
Revoke invite
POST /oauth/device-authorization/
Public
Initiate device flow
POST /oauth/token/
Public
Poll for tokens
GET /api/v1/docs/
—
Swagger UI
GET /api/v1/schema/
—
OpenAPI spec
WS /ws/live_stream/<device_id>/
Token/Session/?share_token=
WebRTC signaling + display_message
Django Management Commands
Command
Purpose
migrate
Run database migrations
createsuperuser --noinput
Create Django superuser from env vars
create_local_invite
Generate a local dev invite token
create_cognito_user
Create superuser in AWS Cognito (idempotent — sets password even if user exists)
register_device
Full OAuth device flow: initiate → approve → exchange tokens → write to shared volume
auto_approve_devices
Poll and auto-approve pending devices (continuous loop)
Deployment
Push to master triggers GitHub Actions
Docker image built (multi-stage: Node → Python → Alpine) and pushed to docker.kmanning.ie:5000
Self-hosted runner pulls and runs docker compose -f infra/docker-compose.yml -f infra/docker-compose.prod.yml up -d