Appearance
Doc status: Latest (rolling). See Versions.
Design principle
N-Watch is architecturally designed so that the end-user experience never scales beyond their trusted group. Every member belongs to a small, curated group managed by someone they know. The manager controls membership, the server enforces it, and no platform growth changes this relationship. See Security for the full trust model.
High level
- Mobile App (Expo SDK 55 / React Native +
@react-native-firebase/authnative SDK) - Admin Web (Next.js on Cloudflare Workers via vinext)
- API (Cloudflare Workers — Hono)
- Data (Firestore)
- Push (FCM → APNs/Android)
- Payments (Stripe — REST API, no SDK)
- PQC Crypto (
mlkem+@noble/ciphers— pure JS, MIT)
mermaid
flowchart LR
Mobile[Mobile App] -->|Firebase ID Token| API[Cloudflare Worker API]
Admin[Admin Web] -->|CF Access JWT| API
EntraID[Microsoft Entra ID] -->|OIDC SSO| CFA[Cloudflare Zero Trust Access]
CFA -->|JWT cookie| Admin
API -->|Firestore REST| DB[(Firestore)]
API -->|FCM HTTP v1| FCM[FCM]
API -->|REST| Stripe[Stripe]
Stripe -->|Webhooks| API
FCM --> APNs[APNs / iOS]
FCM --> Droid[Android]Request / auth flow
Mobile (Firebase Auth — native SDK)
- User signs in via
@react-native-firebase/auth(native SDK) using Google Sign-In or Sign in with Apple. - The native SDK initializes automatically via
google-services.json(Android) andGoogleService-Info.plist(iOS) — no JS-levelinitializeAuthcall. - App calls the Worker API with
Authorization: Bearer <FirebaseIdToken>. - Worker verifies the Firebase ID token server-side.
- Worker enforces authorization (role + membership + status) server-side.
Admin (Cloudflare Zero Trust + Microsoft Entra ID)
- Admin visits the dashboard — Cloudflare Access intercepts and redirects to Microsoft Entra ID SSO.
- After SSO, CF Access sets a signed
CF_AuthorizationJWT cookie. - Admin app reads the cookie and forwards it as
cf-access-jwt-assertionheader to the Worker API. - Worker verifies the CF Access JWT against the team's JWKS endpoint.
- Worker maps the Entra email to a Firestore user (auto-provisions a stub if new).
- Worker enforces authorization (role + membership + status) server-side.
Common
- Worker reads/writes Firestore via REST using a service account.
- Worker triggers push fan-out using FCM HTTP v1.
Authorization model
Standard users (mobile only) can:
- register device
- join group via invite
- trigger incident (only for groups they are an active member of, with active license)
- view their own profile and notifications
Managers can (scoped to their groups):
- create groups and invite codes
- broadcast messages
- pause/ban members
- view group members, invites, payments, license status
- initiate Stripe checkout for group license
Super admins (platform-wide) can:
- view all users, groups, licenses, payments
- set user roles (standard/manager/super_admin)
- grant and revoke licenses
- all manager capabilities across all groups
Server-side checks are mandatory for all privileged actions.
Data model (logical)
This is the conceptual structure (implemented in Firestore collections/subcollections):
users/{uid}- profile:
email,displayName,role,status devices/{deviceId}:platform,token,createdAt,lastSeenAt
- profile:
groups/{groupId}- metadata:
name,zoneId,createdByUid,createdAt members/{uid}:role(member/manager),status(active/paused/banned)incidents/{incidentId}: incident records
- metadata:
invites/{inviteCode}groupId,expiresAt,maxUses,uses,revoked
licenses/{licenseId}groupId,status(active/expired/revoked/trial),type(paid/granted)stripeSubscriptionId,stripeCustomerId,grantedByUidstartsAt,expiresAt
payments/{paymentId}licenseId,groupId,adminUidstripePaymentIntentId,amount,currency,status
notifications_log/{notifId}type,targetUid,groupId,message,read
system/cryptokemPublicKey,kid,updatedAt— the server-managed system KEM identity
Security model
- All privileged actions are checked server-side:
- Mobile auth: Firebase ID token verification
- Admin auth: Cloudflare Access JWT verification (JWKS), backed by Microsoft Entra ID SSO
- Authorization: group manager role checks in Firestore
- Admin dashboard is gated by Cloudflare Zero Trust — only authorised Entra ID users can reach it.
- Avoid storing exact addresses; prefer coarse zones.
Platform ownership
Owned and maintained by RME Solutions Technology Australia.
Address handling (on-device + encrypted)
N-Watch does not store a user’s exact home address centrally.
- The address / alert text is stored only on the user's phone and can be edited at any time.
- The alert text is sent only when an alert is triggered, as an encrypted payload.
- The server stores opaque per-recipient envelopes plus a system envelope that the server can decrypt for manager operations (admin decrypt, re-wrap).
- Plaintext is never persisted server-side — decryption is transient per request and audit-logged.
Encrypted incident payload (PQC-ready, locked suite)
Locked crypto suite:
- Key transport: ML-KEM-768
- Signatures: ML-DSA-44 (optional but recommended for integrity)
- Payload: AES-256-GCM
Important detail: ML-DSA is a signature algorithm (authenticity/integrity), not encryption. For encrypting/wrapping a per-incident symmetric key we use ML-KEM. Payload encryption remains AES-256-GCM.
Keys
- Each device generates a KEM keypair on first sign-in using pure-JavaScript PQC libraries (
mlkemfor ML-KEM-768,@noble/ciphersfor AES-256-GCM), compatible with Hermes engine on React Native. - The secret key is stored in the OS secure enclave via
expo-secure-store(Android Keystore / iOS Keychain) — never leaves the device, never in plain-text storage. - Secret keys are retained by
kidin secure storage so older envelopes can still be decrypted after key rotation. - Users can manually rotate their keys at any time from the settings menu.
- The public key is uploaded to Firestore at
users/{uid}/crypto/{kid}during sign-in and again at invite redemption so other group members can encrypt for this user. - One device per account — multi-device is not supported. A second device requires a separate email address.
Per-user keys:
- KEM keypair: ML-KEM-768 for wrapping a per-incident content key.
- Signature keypair: ML-DSA-44 (optional, for integrity verification).
Firestore: users/{uid}/crypto/{kid} → kemPublicKey, kid, createdAt
System key (server identity)
- The server holds its own ML-KEM-768 keypair for system-level operations.
- Secret key → Cloudflare Worker secret
PQC_KEM_SECRET_KEY(+PQC_KEM_KID). - Public key → Firestore
system/cryptodocument, bootstrapped viaPOST /v1/superadmin/bootstrapSystemKey. - Returned alongside member keys by
/v1/groups/:groupId/keys(uidnwatch-sys), so every alert is encrypted for the server too. - Enables: admin decrypt (view alert text), manager re-wrap (after user key rotation), and system key rotation.
Payload flow (E2E with system envelope)
Live in production since v0.3.0.
- On first sign-in the app generates an ML-KEM-768 keypair (native-first with WASM fallback); the secret stays in Keychain/Keystore; the public key +
kidare uploaded tousers/{uid}/crypto/{kid}. - When a user joins a group (invite redemption), the public key is also uploaded to ensure it's available.
- When the sender taps ALERT, the app calls
GET /v1/groups/:groupId/keysto fetch every active member's KEM public key plus the system key (uidnwatch-sys). - The sender's app encrypts the alert text independently for each recipient on-device (including the system key):
KEM-Encaps(recipientPK)→kemCiphertext+sharedSecretAES-256-GCM-Encrypt(sharedSecret, alertText)→ciphertext+nonce
- All per-recipient envelopes
{ uid, kid, kemCiphertext, ciphertext, nonce }are sent to the Worker in thetriggerIncidentrequest. - The Worker stores envelopes at
groups/{groupId}/incidents/{incidentId}/envelopes/{uid}— including thenwatch-sysenvelope. - FCM push contains only metadata:
incidentId,groupId,kind,hasEnvelope. No secrets or ciphertext in the push. - The sender is excluded from the FCM fan-out — the sender already sees the alert on-screen, and alerting their own phone could be dangerous if they are under duress.
- Recipient calls
GET /v1/incidents/:groupId/:incidentId/envelopeto fetch their opaque envelope. - Recipient decrypts locally:
KEM-Decaps→sharedSecret→AES-256-GCM-Decrypt→ plaintext. - Decrypted location shown in an in-app alert. GPS coordinates are rendered as tappable links that open in the native maps app.
- Manager operations (via admin dashboard): the Worker can decrypt the
nwatch-sysenvelope to view alert text or re-wrap for a user who rotated keys.
What never travels over the wire: private keys, plaintext addresses, shared secrets.
Emergency alert capabilities
- Android: Channel
alertscreated natively inMainApplication.onCreateviawithAlarmChannelconfig plugin. UsesAudioAttributes.USAGE_ALARM(alarm audio stream) +setBypassDnd(true)+ custom 29-second siren (alarm.wav). RequiresACCESS_NOTIFICATION_POLICY+USE_FULL_SCREEN_INTENTpermissions (manifest-declared); runtime DND-override prompt shown on first sign-in. - iOS:
apns-priority: 10, Critical Alert (critical: 1,interruption-level: critical). Requires Apple-grantedcom.apple.developer.usernotifications.critical-alertsentitlement. Sound routed through iOS critical alert audio channel automatically — no native code needed (entitlement + APNs payload is the Apple-recommended approach).
See also: Security.
Static “waterfall” delivery plan
The requirements are intentionally clear and narrow; this supports a straightforward waterfall delivery sequence:
- Requirements sign-off (roles, invite join, alerting, admin controls, data minimisation)
- System design sign-off (API contracts + data model + security invariants)
- Implementation
- Worker API + Firestore schema
- Mobile minimal UX
- Admin minimal UX
- Verification
- Contract tests (local/staging)
- Staging E2E tests
- Deployment
- Worker deploy
- Mobile release pipeline
- Admin deploy
- Operational hardening
- Monitoring + alerting
- Incident review process
Content & diagrams
- Put images in
docs/public/images/and reference as/images/.... - Use Mermaid blocks (like above) for flow diagrams.

