technical deep-dive

Nexus Forge

An infinite-canvas mind-map with four AI personalities that read your thinking, stream their reasoning live, and restructure the board with a click. Here's how it works under the hood.

Nuxt 4
SPA + Nitro
LangGraph
2-node graph
SSE
token streaming
6
board actions
4
personalities
SVG
hand-drawn canvas
01 — the big picture

What is Nexus Forge?

It's a single-board mind-map you draw on an infinite SVG canvas — pan, zoom, branch, connect, rename. When you hit ask AI, the current graph is serialized and sent to a personality (cold analyst, weary maintenance bot, ancient oracle, chaotic repair droid). It streams a dry, opinionated analysis of your thinking, then proposes concrete edits — add a node, link two ideas, expand a thin branch, tidy the layout — each one a card you Apply with a click.

✶ Streaming-first

Server-Sent Events push the reasoning token-by-token. You watch the AI think, not a spinner.

✎ Applies itself

Suggestions aren't text — they're typed actions that mutate the board and ride the same undo stack as your own edits.

▤ Strictly layered

Data, AI, API and UI live in separate folders with a one-way import rule — no cross-layer shortcuts.

The stack

Nuxt 4 · Vue 3 · SPA Pinia · 3 focused stores LangGraph.js Fireworks.ai · OpenAI-compatible SDK Zod · schema = source of truth Nitro · SSE endpoint Upstash Redis · rate limit Vitest + Playwright
02 — architecture

Four layers, one rule

Code is organized by responsibility, and dependencies flow in one direction. The UI talks to the API; the API runs the AI; the AI and data layers know nothing about HTTP or Vue.

AI
lib/ai/
The LangGraph workflow (analyzer → suggester), the four personality prompts, Zod schemas, and the moderation pass. Pure orchestration — emits typed events, knows nothing about Nitro or components.
Data
stores/ + lib/mindmap/
Three Pinia stores (graph, AI, settings), the radial-layout algorithm, the graph serializer, and the action applier. The single source of truth for what's on the board.
API
server/ + composables/
The HTTP boundary: the Nitro /api/ai/analyze route (keys → rate-limit → moderation → stream) and the useAIAnalysis composable that consumes the SSE stream with an AbortController.
UI
components/ + pages/
The SVG canvas, toolbar, side note, AI panel and suggestion cards. Thin Vue wiring over the stores; viewport / drag / label-edit logic lives in focused composables.
UI API AI  ·  everything Data  ·  never the reverse
Why it matters: the AI layer is testable without a browser, the serializer/applier are pure functions, and a UI rewrite can't reach into model code. All constants (model id, pricing, rate limits, security headers) live in lib/config.ts — no magic numbers in logic.
03 — end to end

The journey of one analysis

From click to applied edit. Colours mark the segment: client, server pipeline, model loop, stream back.

1
Pick a personality & hit “analyze”
AIPanel → useAIAnalysis.analyze()
The active agent gates the run; the panel flips to the ideas tab and clears the previous result.
2
Serialize the board & POST
lib/mindmap/serializer.ts → /api/ai/analyze
Nodes become a compact SerializedGraph. The Fireworks key rides in a request header (never the body); an AbortSignal backs the stop button.
3
Server policy pipeline
server/api/ai/analyze.post.ts
A linear gauntlet: resolve key → rate-limit → validate (Zod, 50 KB cap, null-byte check) → moderation. Any failure short-circuits before a token is generated.
4
Two-node LangGraph runs
lib/ai/graph.ts · analyzer → suggester
The analyzer streams the reasoning text; the suggester reads that analysis and emits a typed list of board actions.
5
Events stream back as SSE
data: <json>\n\n
Each event is one frame: thinking text, then a suggestion per action, closed by a done frame carrying latency, tokens, cost and a truncation flag.
6
Client parses & the panel renders live
useAIAnalysis → useAIStore → AIPanel
Reasoning streams into the trace; each action becomes a suggestion card. Apply runs the action through applier.ts, mutating the graph store — on the same undo stack as manual edits.
04 — the wire format

The SSE streaming protocol

Every server→client message is one SSE frame: data: <JSON>\n\n. The JSON is one variant of the BoardStreamEvent discriminated union — defined once as a Zod schema, so a switch on event.type stays exhaustive at compile time.

