Architecture
Request Routing
Norman Chat acts as a smart proxy, routing chat requests to the appropriate backend service based on the selected agent mode:
User → Norman Chat /api/chat
│
├─ agentMode: "v0.1" → Norman Engine /api/complete (stream)
│
└─ agentMode: "v1" → Norman Agent /api/chat (stream)Streaming Flow
- Client opens SSE connection to
/api/chat - Chat route forwards request to upstream service with
stream: true - Upstream responds with SSE events
- Chat route re-emits events to the client
- On completion, chat is persisted to MongoDB asynchronously
Non-Streaming Fallback
If the upstream returns JSON instead of SSE:
- Full response extracted from JSON body
- Emitted as a single
tokenevent for UI consistency doneevent sent with metadata
Data Model
Chat Document
interface ChatDocument {
chatId: string;
userId: string;
title: string; // Derived from first user message
agentMode: 'v0.1' | 'v1';
messages: {
role: 'user' | 'assistant' | 'system';
content: string;
timestamp: string;
toolsUsed?: string[];
}[];
createdAt: string;
updatedAt: string;
}Persistence Strategy
- Upsert pattern: First message creates the chat document; subsequent messages append
- Title derivation: First user message truncated to 60 chars becomes the chat title
- Background persistence: MongoDB writes happen after the response stream completes (non-blocking)
Authentication
NextAuth.js configured with ShellApps as a custom OAuth provider:
Browser → NextAuth → ShellApps OAuth → Authorization Code → Token Exchange → JWT SessionAll API routes verify the session before processing.