Appearance
NWatch security
Doc status: Latest (rolling). See Versions.
Goals
- Prevent unauthorized alerts, membership changes, and admin actions.
- Minimise stored sensitive data (no saved address as a plaintext profile field; optional precise GPS only in encrypted incident envelopes).
- Make abuse and spam harder than legitimate use.
- Keep security controls server-side and auditable.
- Preserve the invited-group model — every member belongs to a manager-controlled group with no public searchable directory or automatic admission.
The group as a security boundary
The most important security property of NWatch is structural, not cryptographic: every member belongs to an invited group whose manager controls membership.
Groups are small by design. Managers decide who joins (invite codes), who stays (active), and who is removed (pause/ban). There is no public searchable directory or self-service group admission, and no one can enter a group without an invitation from its manager. A manual introduction request for a participating community group does not bypass that decision.
This human-scale trust model means:
- Automatic discovery and admission are removed — joining requires an invite issued by a manager.
- Abuse is controllable — managers can immediately pause or ban any member.
- Scale does not erode trust — no matter how many groups exist across the platform, your group stays small and curated.
This structure is a launch design constraint. Any change to discovery or membership boundaries requires explicit security and privacy review.
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/authusing Google, a passwordless email link, or Microsoft when configured; Sign in with Apple is also available on iOS. The email link is one-time and secure: it is not a numeric OTP, and NWatch does not issue or store an NWatch password. - Microsoft accounts are linked only through an explicit signed-in account-linking flow. NWatch does not silently merge accounts that merely share an email address.
- 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.
Super-admin users (Cloudflare Zero Trust + Microsoft Entra ID)
- The super-admin console 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
- change member roles while preserving at least one active manager
- create/revoke invites
- view group members, invites, readiness and licence state
- reveal one incident's details through an audited workflow and resolve incidents
- manage seats and billing through Stripe-hosted pages
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)- Portal plaintext reveal (
GET /v1/portal/groups/:groupId/incidents/:incidentId/decrypt) requires an active manager membership. Administrative reveal (GET /v1/admin/groups/:groupId/incidents/:incidentId/decrypt) still requires asuper_adminto hold direct active membership in the target group.
Data minimisation & PII policy
- Do not persist exact home addresses as plaintext server fields.
- 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 (per-recipient encryption with a system recovery key)
Alert text and optional precise GPS are sealed on the sender's phone before upload. Sending captures the selected groups, their saved alert text and any chosen current GPS; retries reuse that encrypted snapshot. The portal cannot inspect unsent phone settings or live location.
The Encryption and group access guide explains the full message flow, wire format and access rules.
Cryptography (PQC)
Each recipient receives an independently encrypted envelope: ML-KEM-768 establishes its shared secret and AES-256-GCM protects its message. Current envelopes authenticate the group, incident, recipient and key identifier as AAD. There is no shared group decryption password, separate HKDF step or ML-DSA incident signature.
PQC implementation (mobile)
iOS attempts native CryptoKit operations and falls back to JavaScript when needed. Android uses JavaScript mlkem and @noble/ciphers. Both use the same envelope format and verify the authentication tag before displaying details.
Server-side PQC (API Worker)
A separate service-recovery envelope lets the Worker open a sent incident for authorised manager reveal or incident-key recovery. It does not provide account recovery or access to unsent local data. Member private keys remain on their devices.
In the manager portal, select a group, open Incidents and choose Reveal alert details. The API requires active manager membership in that exact group, logs the reveal and returns the requested details over HTTPS. A super administrator using the separate administrative reveal route must also hold direct active membership in the group.
Key management & rotation
Device keys (per user)
The phone generates and self-tests its keypair and registers its public key. Private keys are stored through expo-secure-store, backed by iOS Keychain or Android Keystore. One active device/key identity per account is supported. Rotation registers the new public key and retains old private keys locally by key identifier for historical envelopes.
System key (server-managed)
The production recovery private key is stored as an encrypted Cloudflare Worker secret. Its saved value is not displayed in the dashboard or Wrangler; the authorised Worker reads it at runtime for decryption. It is not sent to member devices or the portal. Only the recovery public key is distributed for sealing.
The service remains inside the encryption trust boundary. Authorised reveal and re-wrapping process plaintext transiently without saving it back to the incident database. See server-key storage and incident recovery for the detailed access model.
Recipient scoping
- The Worker computes recipients using server-side membership state (group + status).
- The Worker includes only device tokens belonging to active members in a new push fan-out. Targeting a token or receiving push-service acceptance is not proof that a device displayed, sounded or decrypted the alert.
- Ex-members are excluded from new fan-out and cannot fetch a new recipient envelope merely because they retained an old key.
Sender safety
- When a member triggers an alert, their own device does not receive the loud alarm notification.
- This protects users who may be under duress. The service then attempts delivery to eligible devices belonging to the other active members.
- The sender sees the server's storage and push-acceptance result on-screen; this is not confirmation that another device displayed the alert.
Push notification safety
- Push requests are submitted via FCM HTTP v1 from the Worker; FCM acceptance is not device receipt.
- Fan-out targets only the devices of active members in the relevant group.
- Android alert presentation: Notification channel
alertsusesAudioAttributes.USAGE_ALARM, high importance, vibration and public lock-screen visibility. DND bypass and full-screen presentation remain subject to special access, user settings and Android policy. NWatch does not turn DND off or raise system volume. - iOS Critical Alerts: APNs payload for incident notifications includes
aps.alert,apns-priority: 10,sound.critical: 1, custom sound name, andinterruption-level: critical(requires Apple-granted Critical Alerts entitlement). - Permission upgrade flow: On iOS, the app explicitly re-requests critical-alert permission even when general notification permission is already granted, and prompts the user to open Settings when
criticalAlertSettingremains disabled. - Device tokens are treated as secrets:
- never log full tokens
- rotate/re-register on app reinstall
Emergency alerts
- Android: Uses high-importance
alertsnotifications, alarm audio attributes and a full-screen incident activity when Android permits it. The app continuously checks whether the incident alarm has stopped so the activity closes instead of remaining over other apps. Overlay permission is not requested. - iOS: FCM payloads request Critical Alert presentation. Bypassing silent or Focus modes requires both an Apple-granted Critical Alerts entitlement and the user's permission; ordinary push remains the fallback.
Abuse controls
Minimum controls (server-side):
- Rate limiting for incident triggering, configured to fail open so a limiter fault cannot silence a real alert.
- 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 saved addresses stored as plaintext server profile fields
- Device tokens never logged
- Rate-limit incident triggering without making the limiter a delivery dependency
- Invite expiry + max-uses + revocation