Architecture and the legal record
One process, one append-only log, hash-addressed originals and a SQLite projection you can always rebuild.
BINA is a single Node.js process. It owns the HTTP API, the ingest scheduler and webhook delivery. That simplicity is a design choice: with one writer, the order of events is never in question, and the record a lawyer cites is easy to audit.
#Three stores, two of them authoritative
| Store | Path | Role |
|---|---|---|
| Event log | data/events.jsonl | Authoritative. Append-only, fsynced per line. Every counsel event and extraction outcome. |
| Originals | data/raw/<sha256>.gz | Authoritative. The fetched bytes of every source, addressed by hash and never deleted. |
| Derived database | data/reference.db | A projection. Rebuilt by replaying the log; never cited. |
Reports cite hashes, so a reader can fetch the exact bytes a recital was parsed from. Because the database is derived, a corrupted or deleted SQLite file is not data loss — replayRadarEvents folds the log back into the tables at startup.
#Write path
Every state-changing radar route follows the same order:
request → validate → store.recordRadarEvent() → apply to SQLite → respond
(fsync to events.jsonl)If the process dies between the log write and the SQLite write, the next start replays the event and the derived tables catch up. A torn last line in the log is ignored.
#Request path
console (React, :5173) ──/api──▶ node:http (:8787)
│ Host / Origin guard
│ optional review password
│ optional operator token → attribution
▼
route table (server/index.mjs)
▼
server/radar/* ←→ SQLite projectionThere is no HTTP framework and no ORM. Routes are pathname matches in one handler; SQL is inline in the modules that use it.
#Where the code lives
| Area | Files |
|---|---|
| Routes | server/index.mjs |
| Level table | server/radar/levels.mjs |
| Validation | server/radar/contract.mjs |
| Quote verification | server/radar/text.mjs |
| Ingest | server/radar/ingest.mjs, sources.mjs |
| Bulletin / report | server/radar/bulletin.mjs, report.mjs, export.mjs |
| Extraction | server/radar/llm.mjs, extract.mjs, dspy.mjs |
| DMA / cartel | server/radar/dma.mjs, dma-seeds.mjs, cartel.mjs, legal-map.mjs |
| Signals / research | server/radar/signals.mjs, claimant-research.mjs, population.mjs |
| Watchlist | server/radar/watchlist.mjs |
| Tests | test/radar.test.mjs |
#Stack
Node ESM (.mjs), node:http, better-sqlite3 (WAL, synchronous = FULL), React 19 console on Vite 6, and Node's built-in test runner. The only model dependency is the Vercel AI SDK, restricted to grounded extraction proposals.
#Deployment shape
Locally, the API binds 127.0.0.1 only. The team review workspace serves the console from Vercel and forwards /api/* to a container with a persistent data volume. Bulk ingest, registry enrichment and webhook delivery run in the local publisher; the hosted workspace answers 501 for those.
#Webhooks
Radar events go outward as HMAC-SHA256-signed webhooks with a 300-second replay window: radar.document.published (a new level 1 or 2 document after an ingest) and radar.watchlist.matched. Targets are SSRF-guarded: no plain http and no private addresses unless CLEARY_WEBHOOK_ALLOW_PRIVATE=1.