Repository navigation
02. Architecture Overview
AI Client is a full-stack Next.js 16 AI chat application built around three core ideas:
- Branching conversations — messages form a tree, not a flat list, enabling non-destructive edits and explorations.
- Real-time streaming — Server-Sent Events deliver text, reasoning tokens, and tool-call results as they arrive.
- Extensible tool use — the Model Context Protocol (MCP) allows external tool servers to be plugged in per-message.
The application supports multi-format artefact rendering (Markdown, HTML, spreadsheets, Mermaid), retrieval-augmented generation via hybrid vector + full-text search, and five authentication methods backed by PostgreSQL.
Purpose of this document: provide the high-level architectural overview that connects all specialised wiki pages. Each section summarises a subsystem and links to its dedicated deep-dive page. No section here duplicates the depth of those pages. For a complete feature catalogue, see the Features wiki page.
graph TB
subgraph Client
Browser["Browser<br/>(React 19 SPA)"]
end
subgraph "Next.js Server"
AppRouter["App Router<br/>API Routes + Server Actions"]
end
subgraph "Data Stores"
PG["PostgreSQL 17<br/>+ pgvector"]
S3["MinIO / S3<br/>(Object Storage)"]
end
subgraph "External Services"
OR["User's AI Provider<br/>(OpenAI-Compatible)"]
PM["Postmark<br/>(Transactional Email)"]
end
subgraph "Tool Servers"
MCP_HTTP1["MCP Server 1<br/>(Streamable HTTP)"]
MCP_HTTP2["MCP Server 2<br/>(Streamable HTTP)"]
end
Browser -->|"HTTP / SSE"| AppRouter
AppRouter -->|"SQL (Drizzle ORM)"| PG
AppRouter -->|"S3 API"| S3
AppRouter -->|"HTTPS (AI SDK)"| OR
AppRouter -->|"HTTPS (SMTP API)"| PM
AppRouter -->|"Streamable HTTP<br/>(MCP Protocol)"| MCP_HTTP1
AppRouter -->|"Streamable HTTP<br/>(MCP Protocol)"| MCP_HTTP2
Browser -->|"Presigned URL"| S3
| Path | Protocol | Notes |
|---|---|---|
| Browser ↔ Next.js | HTTP/1.1 + SSE | SSE for chat streaming; standard fetch for mutations |
| Next.js → PostgreSQL | TCP (SQL) | Via Drizzle ORM + pg driver |
| Next.js → MinIO/S3 | HTTPS (S3 API) | AWS SDK v3; presigned URLs for direct browser downloads |
| Next.js → AI Provider | HTTPS | Vercel AI SDK with provider's base URL and API key |
| Next.js → MCP Servers | Streamable HTTP | MCP Protocol, SSRF-protected |
| Next.js → Postmark | HTTPS | REST API for transactional email |
| Browser → S3 | HTTPS | Direct download via presigned URLs |
AI Client is a full-stack monolith — a single Next.js application that serves both the React frontend and all backend logic.
-
Server-side: API Route handlers for streaming responses; Server Actions for all data mutations. There is no
middleware.ts— all request-level protection happens inside handlers and actions. - Client-side: React 19 with a single Zustand store for global state. Optimistic updates are applied immediately; the server is the source of truth.
- No microservices, no message queues, no separate backend. External integrations (user's selected AI provider, MCP servers, Postmark) are called directly from the Next.js server process.
- Database-first: PostgreSQL is the single source of truth for all persistent state. pgvector provides vector similarity search for RAG.
graph TB
subgraph "Presentation Layer"
RC["React Components"]
ZS["Zustand Store<br/>(ChatSlice + EntitySlice)"]
HK["Custom Hooks"]
end
subgraph "API Layer"
AR["Route Handlers<br/>/api/chat · /api/rag/ingest/[documentId]<br/>/api/transform/run · /api/auth/[...all]"]
SA["Server Actions<br/>chats · projects · assistants<br/>prompts · knowledgebases · mcp-servers"]
end
subgraph "Business Logic Layer"
CP["Chat Pipeline<br/>System prompt · Tool registration · streamText"]
RP["RAG Pipeline<br/>Ingestion · Hybrid search · RRF"]
TP["Transform Pipeline<br/>Multi-step execution · Review gates"]
MO["MCP Orchestration<br/>Connection pool · Tool filtering"]
end
subgraph "Data Access Layer"
DR["Drizzle ORM"]
S3C["S3 Client"]
end
subgraph "Infrastructure"
PG["PostgreSQL 17 + pgvector"]
MIN["MinIO / S3"]
ORA["OpenRouter"]
MCPS["MCP Servers"]
PMK["Postmark"]
end
RC --> HK
HK --> ZS
RC --> AR
RC --> SA
AR --> CP
AR --> RP
AR --> TP
CP --> MO
CP --> DR
RP --> DR
TP --> MO
SA --> DR
SA --> S3C
DR --> PG
S3C --> MIN
CP --> ORA
MO --> MCPS
SA --> PMK
Each layer has a single responsibility:
- Presentation — rendering, local state, user interaction.
- API — authentication, validation, request routing.
- Business Logic — domain rules, orchestration, pipeline coordination.
- Data Access — query construction, storage I/O.
- Infrastructure — external services and databases.
For the complete schema reference, see the Database Design wiki page.
The application uses Next.js App Router with route groups to separate concerns and apply distinct layouts.
graph TB
RL["RootLayout<br/>(app/layout.tsx)"]
RL --> MAIN["(main)/layout.tsx<br/>AuthenticatedLayout + AppSidebar"]
RL --> PROFILE["profile/layout.tsx<br/>AuthenticatedLayout + ProfileSidebar"]
RL --> SETTINGS["settings/layout.tsx<br/>AuthenticatedLayout + SettingsSidebar"]
RL --> WORKFLOWS["workflows/layout.tsx<br/>AuthenticatedLayout + WorkflowSidebar"]
RL --> AUTH["auth/<br/>No layout wrapper (public)"]
AUTH --> AUTH_LOGIN["login/<br/>Sign-in and sign-up"]
AUTH --> AUTH_RESET["reset-password/<br/>Password reset flow"]
AUTH --> AUTH_2FA["2fa/<br/>Two-factor authentication challenge"]
MAIN --> CHATS["chats/[chatId]"]
MAIN --> PROJECTS["projects/[id]"]
MAIN --> ASSISTANTS["assistants/[id]"]
MAIN --> KBS["knowledgebases/[id]"]
MAIN --> SEARCH["search/"]
PROFILE --> PG["general/"]
PROFILE --> PS["security/"]
PROFILE --> PA["accounts/"]
PROFILE --> PSS["sessions/"]
PROFILE --> PD["danger/"]
SETTINGS --> ST["tools/"]
SETTINGS --> SP["prompts/"]
SETTINGS --> SAP["app/"]
WORKFLOWS --> WT["transform/[id]/runs/[runId]"]
WORKFLOWS --> WTR["translation/"]
-
Route groups (
(main)) influence layout nesting but do not appear in URLs. -
Dynamic segments (
[id],[chatId],[runId]) for entity detail pages. -
Private
_components/folders co-locate route-specific components within route directories. -
Error boundaries (
error.tsx) and not-found pages (not-found.tsx) at the(main)group level. - Each authenticated group wraps its content with
AuthenticatedLayout, which performs a session check and redirects unauthenticated users.
AI Client uses Better Auth v1.6.8 with five authentication methods:
| Method | Provider | Storage |
|---|---|---|
| Email + Password | Built-in (scrypt hash) |
account table |
| GitHub OAuth | GitHub |
account table |
| Discord OAuth | Discord |
account table |
| WebAuthn Passkeys | FIDO2 |
passkey table |
| TOTP 2FA | Built-in |
two_factor table |
- JWT tokens signed with HS256, stored in HTTP-only cookies.
- 7-day expiry with 24-hour silent refresh.
- Device metadata (IP, user agent) recorded for session auditing.
-
Server Actions —
requireSession()guard at the top of every protected action. Throws"Unauthorized"if no valid session. -
API Routes —
auth.api.getSession()called per-handler for streaming endpoints. -
Client —
AuthenticatedLayoutperforms a session check on mount and redirects to the sign-in page if unauthenticated.
There is no middleware.ts — all protection is application-layer.
sequenceDiagram
participant B as Browser
participant A as Auth API<br/>(/api/auth/[...all])
participant DB as PostgreSQL
participant C as Cookie Store
B->>A: POST /api/auth/sign-in (credentials)
A->>DB: Verify user + hash
DB-->>A: User record
A->>C: Set HTTP-only JWT cookie<br/>(HS256, 7-day expiry)
A-->>B: 200 OK + session data
Note over B: Subsequent requests
B->>A: POST /api/chat<br/>(with cookie)
A->>A: auth.api.getSession()<br/>Validate JWT
A->>A: Proceed with handler
A-->>B: SSE stream
B->>A: Server Action call<br/>(with cookie)
A->>A: requireSession()
A->>DB: Execute mutation
A-->>B: Action result
-
SSRF protection —
isBlockedUrl()blocks RFC 1918, localhost, link-local, and cloud metadata endpoints for all MCP HTTP URLs and provider base URLs. - BYOK model — users configure OpenAI-compatible providers (OpenRouter, Ollama, Groq, etc.) with encrypted credentials at rest (AES-256-GCM). See wiki/18.-Provider-Registry-&-Model-Management.md and wiki/17.-Per‐User-AI-Provider-&-API-Key-Encryption.md.
- Zod validation — applied at every system boundary (API routes, Server Actions, environment variables).
- No secrets in client bundles — all sensitive operations happen server-side.
📖 Deep dive: Authentication Architecture · AI Provider Key Encryption · Provider Registry & Model Management
The chat pipeline is the core feature of the application. Every AI response follows the same ten-step lifecycle.
sequenceDiagram
participant U as User (Browser)
participant Z as Zustand Store
participant S3 as MinIO/S3
participant API as POST /api/chat
participant Auth as Auth Layer
participant SP as System Prompt<br/>Assembly
participant MCP as MCP Servers
participant AI as OpenRouter<br/>(via AI SDK)
participant DB as PostgreSQL
U->>Z: Compose message<br/>(text, attachments, model, tools)
U->>S3: Upload attachments<br/>(via Server Action)
S3-->>U: Attachment keys
U->>Z: Optimistic insert<br/>(user + placeholder assistant message)
U->>API: POST /api/chat<br/>(message, metadata, selected tools)
API->>Auth: Authenticate + validate (Zod)
Auth-->>API: Session + user ID
API->>DB: Fetch user settings<br/>(global system prompt, model defaults)
API->>API: Resolve provider for selected model<br/>(decrypt provider credentials — BYOK)
API->>DB: Verify chat ownership
API->>SP: Assemble system prompt<br/>(user global → project → assistant → KB → spreadsheet)
API->>MCP: Connect selected servers<br/>(parallel, 10s timeout)
MCP-->>API: Tool registries
API->>API: Register tools<br/>(MCP tools + built-ins)
API->>AI: streamText() via selected provider<br/>(max 10 tool steps, 60s timeout)
loop SSE Stream
AI-->>API: Token / tool-call / reasoning
API-->>U: SSE event<br/>(text | reasoning | tool-call | tool-result | error)
U->>Z: Accumulate streaming content
end
AI-->>API: Stream complete
API->>DB: Persist user + assistant messages
API->>MCP: Close connections
API-->>U: SSE "done" event<br/>(final metadata)
U->>Z: Commit final state
- Per-message model selection — the model is chosen each time the user sends a message, not set globally. Stored in message metadata.
- Multi-provider architecture — users configure any OpenAI-compatible providers; the system dynamically resolves and routes requests based on model selection (see wiki/18.-Provider-Registry-&-Model-Management.md).
- 5-layer system prompt — assembled from the user's custom global system prompt, project prompt, assistant prompt, knowledge base instructions, and spreadsheet URLs.
-
Tool step limit —
streamText()allows up to 10 tool-use rounds per response, preventing runaway tool loops. - Optimistic rendering — the client inserts placeholder messages before the server responds, updating them as SSE events arrive.
📖 Deep dive: AI Responses and Streaming · MCP Integration · Prompts and System Prompts · Client-Side Attachment Pipeline
Messages are stored as an adjacency-list tree — each message has a parentId linking it to its parent. This is analogous to Git's branching model.
graph TD
S["System Prompt<br/>(root)"]
U1["User: Hello"]
A1["Assistant: Hi there!"]
U2["User: Tell me about X"]
A2["Assistant: X is..."]
U2b["User: Tell me about Y<br/>(edit → sibling)"]
A2b["Assistant: Y is..."]
A2c["Assistant: Actually, Y...<br/>(regen → sibling)"]
U3["User: Thanks"]
A3["Assistant: You're welcome!"]
S --> U1
U1 --> A1
A1 --> U2
A1 --> U2b
U2 --> A2
U2b --> A2b
U2b --> A2c
A2 --> U3
U3 --> A3
style S fill:#4a5568,color:#fff
style U1 fill:#3182ce,color:#fff
style A1 fill:#38a169,color:#fff
style U2 fill:#3182ce,color:#fff
style A2 fill:#38a169,color:#fff
style U3 fill:#3182ce,color:#fff
style A3 fill:#38a169,color:#fff
style U2b fill:#805ad5,color:#fff
style A2b fill:#805ad5,color:#fff
style A2c fill:#d69e2e,color:#fff
In the diagram above, blue = user messages on the original branch, green = assistant replies on the original branch, purple = an edit branch (sibling), gold = a regeneration (sibling assistant node).
-
parentIdlinks each message to its parent — stored in the database. -
childrenIdsare computed at runtime during hydration — never persisted. -
currentLeafIdon the chat record tracks which branch is active. - Edits create a sibling user node under the same parent.
- Regenerations create a sibling assistant node under the same parent.
-
Branch navigation walks the tree from root to
currentLeafIdto reconstruct the visible thread. - Two-pass hydration — DB rows are loaded into an in-memory hash map, then children are computed in a second pass.
📖 Deep dive: Message Tree and Branching
The application uses a single Zustand v5 store composed from two slices:
| Slice | Contents | Update Pattern |
|---|---|---|
| ChatSlice | Normalised hash map of messages, active chat ID, current leaf, tree operations | Optimistic: snapshot → mutate → rollback on failure |
| EntitySlice | Flat arrays for projects, assistants, prompts, MCP servers, knowledge bases, transform agents | Load-after-mutate: Server Action → reload from DB |
- No persistence middleware — all state is ephemeral. The store is rehydrated from PostgreSQL on navigation.
- Optimistic updates for chat — user and placeholder assistant messages are inserted into the store immediately. If the server persist fails, the store rolls back to the pre-mutation snapshot.
- Load-after-mutate for entities — after a Server Action completes (create, update, delete), the full entity list is reloaded from the database to ensure consistency.
-
Streaming accumulation — during SSE streaming, content is accumulated in React local state (not Zustand) to avoid excessive re-renders. The final content is committed to the Zustand store when the
doneevent arrives. - Selector-based access — components subscribe to specific slices of state via selectors, minimising unnecessary re-renders.
📖 Deep dive: State Management
The Model Context Protocol enables AI Client to call external tool servers during chat responses.
graph LR
subgraph "Chat API Handler"
REQ["Incoming Request"]
TR["Tool Registration"]
ST["streamText()"]
end
subgraph "MCP Connection Pool"
C1["Connection 1<br/>(HTTP)"]
C2["Connection 2<br/>(HTTP)"]
C3["Connection 3<br/>(HTTP)"]
end
subgraph "MCP Servers"
S1["File System Server"]
S2["Database Server"]
S3["Web Search Server"]
end
subgraph "Built-in Tools"
BT1["manage_artifact"]
BT2["search_knowledge_base"]
end
REQ --> TR
TR -->|"Promise.allSettled<br/>(parallel, 10s timeout)"| C1
TR --> C2
TR --> C3
C1 --> S1
C2 --> S2
C3 --> S3
TR --> BT1
TR --> BT2
TR --> ST
ST -->|"Tool calls"| C1
ST -->|"Tool calls"| C2
ST -->|"Tool calls"| C3
ST -->|"Tool calls"| BT1
ST -->|"Tool calls"| BT2
- Streamable HTTP transport — connections use the Streamable HTTP protocol exclusively.
- Per-request lifecycle — connections are created fresh at the start of each API call and torn down after the stream ends. No persistent connection pool.
-
Parallel connection —
Promise.allSettledensures a failed server never blocks others. - 10-second timeout on all MCP operations.
-
Namespaced tool IDs —
serverId:tool:toolNameprevents collisions across servers. - Per-message tool selection — users choose which servers and tools to activate for each message. Validation limits: 20 servers, 100 tools.
-
Built-in tools —
manage_artifact(creates/updates artefacts in the side panel) andsearch_knowledge_base(triggers RAG retrieval). -
SSRF protection — all HTTP MCP server URLs pass through
isBlockedUrl().
📖 Deep dive: MCP Integration
AI Client uses an agentic RAG design — the LLM autonomously decides when to search the knowledge base by calling the search_knowledge_base built-in tool.
graph TB
subgraph "Ingestion Pipeline"
UP["Upload Document"]
S3I["Store in S3"]
EXT["Text Extraction"]
CHK["Chunking<br/>(~400 tokens, 200-char overlap)"]
EMB["Embedding<br/>(2048-dim vectors)"]
PGI["Upsert to PostgreSQL<br/>(pgvector)"]
end
subgraph "Retrieval Pipeline"
QR["User Query<br/>(via search_knowledge_base tool)"]
VEC["Vector Cosine<br/>Similarity"]
FTS["PostgreSQL<br/>Full-Text Search"]
RRF["Reciprocal Rank Fusion<br/>(k=60)"]
TOP["Top 5 Results"]
end
UP --> S3I --> EXT --> CHK --> EMB --> PGI
QR --> VEC
QR --> FTS
VEC --> RRF
FTS --> RRF
RRF --> TOP
-
Embedding model —
nvidia/llama-nemotron-embed-vl-1b-v2:freevia OpenRouter, producing 2048-dimensional vectors. - Chunking — ~1,600 characters (~400 tokens) per chunk with 200-character overlap.
- Hybrid search — vector cosine similarity + PostgreSQL FTS, fused with Reciprocal Rank Fusion (k=60), returning the top 5 results.
- Knowledge base resolution cascade — per-message selection → chat-level default → project-level default.
-
Security — the
kbIdfilter is enforced on every retrieval query to prevent cross-user data leakage.
📖 Deep dive: RAG Knowledge Base
AI Client uses S3-compatible object storage (MinIO in development, AWS S3 in production).
- Single bucket with key prefix conventions to separate subsystems (chat attachments, KB documents, transform I/O).
- Presigned URLs for downloads — files are never streamed through the application server.
- AWS SDK v3 for all S3 operations.
-
Bucket auto-creation on startup via
ensureBucket().
| Subsystem | Operation | Key Pattern |
|---|---|---|
| Chat attachments | Upload + download | User-scoped prefix |
| KB documents | Upload + ingestion source | KB-scoped prefix |
| Transform I/O | Input + output files | Run-scoped prefix |
| FileBridge | Temporary local staging for spreadsheets | Temporary local staging |
📖 Deep dive: Object Storage
The artefact panel is a side panel for rendering AI-generated structured content.
| Type | Renderer | Interaction |
|---|---|---|
| Markdown | BlockNote editor | Editable rich text |
| Spreadsheet | TanStack Table | Tabular display |
| HTML | Sandboxed <iframe>
|
Isolated rendering |
| Mermaid | Mermaid.js | Diagram visualisation |
- Artefacts are created via the
manage_artifactbuilt-in tool or implicit Mermaid fence detection in assistant responses. - Stored in message metadata — there is no dedicated artefact table.
- The artefact panel opens alongside the chat when an artefact is detected.
- Markdown artefacts use BlockNote for WYSIWYG editing; HTML artefacts render in a sandboxed iframe for security isolation.
📖 Deep dive: Artefact Panel
Transform Agents are multi-step AI pipelines for document automation.
stateDiagram-v2
[*] --> Pending
Pending --> Running : Start execution
Running --> AwaitingReview : Review gate reached
Running --> Completed : All steps done
Running --> Failed : Error
AwaitingReview --> Running : User approves
AwaitingReview --> Failed : User rejects
Completed --> [*]
Failed --> [*]
- Sequential step execution — each step runs in order, with the output of one step feeding the next.
- Human review gates — steps can pause execution and wait for user approval before continuing.
- Shared MCP connection pool — all steps within a run share the same MCP connections.
-
Steps defined as JSON — the step sequence is stored as a JSON string in the
transform_agenttable. - Input/output via S3 — input and output attachment IDs reference files in object storage.
📖 Deep dive: Step-by-Step Automations
| Route | Method | Auth | Purpose | Max Duration |
|---|---|---|---|---|
/api/auth/[...all] |
GET, POST | Public | Better Auth catch-all handler | Default |
/api/chat |
POST | Session | AI chat streaming (SSE) | 60s |
/api/rag/ingest/[documentId] |
POST | Session | RAG document ingestion | 120s |
/api/transform/run |
POST | Session | Transform agent execution | 300s |
/api/logs |
POST | Optional | Client-side log relay | Default |
All authenticated routes validate the session via auth.api.getSession() and return 401 Unauthorized on failure. Request bodies are validated with Zod; malformed payloads return 400 Bad Request.
The chat and transform streaming endpoints emit the following Server-Sent Event types:
| Event Type | Source | Payload | Description |
|---|---|---|---|
text |
Chat, Transform | Delta string | Incremental text content |
reasoning |
Chat | Delta string | Model reasoning/thinking tokens |
tool-call |
Chat, Transform | Tool name, args | Tool invocation notification |
tool-result |
Chat, Transform | Tool name, result | Tool execution result |
transform-start |
Transform | Run metadata | Transform run initialisation |
transform-step-start |
Transform | Step index, name | Individual step begins |
transform-step-complete |
Transform | Step result | Individual step finishes |
transform-complete |
Transform | Final result | Transform run completion |
error |
Chat, Transform | Error message | Stream error |
done |
Chat, Transform | Final metadata | Stream completion signal |
- Events are parsed by an async generator (
parseSseStream) that yields typedSseEventobjects. -
textandreasoningdeltas are accumulated in React local state during streaming. -
tool-callandtool-resultevents are displayed inline within the message bubble. - The
doneevent triggers final state commit to Zustand and metadata persistence. -
errorevents halt streaming and display an error state in the UI.
Validation is enforced at every system boundary using Zod.
| Layer | Mechanism | Failure Mode |
|---|---|---|
| Environment variables | Zod schema at startup | Process exits with descriptive error |
| API route bodies |
.safeParse() in handler |
400 Bad Request with structured errors |
| Server Action inputs |
.safeParse() in action |
Thrown error or structured return |
| Client-side forms | React Hook Form + Zod resolver | Inline field-level validation errors |
| Message metadata | messageMetadataSchema |
Malformed metadata rejected |
Common validators are defined once and reused across schemas:
-
emailField— valid email format -
passwordField— minimum length and complexity -
nameField— non-empty, length-bounded -
descriptionField— optional, length-bounded -
contentField— non-empty text content -
sixDigitCodeField()— TOTP and backup code format
- API routes return structured JSON errors with appropriate HTTP status codes (401, 400, 404, 500).
-
SSE streaming uses
errorevents to signal mid-stream failures without breaking the connection protocol. -
Next.js
error.tsxboundaries catch rendering errors at the(main)route group level. - Zustand rollback — failed persistence after optimistic updates triggers a snapshot restore.
-
MCP failures are isolated —
Promise.allSettledensures one failing server does not affect others.
- Postmark handles all transactional emails: welcome, verification, password reset, and account deletion.
- Email templates are server-side only; no email credentials are exposed to the client.
- Built on the unified ecosystem:
react-markdownwithremark-gfm,remark-math,rehype-katex, andrehype-highlight. - KaTeX for mathematical notation; Mermaid for diagrams; GFM for tables and task lists.
-
Dual rendering paths:
-
react-markdownfor chat message bubbles (read-only). - BlockNote for artefact panel editing (WYSIWYG).
-
- Syntax highlighting for code blocks with language detection.
📖 Deep dive: Markdown Rendering
| Category | Technology | Version |
|---|---|---|
| Framework | Next.js (App Router, Turbopack) | ^16.2.4 |
| Language | TypeScript (strict mode) | ^5 |
| UI Library | React | ^19.2.5 |
| Database | PostgreSQL + pgvector | 17 |
| ORM | Drizzle ORM + drizzle-kit | ^0.45.2 / ^0.31.10 |
| AI | Vercel AI SDK | ^6.0.168 |
| AI Provider | OpenRouter (via @ai-sdk/openai) | ^3.0.53 |
| MCP | @ai-sdk/mcp + @modelcontextprotocol/sdk | ^1.0.36 / ^1.29.0 |
| Auth | Better Auth | ^1.6.8 |
| UI Components | Base UI, Radix UI, shadcn/ui | ^1.4.1 |
| Styling | Tailwind CSS v4, CVA, tailwind-merge | v4 |
| Rich Text | BlockNote | ^0.49.0 |
| State | Zustand | ^5.0.12 |
| Forms | React Hook Form + Zod | ^7.73.1 / ^4.3.6 |
| Storage | AWS SDK v3 (S3-compatible) | v3 |
| Postmark | ^4.0.7 | |
| Testing | Vitest + Testing Library + jsdom | ^4.1.5 |
| Dev Infra | Docker (pgvector, MinIO) | — |
- Next.js App Router Documentation
- Vercel AI SDK Documentation
- Better Auth Documentation
- Drizzle ORM Documentation
- Model Context Protocol Specification
- OpenRouter API Documentation
- Zustand Documentation
- PostgreSQL pgvector Extension
- Base UI Documentation
- Zod Documentation
- MinIO Documentation
- React Hook Form Documentation
- Tailwind CSS v4 Documentation
- BlockNote Documentation
- Postmark Documentation