FernOS is a crisis operating system for Nashville built around one shared live graph. Citizens use StormPath to find safe routes to warmth, food, water, and rides. Responders use CrewIQ to sequence tree crews ahead of electrical crews, prioritize circuits by vulnerability, and react to live crowd reports in the same data model.
The shared sign-in flow routes citizens into StormPath and responders into CrewIQ from one front door.
| StormPath routing | Community support chat |
|---|---|
![]() |
![]() |
Citizens can find safer routes to food, heat, water, and rides while also opening support threads and neighborhood conversations.
| CrewIQ dashboard | Circuit detail and dispatch |
|---|---|
![]() |
![]() |
Responders get neighborhood risk scoring, likely next hotspots, pre-staging recommendations, and live circuit-level operations.
- Shared backend graph for roads, resources, circuits, crews, jobs, crowd reports, needs requests, storm-watch state, chat threads, and recommendations.
- Modified Dijkstra router with time-decaying edge weights and top-3 ranked route responses.
- Citizen StormPath map with live road coloring, resource pins, route overlay, report modal, support-request workflow, community chat, and surplus resource posting.
- Responder CrewIQ dashboard with live metrics, priority map, assignment flow, needs matching, job sequencing, storm-watch toggle, and actionable pre-staging recommendations.
- Socket.io synchronization between citizen and responder views.
- Anthropic-backed NLP classifier with heuristic fallback.
- FastAPI ML microservice scaffold with XGBoost training and prediction endpoints.
- Postgres/PostGIS schema, DB-backed shared graph runtime, responder account storage, and import tooling.
- File-backed demo graph plus demo citizen and responder registries so runtime data is no longer embedded in source arrays.
- Admin import routes and a CLI bootstrap flow for refreshing demo data without code edits.
The repo already had a strong UI scaffold and a small in-memory responder demo, but several core files were placeholders:
server/src/engines/*were mostly empty.server/src/db/*, auth middleware, workers, and SQL assets were blank.ml-service/files were placeholders.- Top-level docs, scripts, env example, and Docker compose were empty.
- The citizen UI still depended on mock route/resource data instead of shared state.
- Frontend: React, TypeScript, Vite, Leaflet, Socket.io client
- Backend: Node.js, Express, Socket.io,
pg - Database: PostgreSQL + PostGIS
- ML service: FastAPI + XGBoost
- NLP: Anthropic Claude API with heuristic fallback
FernOS is designed as one shared operational graph with two different operator experiences on top of it:
- StormPath for citizens
- CrewIQ for responders
Both experiences read from and write back into the same live state model so that road reports, resource availability, crew actions, outage status, and support requests propagate through one system instead of being split across separate tools.
flowchart LR
Citizen["Citizen UI<br/>StormPath"] --> Client["React + Vite client"]
Responder["Responder UI<br/>CrewIQ"] --> Client
Client <-->|REST + Socket.io| API["Node.js + Express API"]
API --> Router["Routing engine<br/>modified Dijkstra"]
API --> NLP["NLP classifier<br/>Claude or heuristic fallback"]
API --> Agent["Resilience agent<br/>risk scoring + hotspot logic"]
API --> Workers["Background workers<br/>decay, scoring, prediction"]
API --> Store["Shared graph store abstraction"]
Store --> PG["Postgres + PostGIS"]
Store --> Demo["Demo JSON graph fallback"]
Workers --> ML["FastAPI ML service<br/>failure prediction"]
ML --> API
The shared graph is the system spine. Every major workflow resolves to one or more of these entities:
road_segments: traversable network edges used by StormPath routingresource_nodes: shelters, food banks, businesses, neighbors, and support sitescircuit_segments: outage and vulnerability regions for CrewIQ prioritizationcrew_units: live responder units and current statusjobs: dispatch work items, including blocked tree-first and ready electrical jobscrowd_reports: citizen or crew hazard signals that update route penaltiesneeds_requests: support requests for food, heat, rides, water, or medical helpchat_threadsandchat_messages: community rooms plus private support threadsapp_state: global flags such asstorm_watch_mode
This graph exists in two interchangeable runtime backends:
PostgresGraphStorefor Postgres/PostGIS-backed operationDemoGraphStorefor file-backed demo mode whenDATABASE_URLis unavailable
The store manager chooses Postgres when it can bootstrap successfully, otherwise it falls back to the demo graph without changing the API contract.
The client is a single React app with route-level experiences:
/shared auth entry point/doorrole-based launcher after citizen login/citizenStormPath map, routing, reporting, support requests, and chat/responderCrewIQ dashboard, dispatch, needs queue, AI brief, and outage map
Frontend state is partitioned into small stores:
- graph store for roads, routes, resources, reports, and needs
- responder store for circuits, crews, jobs, recommendations, and metrics
- chat store for support threads and community rooms
- user/session store for citizen and responder identity
Socket.io pushes shared graph updates into those stores at the app root through useSocketSync.
Express is organized around product surfaces instead of technical layers:
/api/authfor citizen registration, citizen login, responder login, and session introspection/api/citizenfor graph reads, route computation, surplus resource creation, and support requests/api/reportsfor crowd hazard submissions and reconfirmation/api/chatfor authenticated thread and message access/api/responderfor dashboard reads, dispatch actions, storm-watch control, and AI operations/api/adminfor dataset bootstrap and import flows
Every route writes through the same GraphStore contract, which is responsible for:
- querying the shared snapshot
- computing routes
- creating and matching needs requests
- managing jobs and crews
- mutating circuit state
- updating road penalties
- applying prediction results
This keeps business behavior consistent across demo mode and Postgres mode.
Postgres + PostGIS is the canonical persistence model. Geometry is stored with SRID 4326 and used for:
LineStringroad geometryPointresource, crew, and report locationsPolygoncircuit operating areas
The main persistence patterns are:
- route edges live in
road_segments - destinations live in
resource_nodes - outage zones live in
circuit_segments - assignment state lives in
jobsandcrew_units - citizen support state lives in
needs_requests - authenticated social coordination lives in
chat_threadsandchat_messages
Operationally, the README schema now reflects the real runtime fields, including:
priority_scorefailure_probabilitycoordinating_with_crew_idblocked_by_job_id- support-thread linkage from
needs_requests.thread_id - hashed citizen and responder credentials in
citizen_accountsandresponder_accounts
FernOS uses Socket.io as the live graph fan-out layer.
When a client connects:
- the server emits
graph:snapshot - the client hydrates citizen and responder stores from the same snapshot
- subsequent deltas arrive as entity-specific events
Primary broadcast events include:
road:updatedreport:createdreport:updatedresource:updatedneeds_request:updatedcircuit:updatedjobs:updatedcrew:updateddashboard:updatedstorm_watch:updatedzone:clearedpower:restored
Chat is intentionally handled a little differently right now:
- support and community messages are authenticated over HTTP
- private support-thread safety is favored over broad socket fan-out
- the client store already has socket handlers for chat events, so room-based realtime chat can be layered in later without changing the high-level shape
StormPath is built around three loops that all touch the same graph.
- citizen selects needs such as heat, food, water, or ride
- client sends
origin,needs, andcurrentTimeto/api/citizen/routes - backend computes top routes against current road penalties and resource fit
- UI renders the ranked destinations and animated map route
Routing is graph-based rather than external-map-API based. The server builds an in-memory graph from road_segments and runs a modified Dijkstra search over weighted edges.
Edge weight:
base_weight + decayed_ice_penalty + decayed_debris_penalty
Decay model:
decay_factor = e^(-1.5 * age_hours)
Ranking model:
rank_score = travel_weight / max(resource_match_score, 0.25)
This means safer and shorter routes still matter, but a destination that actually satisfies the requested needs will rank better than a slightly closer but less useful destination.
- citizen submits a free-text or quick-tap report
- backend classifies the report with Claude when an API key is present
- if Claude is unavailable, the heuristic classifier extracts hazard, urgency, and summary
- urgency maps into road penalties and condition changes
- the updated road segment is broadcast to all connected clients
Penalty mapping:
- urgency
1-2: minor ice penalty - urgency
3: larger ice penalty - urgency
4: heavy debris penalty - urgency
5: effectively blocked edge
- citizen opens a support request
- backend creates a
needs_request - backend also creates a private support thread linked to that request
- responders can match the request to a resource and later resolve it
- the citizen sees status changes in the same shared graph
Parallel to that, citizens also participate in community rooms and post non-routing neighborhood updates.
CrewIQ is structured around circuit-level operations, job sequencing, and daily resilience monitoring.
The responder dashboard combines:
- live circuit map
- active job queue
- crew availability
- support-request queue
- storm-watch recommendation state
- AI resilience brief
The top metric cards derive from shared graph state:
- active zones
- available crews
- critical jobs
- households affected
Circuit priority is recomputed by worker and on major workflow changes using:
priority_score =
(
(population_affected * 0.35) +
(vulnerability_score * 0.30) +
(hours_without_power * 0.20) +
(re_outage_risk * 0.15)
) / repair_complexity_estimate
This pushes vulnerable, high-population, long-duration outages upward while still discounting zones with unusually complex repairs.
The responder workflow enforces tree-first dependency handling:
- tree crews are assigned to blocked circuits
- electrical work remains blocked until the zone is cleared
tree_clearedupdates the circuit- affected jobs move from blocked toward ready/in-progress state
- updated circuits, jobs, roads, and metrics are broadcast immediately
This is the design core of the product: dispatch logic and citizen routing are linked through the same live operating picture.
Responders can also operate as social-good coordinators:
- view unresolved needs requests
- match a request to the best resource node
- resolve after handoff is complete
This extends FernOS beyond outage repair into daily neighborhood resilience operations.
FernOS has two intelligence layers plus one rules-based operational layer.
Purpose:
- classify unstructured crowd reports
- infer urgency, hazard type, confidence, and short summary
- translate free text into route-impacting penalties
Runtime path:
server/src/engines/nlpClassifier.ts- Anthropic Claude API when configured
- heuristic classifier when not configured
Purpose:
- estimate which circuits are most likely to fail during storm watch
Runtime path:
- FastAPI service exposes
/predict - Node prediction worker polls the ML service every 60 minutes while
storm_watch_modeis enabled - returned
failure_probabilityvalues are written back intocircuit_segments - responder recommendations are regenerated from that updated graph
This makes failure probability part of the same dispatch view rather than a separate analytics dashboard.
Purpose:
- neighborhood risk scoring
- likely next hotspot prediction
- pre-staging suggestions
- daily “what should responders do next?” recommendations
The resilience agent is not a separate model server. It is a backend intelligence layer that synthesizes:
- active outages
- vulnerability scores
- route disruptions
- crowd-report volume
- resource strain
- pending support requests
- ML failure probabilities
It groups signals by neighborhood, computes a composite risk score, predicts the dominant next issue type, and proposes pre-stage actions for available crews.
FernOS uses three background workers:
- decay worker every 10 minutes
- priority scoring worker every 5 minutes
- prediction worker every 60 minutes
Their responsibilities are:
- clear fully stale road penalties after the 2-hour report lifetime
- keep circuit and job priority scores fresh as outage duration increases
- ingest ML failure predictions during storm watch
This gives the system three time horizons:
- immediate: websocket deltas and direct route recomputation
- short-term: decayed road validity and priority refresh
- medium-term: forecast-driven pre-staging
FernOS has one shared auth surface with two identity classes:
- citizens
- responders
Citizens:
- register with name, email, and password
- receive signed session tokens
- can create needs requests and participate in chat
Responders:
- authenticate with
crew_idand responder code - receive signed session tokens with responder role
- are required for CrewIQ and dispatch actions
The server supports:
- DB-backed hashed credentials in Postgres
- demo-file-backed credentials when running without Postgres
FernOS is intentionally designed to run in two modes.
- no Postgres required
- file-backed JSON graph
- demo citizens and responders
- fastest path for judging and hackathon demos
- PostGIS-backed shared graph
- import/bootstrap pipeline
- persistent auth tables
- canonical path for a city or nonprofit pilot
Because both modes implement the same GraphStore contract, the product surface stays stable while the infrastructure matures.
The system is intentionally not just a once-a-year storm app. The same architecture supports:
- daily hazard reporting
- mutual aid and support matching
- community operations
- public-works style dispatch
- storm escalation when needed
That is the core product design choice: FernOS is useful on ordinary days, then becomes a crisis operating system without forcing the city or community to adopt a second platform.
- Export the environment variables you need from
.env.example. Fill inANTHROPIC_API_KEYif you want live NLP classification. - If you want Postgres mode, start a Postgres/PostGIS instance and export
DATABASE_URL. - Start the backend:
cd servernpm installnpm run dev
- Start the frontend:
cd clientnpm installnpm run dev
- Start the ML service:
cd ml-servicepython3 -m venv .venv && source .venv/bin/activatepip install -r requirements.txtuvicorn main:app --reload --port 8000
- To bootstrap Postgres from the demo assets:
export DATABASE_URL=..../scripts/seed_db.sh
If DATABASE_URL is unset or Postgres is unavailable, FernOS falls back to the file-backed demo graph in server/data/demo/graph.json.
Common local URLs during development are usually:
- Client:
http://localhost:5173 - Server:
http://localhost:3001 - ML service:
http://localhost:8000
Demo auth registries now live in responders.json and citizens.json instead of being embedded in the UI.
Demo logins:
- Citizen:
maya@fernos.demo/citizen2026 - Responder:
crew_tree_001/tree2026
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/fernos
ANTHROPIC_API_KEY=your_key_here
ML_SERVICE_URL=http://localhost:8000
JWT_SECRET=replace_with_a_real_secret
ADMIN_API_TOKEN=replace_with_an_admin_token
DB_AUTO_BOOTSTRAP_DEMO=true
SOCKET_PORT=3001
PORT=3001
CLIENT_ORIGINS=
RESPONDER_CREDENTIALS_PATH=
CITIZEN_CREDENTIALS_PATH=
DEMO_GRAPH_PATH=
VITE_API_BASE_URL=
VITE_SOCKET_URL=
NOAA_FORECAST_URL=
FERNOS_CIRCUIT_SEEDS_PATH=
FERNOS_TREE_CANOPY_DEFAULTS_PATH=The easiest way to deploy the full FernOS stack is with Render using the root render.yaml Blueprint.
fernos-client: Render Static Site for the Vite frontendfernos-server: Render Web Service for Express + Socket.iofernos-ml: Render Private Service for FastAPI predictionsfernos-db: Render Postgres database
FernOS uses:
- a long-running Express server
- Socket.io realtime updates
- a separate Python ML service
- Postgres with PostGIS
That makes Render a better fit for the full stack than Vercel, which is better used for the frontend only.
- Push the latest
mainbranch to GitHub. - Go to Render Blueprint Setup.
- Click
New Blueprint Instance. - Connect the GitHub repo:
- Render will detect render.yaml automatically.
- Approve the 4 resources:
- static site
- Node web service
- Python private service
- Postgres database
- Add optional environment variables only if you want them:
ANTHROPIC_API_KEYonfernos-server- custom domain settings later if needed
- Click
Apply. - Wait for the services to finish deploying.
- Open the
fernos-clientURL first.
- builds the React app from
client/ - deploys the Express backend from
server/ - deploys the FastAPI ML service from
ml-service/ - provisions Postgres
- connects
DATABASE_URLfrom Render Postgres automatically - connects the backend to the ML private service automatically
- points the frontend at the backend automatically
- rewrites SPA routes like
/citizenand/responderback toindex.html - bootstraps demo graph data into Postgres on first startup
After Render finishes:
- Open the frontend URL.
- Confirm the health endpoint on the backend:
https://your-backend-url/health
- Log in with the demo accounts:
- Citizen:
maya@fernos.demo/citizen2026 - Responder:
crew_tree_001/tree2026
- Citizen:
- If you want Claude-backed report classification, add
ANTHROPIC_API_KEYto the backend service in Render and redeploy that service.
The Blueprint is designed to get FernOS live with the least manual work. The frontend, backend, ML service, and database all deploy from one repo, but Render account access is still required for the final Blueprint launch.
FernOS now ships with:
server/src/db/schema.sqlserver/src/db/seed.sqlserver/data/demo/graph.jsonserver/data/demo/responders.jsonserver/data/demo/citizens.jsonscripts/seed_db.shscripts/reset_db.shscripts/bootstrap_demo_db.shserver/src/admin/importGraphCli.ts
server/src/db/seed.sql is now a compatibility shim. The canonical bootstrap path is the JSON import pipeline, which loads the shared graph and responder registry into Postgres from the files above.
To import demo data directly into Postgres without editing code:
- CLI:
cd server && npm run admin:import -- --replace - Script:
./scripts/bootstrap_demo_db.sh - API:
POST /api/admin/bootstrapwithx-admin-token: $ADMIN_API_TOKEN
Additional admin endpoints:
GET /api/admin/statusPOST /api/admin/import/graphPOST /api/admin/import/responders
- Route ranking uses weighted path cost divided by resource match score. This is an intentional correction of the prompt’s ambiguous "shortest weighted path x resource relevance" wording so better resource matches rank higher instead of lower.
- Storm watch predictions only affect recommendations when
storm_watch_modeis enabled. - If Anthropic or the ML service is unavailable, FernOS falls back to local heuristics instead of failing closed.
- The client now prefers same-origin API and socket connections unless
VITE_API_BASE_URLorVITE_SOCKET_URLis explicitly set. - Responder auth uses hashed codes in Postgres when
DATABASE_URLis configured, and falls back to the file registry only when FernOS is running in demo mode. - Citizen auth now uses the same shared auth entry point as responders, with hashed passwords in Postgres or the demo registry.
- Community chat is authenticated and fetched over HTTP so private support threads do not get broadcast to every socket client.
cd server && npm run buildcd client && npm run buildcd ml-service && python3 -m compileall .- Demo-mode smoke checks:
GET /healthGET /api/citizen/graphPOST /api/auth/login




