This markdown file is the canonical architecture reference. If an
ARCHITECTURE.pngexists in the repo, it is deprecated in favor of this document (which can be version-controlled and diffed).
| Layer | Technology |
|---|---|
| <<<<<<< Updated upstream | |
| Frontend | Next.js 16 (App Router, no src/), React 19, CSS Modules |
| Backend | Next.js API Routes (serverless, Vercel) |
| Database | Supabase — PostgreSQL + pgvector + Auth + RLS |
| AI Agents | Anthropic Claude API (Sonnet for agents, Haiku for routing) |
| Embeddings | Voyage AI (voyage-3-lite, 512-dimensional) |
| Ingestion | GitHub Actions — manual-trigger workflow per game system |
| Hosting | Vercel |
| ======= | |
| Framework | Next.js 15 (App Router, no src/), React 19, TypeScript |
| Backend | Supabase — PostgreSQL, pgvector, Auth (Google SSO), Row Level Security |
| AI — agents & router | Anthropic Claude API |
| AI — embeddings | Voyage AI (voyage-3-lite, 512 dims) |
| Hosting | Vercel |
| Ingestion | GitHub Actions |
| Styling | CSS Modules (per-component .module.css) |
| Protocol | MCP server embedded within the Next.js app |
Stashed changes
Auth is Google OAuth SSO only. Each user's data is isolated at the Postgres level via RLS.
flowchart TD
Player[Player sends message] --> Route["/api/sessions/[sessionId]/message"]
Route --> Router["routeMessage() — Haiku classifier<br/>lib/mcp/coordinator.ts"]
Router -->|narration| Narrator[Narrator Agent]
Router -->|rules query| Rules[Rules Arbiter Agent]
Router -->|recall| Lore[Lore Keeper Agent]
Router -->|NPC speech| NPC[NPC Dialogue Agent]
Router -->|encounter| Enc[Encounter Builder Agent]
Rules --> Voyage[Voyage embed query]
Voyage --> PgVector[(pgvector<br/>match_rules_embeddings)]
PgVector --> Rules
Lore --> Transcript[(sessions.transcript<br/>+ campaigns.notes)]
NPC --> NpcTable[(npcs table)]
Enc --> PgVector
Narrator --> Claude[Anthropic Claude API]
Rules --> Claude
Lore --> Claude
NPC --> Claude
Enc --> Claude
Claude --> SSE[SSE stream to client]
SSE --> Persist[(Save to sessions.transcript)]
The orchestrator does routing, not coordination. For each player message:
- The session message route calls
routeMessage()inlib/mcp/coordinator.ts. - That function sends the message plus a disambiguation prompt to Claude Haiku (fast, cheap) and gets back a single classification — which one of the five agents handles this message.
- Exactly one agent runs. Agents do not call each other. The "multi-agent" experience comes from different messages across a conversation going to different specialists, not from agents collaborating on a single response.
Conversation history is shared across all agents. Each prior assistant message is annotated with an [Agent Name] prefix so an agent does not mistake another agent's statements for its own. A cross-agent trust rule in each agent's system prompt establishes that prior agents' statements of campaign fact are canonical and must not be disavowed.
| Agent | Context it gathers | Label color |
|---|---|---|
| Narrator | Scene narration + previous session's summary (cross-session continuity) | Gold |
| Rules Arbiter | Voyage-embedded query → match_rules_embeddings (pgvector, threshold 0.3, active generation, game-system filtered) → cited SRD chunks |
Slate |
| Lore Keeper | campaigns.notes + last 5 sessions' transcripts |
Purple |
| NPC Dialogue | Campaign NPC roster (disposition, known facts, personality); may emit player-confirmed proposals | Green |
| Encounter Builder | Party composition + monster index from the embeddings | Amber |
Core Postgres tables: campaigns, characters, sessions, npcs, campaign_embeddings.
- RAG:
campaign_embeddingsholds baseline rules chunks (campaign_id NULL) and may hold per-campaign content. pgvector powers similarity search viamatch_rules_embeddings. - Generation swap: baseline embeddings are versioned by a
generationcolumn with anembedding_generationsstate table, enabling zero-downtime re-ingestion (write new generation, atomically promote, delete old). - RLS: every table is owner-scoped (
auth.uid() = owner_id) for per-user isolation. - NPC roster:
npcsper campaign with a 5-value disposition enum and structuredknown_factsJSONB. - Sessions: find-or-create logic (one active session per campaign), transcript persisted per exchange, AI-generated summary on end, summary injected into the Narrator at the next session's start.
Baseline rules content is ingested via a GitHub Actions workflow (workflow_dispatch). The pipeline fetches from source (Open5e for 5E SRD, with document__slug=wotc-srd filter), chunks, embeds via Voyage, and writes under a new generation, promoting atomically on success. 5E_2014 has ~2335 chunks. ADD2E and PATHFINDER_2E are stubs (training-knowledge fallback / data deferred).
Agent responses stream to the client via Server-Sent Events:
event: agent_type → which agent is responding (sets label immediately)
event: token → incremental text chunks
event: proposal → NPC proposal block (NPC Dialogue only, after narrative)
event: done → stream complete
The route strips NPC proposal sentinel blocks out of the streamed text and emits them as a separate proposal event so markers never appear in chat. The full response is accumulated server-side and saved to sessions.transcript.
Desktop sync from Fantasy Grounds into RoleVerse is complete — FG backup-file parsing maps rulesets to game systems and characters into the app.
- TTS / voice output for NPCs (optional, off-by-default)
- Voice input via browser microphone → Whisper (no Discord)
- PDF ingestion + house rules editor + Lore Keeper semantic search
- Kanka integration for external lore management
- PF2E proper data sourcing
Discord integration was evaluated and removed from the project — it is not planned.