EventFires when…Key payload
thinkingThe analyzer streams a chunk of reasoningtext
suggestionThe suggester emits one board actionaction: MindMapAction
doneThe run completeslatencyMs, tokens, costUsd, truncated?
errorValidation, moderation or model failuremessage

Server: emit → write

The graph is handed an emit() callback; each node calls it, and the route writes data: …\n\n straight to the Node response. A client disconnect aborts the in-flight model call.

Client: stream → store

useAIAnalysis reads the body through a TextDecoderStream, buffers partial frames, splits on \n\n, and routes each parsed event into the AI store.

05 — the brain

Two nodes, four personalities

A tiny LangGraph workflow: analyzer → suggester → END. The analyzer writes prose for a human; the suggester turns that prose into machine-applicable actions. Both call Fireworks through the OpenAI-compatible SDK.

🧠 analyzer

Streams an opinionated read of the board — what it's about, structural gaps, hidden connections, next moves. Identifies itself as Nexus Forge, never the model. Flags finish_reason: 'length' as truncation.

✦ suggester

Reads the analysis and returns a typed MindMapAction[], validated against the Zod action schema before it ever reaches the client.

The four personalities

Each wraps the same base prompt with a distinct voice — the analysis is identical in substance, wildly different in tone.

  • AXIOM-9 — cold analyst. “Your diagram is a cry for help.”
  • VERN — blue-collar bot. “I've seen worse. Not much worse.”
  • ORACLE-3 — ancient AI. “Watched empires fall. This too shall pass.”
  • PATCH — chaotic repair droid. “FASCINATING. Also a disaster.”
06 — what the AI can do

Six board actions

The suggester can only speak in this vocabulary — a discriminated union so every action is fully typed and the applier's switch is exhaustive. highlight touches AI state; the rest mutate the graph store (and ride its undo stack).

+ add_node

Adds a child under a parent, auto-placed to avoid siblings.

↗ link_nodes

Draws a cross-link — a non-tree association between two ideas.

✎ relabel

Renames a vague node to something sharper.

◎ highlight

Pulses a ring around nodes for a few seconds to draw the eye.

⊕ expand_branch

Adds several children to a thin branch in one move.

⊹ tidy_layout

Re-runs the radial layout to untangle the whole board.

07 — client state

Three focused stores

State is split by concern across three Pinia stores. Components import only what they need; there's no god-store and no facade in between.

useGraphStore

Nodes, cross-links, selection, the current tool, undo/redo history, and debounced localStorage persistence.

useAIStore

Streaming reasoning, suggestions, highlights, the active agent, and the cached last result.

useSettingsStore

The single user-selectable accent colour, persisted and pushed to a CSS variable.

One careful detail: every ref is wrapped in skipHydrate() so the SPA's client-only state never trips Nuxt's SSR hydration. Viewport, node-drag and label-edit logic live in their own composables, keeping the 400-line SVG canvas about rendering, not bookkeeping.
08 — safety & limits

Guardrails before generation

Several independent checks run before the model sees a request — and the optional ones degrade gracefully when their backing service is missing.

🛡 Moderation — two passes

1. A fast, always-on jailbreak regex scan (zero cost). 2. If an OpenAI key is set, the text goes to the Moderation API. It fails open — an outage never blocks a legit user. Blocks come back as an SSE error frame, not a 4xx.

⏳ Rate limiting — two tiers

Prod: Upstash Redis, keyed by API-key suffix or IP. Dev: an in-memory bucket. The shared demo key gets a tighter budget than your own key; a 429 carries Retry-After.

Rate-limit budgets (per key / hour)

demo key
20
your own key
100

Plus a strict CSP, exact-origin CORS, and a key that's server-only — it travels in a header and is never logged. microphone=(self) keeps voice input working under the Permissions-Policy.

09 — the tuning knobs

Key numbers

A core rule of the codebase: no magic numbers. Every constant below lives in lib/config.ts.

Model

model minimax-m2p7 temperature 0.3 max tokens 2048

Validation

payload cap 50 KB prompt cap 2000 chars null-byte rejected

Limits

demo 20 / hr own key 100 / hr window 3600 s

Pricing /1M tok

in $0.60 · out $0.60 cost shown per run

Graph

nodes analyzer → suggester actions 6 kinds events 4 types

Tested with

vitest unit + component playwright golden path CI typecheck → e2e