Three-party architecture
Everything runs through the registry — the central server that owns intents, offers, session tokens, and vendors. Each party talks only to the registry; there is no direct vendor-to-buyer channel. That's how privacy stays a type decision rather than a trust decision.
Request lifecycles
The two flows that matter: a buyer creating an intent, and a vendor submitting an offer. Every other endpoint is a variation on these.
Create-intent lifecycle
Submit-offer lifecycle
On auto-accept, the server creates a session token st_<uuid> bound to (vendorId, intentId) with a 24-hour TTL and returns it alongside the offer. The vendor now has Tier 2 access until the token expires.
Data layer
Four tables. Foreign keys on. WAL mode. JSON columns for ragged structures like attributes, constraints, and tags. Budget and location are split-column for index and filter efficiency.
- intents. The primary resource. Composite index on
(status, category, metro)drives registry filtering. - vendors. One row per registered vendor. The
apiKeycolumn stores a hashedpak_<uuid>; the plain key is returned exactly once at registration. - offers. One row per offer with server-computed
score. Status enum mirrors the protocol:pending,accepted,rejected,countered,expired. - session_tokens. Short-lived,
(vendorId, intentId, tier)-scoped. The registry consults this table on every Tier 2 request.
Why SQLite (for now)
SQLite is not a compromise on a reference implementation — it's the right tool. The design target is single-file, single-node, zero-config. WAL mode gives us concurrent readers without locking. The synchronous better-sqlite3 API collapses the connection-pool layer entirely; route handlers can use it like a local data structure.
The roadmap names the migration path to Postgres for multi-node deployments — and schema has been written with that in mind. No SQLite-only tricks, no sharded primary keys to unwind.
Frontend architecture
A Vite SPA in client/. Five screens: buyer dashboard, intent detail, create-intent form, public registry, vendor portal (with sub-views). One API client in client/src/api/client.ts; all server state lives there. The useEvents() hook subscribes to the SSE stream and triggers targeted refetches on intent_* and offer_* events.
Real-time layer
A single in-memory Set<Response> holds open SSE connections. Route handlers call emitEvent(type, data) after any mutation; the emit helper writes to every connected client. No queue, no broker, no retry logic — the client-side EventSource reconnects automatically on drop.
Four event types fire today:
| Event | Emitted from | Subscribers refetch |
|---|---|---|
intent_created | POST /api/intents | Vendor registry view |
intent_updated | PATCH /api/intents/:id | Buyer dashboard; affected vendors |
offer_created | POST /api/offers | Buyer intent-detail view |
offer_status_changed | Accept / reject / counter handlers | Vendor dashboard; buyer intent-detail |
Stack rationale
Every choice below was made to keep the reference implementation small, readable, and easy to replace. No layer is load-bearing beyond its role.
| Layer | Choice | Why |
|---|---|---|
| Backend | Node.js | Single runtime for HTTP, MCP, and scripts. Fast to iterate on. |
| Backend | TypeScript | Share types between server, client, and MCP without duplication. |
| Backend | Express | Universal, boring, composable. No framework-religion fights on a reference impl. |
| Backend | SQLite (WAL) | Zero-config, single-file, embedded. WAL mode handles the single-node concurrency we need. |
| Backend | Zod | Single source of truth for types and runtime validation of every request body. |
| Agent | MCP SDK | Protocol standard. Every MCP-compatible client gets People API for free. |
| Frontend | React 19 | Reference SPA for humans who want a UI. |
| Frontend | Vite | Fast dev, clean prod build. Express owns prod asset serving. |
| Frontend | Tailwind | Opinion-free styling. No bespoke design system needed for a reference UI. |
| Frontend | React Router 7 | Client routing, five screens, no SSR needed. |
| Realtime | Server-Sent Events | One-direction push. No WebSocket overhead, trivial to proxy. |
| Testing | Vitest | Fast, first-class TS, identical assertion surface to Jest. |