A field-coordination platform for NGOs operating in low-connectivity warzones. Civilians signal sightings and needs over SMS, app push, and a Bluetooth-mesh app (bitchat). SafeThread fuses those signals through an LLM-driven matching engine, surfaces the right cases to operators in a console, and broadcasts decisions back over the same channels β push-first.
A child goes missing in Aleppo. The family member walks into an NGO field office or texts a tip line. Half the city has no internet; cell towers are intermittent; some people only have a $10 dumbphone. SafeThread lets the NGO:
- Take that report through whatever channel reached them β typed at a console, an SMS, an app push, or a Bluetooth message that hopped phone-to-phone until it hit someone with internet.
- Triage it automatically. A small LLM classifies every inbound: missing person Β· medical Β· resource shortage Β· safety Β· noise Β· bad actor. Nothing legitimate gets dropped because an operator was busy.
- Coalesce the noise. When 200 people text about the same incident, the system groups them into one case (by alert, region, time window) so an operator looks at one cohesive view, not 200 rows.
- Broadcast back out. Operator (or the agent, for safe actions) sends an Amber Alert to the right audience: civilians in that region, an NGO partner, or a rescue team. The system prefers free push; falls back to SMS; falls back to bitchat mesh when nothing else works.
- Thread replies onto the case. Civilians who text a sighting back land directly on that case's timeline β not in an undifferentiated inbox. (See How an Amber Alert flows below.)
The product is operator-in-the-loop by design. Low-risk actions (recording a sighting, tagging a case) auto-execute. High-blast-radius actions (mass broadcasts, marking someone a bad actor) route to the operator console as suggestions to approve, edit, or reject.
This is the round-trip you can demo end-to-end on a laptop with a Twilio number and an ngrok tunnel:
- Operator creates a case in the NGO console β
Michiel, 29 β male, white Β· last seen JPH, Amsterdam. The system allocates a 4-character civilian-facing reply code (e.g.K883) and shows it on the case header. - Operator clicks Send Amber Alert. The composer auto-drafts the SMS body:
AMBER ALERT β Michiel, 29 β male, white. Last seen JPH, Amsterdam. Reply: "K883 <your message>" - Civilians get the SMS (push if they have the app; otherwise SMS via Twilio; otherwise bitchat mesh hop).
- Civilian replies by text:
K883 Saw him near Centraal Station 14:30. - Inbound webhook strips the prefix, looks up the active alert by code, sets
in_reply_to_alert_id, and persists the message. The triage worker buckets it onto that case. - The case timeline updates live in the operator console: the new reply appears on the case (not in the global wire), the case
MSGcount ticks up, and the operator sees a real conversation building.
A reply without the code prefix still shows up β but on the global wire as an unthreaded civilian message, with a Make a case affordance for the operator. So the system degrades gracefully when civilians forget the code.
For a fuller walkthrough with diagrams (including the BLE store-and-forward path for civilians who have no cell), see visuals/README.md.
- Matching engine + console: end-to-end on
main. Inbound β triage β buckets β agent β console works in real mode (Opus 4.7) and stub mode (no API key needed). - SMS round-trip: Twilio outbound + inbound webhook + reply-code threading: shipped.
- DTN library + mesh transport adapters: shipped on the
matching-enginebranch. - Real outbound dispatcher (push + audience cascade): roadmap.
- Hub-side BLE radio: stretch goal (skeleton lands the right shape; needs a Pi to validate).
Five execution nodes, four DB-coupled stages, one operator-in-the-loop surface. Every contract is a Postgres table; every node is restartable, replayable, auditable.
βββββββββββββββββββββββββββββββββββββββββββββββ
β CIVILIAN DEVICES β
β (SafeThread iOS β bitchat-derived) β
β SMS Β· app push Β· BLE mesh β
ββββββββββββββββββ¬βββββββββββββββββββββββββββββ
β
ββββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββ
β β β
βΌ βΌ βΌ
carrier SMS POST /api/sim/inbound (HTTP) POST /app/dtn/deliver
(sms_base) (DTN sealed bundle)
β β β
ββββββββββββββββββ¬ββββββββββββββββ΄βββββββββββββ¬ββββββββββββββββ
β β
βΌ βΌ
βββββββββββββββββββββββ ββββββββββββββββββββββββββββ
β API tier β β DTN dispatcher β
β (FastAPI mailroom) β β decode Β· seal-open β
β β β amber / sighting / chat β
ββββββββββββ¬βββββββββββ ββββββββββββ¬ββββββββββββββββ
β β
βΌ βΌ
ββββββββββββββββββββββββββββββββββββββββ
β InboundMessage β β DB-as-bus
βββββββββββββββββββ¬βββββββββββββββββββββ
β NOTIFY new_inbound
βΌ
ββββββββββββββββββββββββββββββββββββββββ
β Triage worker (Haiku) β
β classify Β· geocode Β· dedupe Β· embed β
βββββββββββββββββββ¬βββββββββββββββββββββ
β NOTIFY bucket_open
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββ
β Bucket (alert_id, geohash_4, time_window) β β coalescing primitive
βββββββββββββββββββββββ¬ββββββββββββββββββββββββββ
β pg_advisory_lock(alert)
βΌ
ββββββββββββββββββββββββββββββββββββββββ
β Agent worker (Sonnet/Opus) β
β retrieve β reason β 14 tools β
βββββββββββββββββββ¬βββββββββββββββββββββ
β
βββββββββββββββββββ΄βββββββββββββββββββββ
βΌ βΌ
AgentDecision row ToolCall rows (execute or suggest)
β β
β βββ auto_executed β dispatcher
β βββ pending β operator console
βΌ βΌ
WS /ws/stream βββββΊ NGO console (web/)
β²
β approve / reject / compose
β
human operator
β
βΌ
Outbound dispatcher
(push β SMS β mesh)
For the full breakdown β node-by-node contracts, all 17 tables, every API endpoint, the agent's tool surface, and the operating envelope β see ARCHITECTURE.md.
echo "ANTHROPIC_API_KEY=sk-ant-..." > .env # gitignored; LLM is optional, see below
docker compose up -d --buildOpen http://localhost:8080. The image runs migrations and starts the workers; with SEED_ON_BOOT=true (compose default) it also lands a populated demo scene. Set SEED_ON_BOOT=false in .env to start with an empty database.
Without an ANTHROPIC_API_KEY the triage and agent workers fall back to deterministic stubs β the demo still runs, the wire still moves, you just won't see real LLM reasoning in the console.
docker compose up -d db # Postgres + pgvector only
uv sync && uv run alembic upgrade head
uv run uvicorn server.main:app --reload --port 8080
cd web && npm install && npm run dev # Vite on :5173, proxies API β :8080The reply-code round-trip needs Twilio creds + a public webhook URL:
# .env
TWILIO_ACCOUNT_SID=...
TWILIO_AUTH_TOKEN=...
TWILIO_FROM_NUMBER=+1234567890
TWILIO_DEMO_RECIPIENT=+316... # trial accounts can only message verified numbers
# expose your local backend
ngrok http 8080
# then in the Twilio console set the SMS webhook to:
# https://<your-tunnel>.ngrok.io/webhooks/twilio/smsWe ship to boxd.sh β a one-command VM platform with a built-in TLS-terminating subdomain proxy. An NGO can be live in under 3 minutes with no infra knowledge.
# 1. From your laptop β provision a VM with a memorable name.
ssh boxd.sh
boxd create safethread # creates safethread.boxd.sh with TLS
# 2. Open a shell on the VM.
boxd connect safethread
# 3. Clone + create .env (all secrets stay on the VM, never committed).
git clone https://github.com/<your-fork>/anth-hackathon26.git
cd anth-hackathon26
cat > .env <<'EOF'
ANTHROPIC_API_KEY=sk-ant-... # required for real-LLM mode
APP_PASSWORD=changeme # shared operator login
SEED_ON_BOOT=false # NGO deploy = clean DB
REPLAY_AUTOSTART=false # no demo drip in production
TWILIO_ACCOUNT_SID=AC...
TWILIO_AUTH_TOKEN=...
TWILIO_FROM_NUMBER=+1... # buy a number in the Twilio console
TWILIO_DEMO_RECIPIENT=+316... # optional: pin every send to one phone
TWILIO_MAX_RECIPIENTS=25
RESCUE_TEAM_RECIPIENTS="+316...:Alice,+44...:Bob" # quote if any name has spaces
EOF
# 4. Boot.
docker compose up -d --buildThe console is now live at https://safethread.boxd.sh with TLS, the login screen, and an empty database ready for real traffic.
Wire Twilio so SMS replies land on the deploy:
- Twilio console β Phone Numbers β Active Numbers β click your number.
- Messaging Configuration β "A message comes in" β Webhook β
https://safethread.boxd.sh/webhooks/twilio/smsβ HTTP POST β save.
Updating after a code change:
git pull && docker compose up -d --buildThe Postgres volume persists across rebuilds (pgdata), so cases, accounts, and history survive. To wipe and start clean: docker compose down -v.
server/ FastAPI app + asyncio workers (triage, agent, heartbeat)
web/ React/Vite NGO console
mobileapp/ bitchat-derived iOS app with SafeThread store-and-forward
alembic/ DB migrations
docs/ Specs and plans
visuals/ Architecture diagrams aimed at non-technical readers
tests/ pytest, one file per domain
For the deeper map (server/api/, server/workers/, server/dtn/, server/transports/), see ARCHITECTURE.md Β§3.
uv run pytest # 98 passing on main; +29 DTN tests on matching-engine
uv run pytest -k agent # agent-worker focused
uv run pytest -k dtn # DTN library (matching-engine)
uv run pytest -k transport # mesh adapters (matching-engine)For the real-LLM path: scripts/smoke_real_agent.py does a one-shot end-to-end test with a real ANTHROPIC_API_KEY. Validates SDK connect, multi-turn loop, tool dispatch, persistence. Costs ~$0.06 per run.
All settable via .env or the host shell.
| Var | Default | Effect |
|---|---|---|
DATABASE_URL |
postgresql+asyncpg://app:app@db:5432/matching |
Postgres URL |
ANTHROPIC_API_KEY |
β | enables real Haiku triage + Opus 4.7 agent. Without it, both fall back to deterministic stubs. |
JWT_SECRET |
change-me |
NGO operator JWT signing |
APP_PASSWORD |
β | shared password the operator console asks for on first load. Empty = no gate. |
HEARTBEAT_INTERVAL_SEC |
300 |
how often the heartbeat scheduler ticks each active alert |
HEARTBEAT_ENABLED |
true |
false to skip the heartbeat task entirely |
SEED_ON_BOOT |
true (compose) |
run the demo seeder if the DB is empty |
REPLAY_AUTOSTART |
true (compose) |
start the live drip a few seconds after boot |
REPLAY_INTERVAL_SEC |
4 |
drip cadence; lower = busier dashboard |
TWILIO_* |
β | live SMS; see Inbound SMS via Twilio |
RESCUE_TEAM_RECIPIENTS |
β | comma-separated +phone:Name list for the rescue-team audience. Quote the value if any name has a space. |
ARCHITECTURE.mdβ the engineering reference: 5-node pipeline, DB-as-bus contracts, all 17 tables, every API endpoint and WS event, agent tool surface, operating envelope, build status. Start here if you're going to read or change code.visuals/README.mdβ three architecture views aimed at non-technical readers: topology, flow of one alert, and what the case ledger looks like at the end. Start here if you need to explain SafeThread to a partner.docs/superpowers/specs/2026-04-25-matching-engine-design.mdβ the canonical design spec the matching engine was built against.mobileapp/README.mdβ the iOS app (bitchat fork with SafeThread store-and-forward).
