Doc status: Latest (rolling). See Versions.
Goals
- Prevent unauthorized alerts, membership changes, and admin actions.
- Minimise stored sensitive data (no exact home addresses; avoid exact lat/lng when possible).
- Make abuse and spam harder than legitimate use.
- Keep security controls server-side and auditable.
Threat model (practical)
Primary threats
- Account takeover of a legitimate user (phishing, device compromise).
- Malicious member (or ex-member) attempting spam/harassment via alerts.
- Invite leakage leading to unauthorized joins.
- API abuse (bot traffic, token replay, endpoint discovery).
- Device-token abuse (registering many tokens, sending to wrong audience).
Non-goals
- Defending against a fully compromised manager account beyond standard controls.
- Guaranteeing critical-alert behavior on iOS without Apple entitlement approval.
Identity & authentication
Mobile users (Firebase Auth — native SDK)
- Mobile users authenticate with
@react-native-firebase/auth— the React Native Firebase native SDK (Google Sign-In on both platforms, Sign in with Apple on iOS). - The native SDK initializes via platform config files (
google-services.json/GoogleService-Info.plist), avoiding JS-level initialization issues on Android new-arch/Hermes. - All mobile API calls are authenticated with a Firebase ID token:
Authorization: Bearer <token>. - The Worker verifies the Firebase ID token server-side and derives the canonical
uid. - Apple Sign-In uses
auth.AppleAuthProvider.credential(identityToken, nonce)with a SHA-256 hashed nonce for replay protection.
Admin users (Cloudflare Zero Trust + Microsoft Entra ID)
- The admin dashboard is protected by Cloudflare Zero Trust Access.
- Authentication is delegated to Microsoft Entra ID (Azure AD) via OIDC SSO.
- CF Access injects a signed JWT (
CF_Authorizationcookie) after successful SSO. - The admin app forwards this JWT to the Worker API via the
cf-access-jwt-assertionheader. - The Worker verifies the JWT against the CF Access team's JWKS endpoint (
<team>.cloudflareaccess.com/cdn-cgi/access/certs). - The Worker maps the Entra email to a Firestore user record (auto-provisions if new).
- External collaborators can be accommodated via Entra B2B guest accounts or CF Access one-time PIN policies.
Common
- Never trust
uid/role claims from the client; always compute authorization server-side.
Authorization & privileges
Authorization is enforced in the Worker, using Firestore membership state:
Member actions
registerDevice: only for self (uidfrom token)joinGroupViaInvite: only for selftriggerIncident: only if the user is an active member of the group
Manager actions (group scoped)
- pause / ban member
- create/revoke invites
- broadcast
- view group members, invites, license, payments
- initiate Stripe checkout (redirects to Stripe-hosted page)
Super admin actions (platform-wide)
- view all users, groups, licenses, payments
- set user roles (standard/manager/super_admin)
- grant/revoke licenses
Membership status is enforced server-side:
active: may trigger incidentpaused: cannot trigger incidentbanned: cannot trigger incident or re-join (unless explicitly unbanned)
Data minimisation & PII policy
- Do not store exact home addresses.
- Avoid storing exact latitude/longitude when possible.
- Prefer coarse “zones” (e.g., suburb-level or grid cell identifiers) rather than precise locations.
- Store only what is necessary to operate:
- user identity:
uid, optionalemail/displayName - device push token + platform
- group membership + role + status
- incident metadata (time, group, type)
- user identity:
Address privacy (on-device + true E2E)
- A user's exact home address is stored only on their phone (in
AsyncStorage). - When an alert is triggered, the address is encrypted on-device for each recipient individually using ML-KEM-768 / AES-256-GCM.
- The server receives and stores only opaque per-recipient envelopes — it has no key to decrypt them.
- Only the intended recipient can decrypt their envelope using their device-local KEM secret key.
- The backend never sees, logs, or persists plaintext addresses.
Cryptography (PQC)
Implemented using @maatara/core-pqc (v0.5.0, Apache-2.0) — a WASM-based PQC toolkit providing the locked crypto suite.
Locked crypto suite:
- Payload: AES-256-GCM
- Key transport (KEM): ML-KEM-768
- Key derivation: HKDF-SHA256
- Signatures: ML-DSA-44 (optional but recommended)
Important detail: ML-DSA is a signature scheme (authenticity/integrity), not encryption. For key transport/encryption we use a KEM (ML-KEM).
For forward compatibility, include a cryptoSuite and kid (key id) in encrypted payload metadata.
Dual PQC strategy (mobile)
The mobile app uses a dual strategy for PQC operations:
- Primary:
@neighbourhoodwatch/pqc-native— Rust-based Expo native module (fastest, used in production builds). - Fallback:
@maatara/core-pqc— WASM-based (used automatically when the native module is unavailable, e.g., in Expo Go during development).
Both implementations use the same locked crypto suite and produce interoperable ciphertext.
Server-side PQC (API Worker)
The Cloudflare Worker is zero-knowledge with respect to alert payloads:
- It stores opaque per-recipient envelopes in Firestore.
- It does not possess a KEM secret key for decryption —
PQC_KEM_SECRET_KEYis no longer used. - It serves envelopes back to authenticated recipients for on-device decryption.
Key management & rotation
- Each device generates an ML-KEM-768 keypair on first sign-in.
- The secret key is stored in the OS secure enclave via
expo-secure-store(Android Keystore / iOS Keychain) — never leaves the device, never in plaintext storage. - The public key is uploaded to
users/{uid}/crypto/{kid}at sign-in and at invite redemption. - Key rotation is supported by publishing a new
kidand keeping the previous key for a short overlap window.
Recipient scoping
- The Worker computes recipients using server-side membership state (group + status).
- Only active members receive the push payload.
- Ex-members do not receive pushes; even if they retained old keys, they cannot decrypt messages they do not receive.
Push notification safety
- Push is sent via FCM HTTP v1 from the Worker.
- Fan-out targets only the devices of active members in the relevant group.
- Device tokens are treated as secrets:
- never log full tokens
- rotate/re-register on app reinstall
Emergency alerts
- Android: Uses notification channel
alertswithMAXimportance,bypassDnd: true, public lockscreen visibility. Alerts sound even when device is on silent / DND. - iOS: FCM payloads include
apns-priority: 10and Critical Alert (critical: 1, volume: 1.0) so notifications sound at full volume even on silent. Requires Apple Critical Alerts entitlement.
Abuse controls
Minimum controls (server-side):
- Rate limiting for incident triggering and admin broadcast.
- Idempotency / dedupe to avoid accidental double sends.
- Invite controls
- expiry
- max uses
- revoke capability
Operational controls:
- Managers can pause/ban members.
- Maintain an incident review workflow (even if manual early).
Secrets & configuration
- Service account credentials and FCM credentials must be stored as Cloudflare secrets/env vars.
- Stripe keys (
STRIPE_SECRET_KEY,STRIPE_WEBHOOK_SECRET) must be stored as Cloudflare secrets (never in wrangler.toml). - Never commit secrets to git.
- Keep separate environments for dev/staging/prod where practical.
Payment security (Stripe)
- All payment processing uses Stripe Checkout (Stripe-hosted page); no card data touches our API.
- Stripe webhooks are verified server-side using HMAC-SHA256 with timing-safe comparison and a 5-minute timestamp tolerance.
- License activation occurs only after webhook verification of
checkout.session.completed. - Payment records are stored server-side in Firestore (
paymentscollection) for audit.
Logging & monitoring
Log security-relevant events (without sensitive payloads):
- auth failures
- authorization failures
- invite use / join
- incident triggers (metadata only)
- admin actions
Avoid logging PII and never log device tokens.
Firestore rules
Firestore rules should be treated as defense-in-depth.
- The Worker is the primary enforcement point for privileged actions.
- Client direct access should be restricted to the minimum required (ideally none for privileged writes).
Checklist
- Server-side token verification on every API request
- Group-scoped authorization on every privileged action
- No exact addresses stored
- Device tokens never logged
- Rate limit trigger/broadcast
- Invite expiry + max-uses + revocation

