A minimal real-time private-group comms + Common Operating Picture (COP) starter:
- Socket.IO relay
- Vite/React web client
- shared TypeScript protocol package
Designed for low-overhead experimentation today, with a clear path toward a modern, governance-first “BFT-inspired” workflow: mission packages (“fills”), map overlays/routes, incident reporting, and role-based visibility.
- What it is (v0): invite-only chat rooms with allowlisted rooms + acks/dedupe/retry.
- What it’s becoming: a trusted-team COP: map + overlays + incident workspace + controlled distribution ("fills").
- Why: teams don’t fail from UI — they fail from inconsistent data, unclear roles, and no shared picture.
- Governance-first: identity, enrollment, RBAC, auditability, and retention controls are primary features.
- Transport-agnostic: the app layer stays stable; transport becomes a replaceable module (internet now, local/degraded later).
- Privacy by default: location sharing is opt-in, scoped, and minimized.
- Legit use-cases: oriented toward mutual aid / volunteer response / community CERT-style coordination / organizational teams.
Neighborhood-Watch/
apps/
relay/ # Node + Express + Socket.IO server (message relay)
web/ # Vite + React client
packages/
protocol/ # Shared TS types (events + message envelope/ack)
package.json # npm workspaces (monorepo)
- Node.js 22.12.0 (recommended; pinned via Volta)
- npm
- Git (recommended)
Verify:
volta -v
node -v
npm -vPin Node in this repo:
volta install node@22.12.0
volta pin node@22.12.0From repo root:
npm install
npm -w packages/protocol run buildnpm -w apps/relay run devRelay:
- URL:
http://127.0.0.1:8787 - Health:
GET /health
Verify:
curl http://127.0.0.1:8787/health
# {"ok":true}npm -w apps/web run devWeb (Vite dev server):
- Typically
http://localhost:5173
v0 supports two layers:
- Invite-only connect (handshake auth)
- Allowlisted rooms (server-controlled room list)
Set one or more tokens via INVITE_TOKENS (comma-separated).
If INVITE_TOKENS is set (non-empty), the relay requires a token.
PowerShell example (dev):
$env:INVITE_TOKENS="Alpha-Team"
npm -w apps/relay run devClient provides the token either:
- via URL:
?token=Alpha-Team - or
apps/web/.env:VITE_INVITE_TOKEN=Alpha-Team
Rooms are controlled by ALLOWED_ROOMS (comma-separated). If not set, defaults to:
emergency,TOC,vacant-1,vacant-2,vacant-3,vacant-4
Example: restrict to only emergency + TOC
$env:ALLOWED_ROOMS="emergency,TOC"
npm -w apps/relay run devBehavior:
join(room) returns an ack:
- ok:
trueif joined - ok:
falsewith reason andallowedRoomswhen denied
The web client surfaces this as Room join: joined / denied (room_not_allowed) and disables Send until joined.
apps/web/.env
VITE_RELAY_URL=http://127.0.0.1:8787
# optional convenience; you can still use ?token=...
VITE_INVITE_TOKEN=Alpha-TeamRelay allows these dev origins by default:
http://127.0.0.1:5173http://localhost:5173
Override:
PORT=8787
CLIENT_ORIGIN="http://127.0.0.1:5173,http://localhost:5173"
INVITE_TOKENS="Alpha-Team,Backup-Token"
ALLOWED_ROOMS="emergency,TOC"The simplest mental model: build everything once, then run the relay, and serve the web build.
- Build (from repo root):
npm -w packages/protocol run build
npm -w apps/relay run build
npm -w apps/web run buildNote: if
apps/relaydoesn’t yet have a build + start script, add them (typical pattern):
- build: compile TS -> dist/
- start: node dist/index.js
- Run relay with env vars (PowerShell example):
$env:PORT="8787"
$env:CLIENT_ORIGIN="https://your-domain.example"
$env:INVITE_TOKENS="Alpha-Team"
$env:ALLOWED_ROOMS="emergency,TOC"
npm -w apps/relay run startIf you want logs written to a file:
npm -w apps/relay run start 2>&1 | Tee-Object -FilePath .\relay.log- Serve the web build
For Vite, the usual local production preview is:
npm -w apps/web run previewOr serve apps/web/dist/ using any static server.
Defines:
ChatEnvelope:{ id, room, from, sentAt, body }ChatAck:{ id, ok, reason? }JoinAck:{ room, ok, reason?, allowedRooms? }- Socket.IO event types for client/server, including an ack callback on join
Flow:
-
client connects (optionally must present invite token)
-
client emits
join(room, ack) -
relay validates + allowlists the room
-
relay either:
socket.join(room)and acks ok- or acks denied with
reason + allowedRooms
-
client emits
chat(envelope) -
relay validates envelope (length/type checks)
-
relay enforces membership: sender must have joined
envelope.room -
relay dedupes by
envelope.id(in-memory) -
relay broadcasts
chat(envelope)to the room -
relay sends
chat_ack({id, ok, reason?})to the sender
Notes:
- v0 does not persist messages on the server
- dedupe is best-effort and resets on relay restart
-
connects to relay via Socket.IO (auth token)
-
attempts to join the active room and waits for
JoinAck -
disables Send until the room is joined
-
tracks delivery state via
chat_ack:- pending → sent (ack ok)
- pending → failed (ack reject) or retry limit reached
-
retries pending messages after reconnect (bounded)
-
surfaces room join denial reasons + allowed rooms list
This repo is intentionally small, but the direction is explicit.
A Mission Package is a versioned bundle intended to keep a team in uniformity:
- map references (online/offline)
- overlay layers (GeoJSON/KML)
- route plans (polylines + checkpoints)
- comms plan (rooms/channels + role access)
- effective window + version + provenance
- acknowledgments (“loaded”) + audit trail
Think: distribute once, verify everyone loaded the same picture, then operate.
- App layer (stable): identity, RBAC, mission packages, COP, reporting/tasking.
- Transport layer (replaceable): internet relay now; local/degraded options later.
This keeps the UX and data model stable while transports evolve.
- multiple tokens + rotation/revocation (already supported via
INVITE_TOKENS) - room-scoped tokens (token grants access to a subset of rooms)
- basic audit logging: unauthorized connect attempts + denied joins
- client: pending queue survives refresh (IndexedDB) + retries with backoff
- relay: dedupe evicts by age, not only by max size
- optional: server adds
serverReceivedAt/seqper room for stable ordering
- Leaflet COP view
- layer manager (markers/lines/polygons)
- import/export overlays (GeoJSON)
- incident-scoped, opt-in location sharing with precision controls
- create/version/export/import mission packages
- required acknowledgments (“loaded by X”) + provenance
- role-based distribution (who can publish vs consume)
- report templates (SITREP/hazard/task)
- assignments + acknowledgments + completion states
- leader/dispatcher view (governance + accountability)
- HTTPS (required for many browser APIs later)
- rate limiting + abuse protection
- tighten CORS to real domains once deployed
- retention controls + export policy
- offline tiles (optional)
- store-and-forward sync
- conflict rules for overlay edits
- WebRTC data channel fallback
- local-first sync patterns
- BLE / Wi‑Fi Direct experiments via a transport adapter interface
- This is not a system proxy, MITM, or traffic interception tool.
- This is not a “surveillance platform.” Visibility is role-based and policy-driven.
- This is not a guaranteed substitute for professionally managed emergency systems.
- Use this only where you have authorization and a legitimate purpose.
- Default posture should minimize sensitive data (especially location).
- Treat device compromise as normal: plan for offboarding/revocation.
- v0: chat rooms + invite tokens + allowlisted rooms + acks/dedupe/retry
- direction: BFT-inspired COP with mission packages, overlays, and incident workflows