Faster Chat — Architecture Guide
Last Updated: 2026-04-23 Stack: Preact + Hono + SQLite + Bun + Vercel AI SDK
1. System Overview
Section titled “1. System Overview”Faster Chat is a self-hosted, privacy-first AI chat interface. It is a monorepo with three workspaces: a Preact SPA frontend, a Hono API backend, and shared internal packages.
┌──────────────────────────────────────────────────────────────┐│ Frontend (Vite) ││ Preact SPA — http://localhost:3000 ││ ├─ TanStack Router — File-based routing ││ ├─ TanStack Query — Server state sync ││ ├─ Zustand — UI state (theme, sidebar, prefs) ││ ├─ Vercel AI SDK — Streaming chat transport ││ └─ Tailwind CSS 4 — Utility-first styling │└───────────────────────┬──────────────────────────────────────┘ │ /api/*┌───────────────────────▼──────────────────────────────────────┐│ Hono API Server ││ Bun runtime — http://localhost:3001 ││ ├─ /api/chats — Streaming chat + CRUD ││ ├─ /api/auth — Session-based auth ││ ├─ /api/admin — User/provider/model management ││ ├─ /api/files — File uploads/attachments ││ ├─ /api/images — Image generation ││ ├─ /api/memory — Cross-conversation memory ││ └─ /api/settings — App-wide configuration │└───────────────────────┬──────────────────────────────────────┘ │ bun:sqlite┌───────────────────────▼──────────────────────────────────────┐│ SQLite Database ││ Single-file database (server/data/chat.db) ││ ├─ users, sessions — Auth & RBAC ││ ├─ chats, messages — Conversation persistence ││ ├─ providers, models — AI provider registry ││ ├─ files — Attachment metadata ││ ├─ folders — Chat organization ││ ├─ user_memories — Cross-conversation facts ││ └─ settings — App configuration │└──────────────────────────────────────────────────────────────┘2. Directory Structure
Section titled “2. Directory Structure”faster-chat/├── frontend/ # Preact SPA│ ├── src/│ │ ├── main.jsx # Entry point — renders App│ │ ├── App.jsx # Root component, QueryClientProvider│ │ ├── router.jsx # TanStack route tree definition│ │ ├── pages/│ │ │ ├── public/│ │ │ │ └── Login.jsx # Public login page│ │ │ └── authenticated/│ │ │ ├── Chat.jsx # Main chat page│ │ │ ├── Admin.jsx # Admin dashboard│ │ │ ├── Settings.jsx # User settings│ │ │ ├── Import.jsx # Conversation import│ │ │ └── Folder.jsx # Folder view│ │ ├── components/│ │ │ ├── chat/ # Chat-specific components│ │ │ │ ├── ChatInterface.jsx # Main chat layout│ │ │ │ ├── MessageList.jsx # Render message stream│ │ │ │ ├── MessageItem.jsx # Individual message│ │ │ │ ├── InputArea.jsx # Text input + submit│ │ │ │ ├── ModelSelector.jsx # Model dropdown│ │ │ │ └── ...│ │ │ ├── admin/ # Admin panel components│ │ │ ├── layout/ # Shell components│ │ │ │ ├── MainLayout.jsx # App shell (sidebar + content)│ │ │ │ ├── Sidebar.jsx # Chat list sidebar│ │ │ │ └── ...│ │ │ ├── settings/ # Settings sections│ │ │ ├── markdown/ # Markdown rendering│ │ │ └── ui/ # Reusable UI primitives│ │ ├── hooks/ # Custom React hooks│ │ │ ├── useChat.js # Core chat orchestration hook│ │ │ ├── useChatStream.js # AI SDK streaming hook│ │ │ ├── useChatPersistence.js # Save/load messages via API│ │ │ ├── useChatsQuery.js # TanStack Query hooks for chats│ │ │ └── ...│ │ ├── state/ # Zustand stores│ │ │ ├── useAuthState.js # Auth state (user, login, logout)│ │ │ ├── useUiState.js # UI preferences│ │ │ ├── useThemeStore.js # Theme settings│ │ │ └── useAppSettings.js # App-wide settings│ │ ├── lib/ # Utilities & API clients│ │ │ ├── api.js # Base API client│ │ │ ├── authClient.js # Auth API wrappers│ │ │ ├── chatsClient.js # Chat API wrappers│ │ │ ├── messageUtils.js # Message content helpers│ │ │ └── shiki.js # Syntax highlighting setup│ │ └── constants/ # Frontend-only constants│ ├── index.html│ ├── vite.config.js # Vite + Preact + Tailwind│ └── package.json│├── server/ # Hono API│ ├── src/│ │ ├── index.js # Server entry — routes + middleware│ │ ├── init.js # First-run setup (encryption keys, dirs)│ │ ├── routes/│ │ │ ├── auth.js # Login/register/logout│ │ │ ├── chats.js # Chat CRUD + streaming endpoint│ │ │ ├── admin.js # User management, audit logs│ │ │ ├── providers.js # Provider CRUD│ │ │ ├── models.js # Model listing & defaults│ │ │ ├── files.js # File upload/download│ │ │ ├── images.js # Image generation│ │ │ ├── memory.js # Memory extraction & storage│ │ │ ├── folders.js # Chat folders│ │ │ ├── settings.js # App settings│ │ │ ├── import.js # ChatGPT import│ │ │ └── version.js # Version info│ │ ├── middleware/│ │ │ ├── auth.js # Session validation + RBAC│ │ │ ├── securityHeaders.js # HSTS, CSP, etc.│ │ │ └── rateLimiter.js # Per-endpoint rate limiting│ │ ├── lib/│ │ │ ├── db.js # SQLite database + dbUtils│ │ │ ├── providerFactory.js # AI SDK provider instantiation│ │ │ ├── imageProviderFactory.js# Image generation providers│ │ │ ├── imageGeneration.js # Image generation orchestration│ │ │ ├── encryption.js # AES-256-GCM for API keys│ │ │ ├── memory.js # Memory extraction logic│ │ │ ├── modelsdev.js # models.dev integration│ │ │ ├── fileUtils.js # File storage helpers│ │ │ ├── security.js # SSRF protection, validation│ │ │ ├── chatgptImporter.js # ChatGPT JSON import│ │ │ ├── tools/ # AI tool implementations│ │ │ │ ├── webSearch.js # Brave Search tool│ │ │ │ └── fetchUrl.js # URL fetching for search│ │ │ └── search/ # Search subsystem│ │ │ ├── index.js # Search dispatcher│ │ │ ├── fetchUrl.js # URL content extraction│ │ │ └── providers/│ │ │ └── brave.js # Brave Search API│ │ └── test/ # Bun test suite│ ├── data/│ │ ├── uploads/ # Uploaded files & generated images│ │ └── chat.db # SQLite database│ └── package.json│├── packages/shared/ # Shared internal package│ └── src/│ ├── constants/ # Shared constants│ │ ├── providers.js # Provider defaults & registry│ │ ├── prompts.js # System prompts│ │ ├── ui.js # UI limits & defaults│ │ ├── database.js # DB constants│ │ ├── voice.js # Voice config│ │ ├── search.js # Search constants│ │ ├── imageGeneration.js # Image gen defaults│ │ └── ...│ ├── types/ # JSDoc type definitions│ ├── utils/ # Shared utilities│ └── index.js # Package exports│├── docs/ # Documentation├── scripts/ # Build & deploy helpers├── docker-compose.yml # Docker orchestration├── Dockerfile # Production image└── package.json # Workspace root3. Data Flow
Section titled “3. Data Flow”3.1 Chat Message Flow
Section titled “3.1 Chat Message Flow”User types message ↓[frontend] InputArea.jsx ↓ handleSubmit[frontend] useChat.submitMessage() ↓[frontend] useChatPersistence.saveUserMessage() → POST /api/chats/:id/messages ↓ (TanStack Query invalidates chat cache)[frontend] useChatStream.send() → POST /api/chats/:id/stream ↓[server] chats.js — streamText() with Vercel AI SDK ↓[server] providerFactory.js — creates provider client ↓[external] OpenAI/Anthropic/Ollama/etc. ↓ (SSE streaming response)[frontend] useChatStream — receives chunks, updates messages ↓[frontend] onMessageComplete callback ↓[frontend] useChatPersistence.saveAssistantMessage() → POST /api/chats/:id/messages ↓[server] Memory extraction (async, post-response) ↓[server] SQLite — messages table updated3.2 Authentication Flow
Section titled “3.2 Authentication Flow”User visits /login ↓[frontend] Login.jsx → POST /api/auth/login ↓[server] auth.js — bcrypt compare, createSession() ↓[server] Set HTTP-only cookie: session=<id> ↓[frontend] useAuthState.login() — updates user state ↓[frontend] router navigates to /chat/:id ↓Subsequent requests include session cookie automatically ↓[server] ensureSession middleware validates cookie against SQLite ↓[server] Attaches user object to Hono context: c.get('user')3.3 Admin Configuration Flow
Section titled “3.3 Admin Configuration Flow”Admin opens /admin ↓[frontend] Admin.jsx — tabbed interface (Connections, Models, Users, Customize) ↓[frontend] ConnectionsTab — lists providers via GET /api/admin/providers ↓Admin adds provider + API key ↓[frontend] POST /api/admin/providers ↓[server] Encrypt API key with AES-256-GCM → store in providers table ↓Admin clicks "Refresh Models" ↓[server] Fetch models from provider API (or models.dev cache) ↓[server] Populate models table ↓[frontend] ModelsTab — enable/disable models, set default4. Frontend Architecture
Section titled “4. Frontend Architecture”4.1 Routing
Section titled “4.1 Routing”TanStack Router provides code-based routing with lazy-loaded page components:
| Route | Auth | Component | Purpose |
|---|---|---|---|
/login | Public | Login.jsx | Authentication |
/ | Protected | IndexRouteGuard | Redirects to active chat |
/chat/$chatId | Protected | Chat.jsx | Main chat interface |
/admin | Protected (admin) | Admin.jsx | Admin dashboard |
/settings | Protected | Settings.jsx | User preferences |
/import | Protected | Import.jsx | Import conversations |
/folder/$folderId | Protected | Folder.jsx | Folder chat list |
Routes are defined in frontend/src/router.jsx. The ProtectedLayout component wraps all authenticated routes, checking the session and rendering MainLayout with Sidebar.
4.2 State Management
Section titled “4.2 State Management”Three-layer state architecture:
| Layer | Tool | Purpose | Example |
|---|---|---|---|
| Server State | TanStack Query | API data that persists on server | Chat list, messages, models |
| Client UI State | Zustand | Ephemeral UI preferences | Theme, sidebar open, selected model |
| Local Component State | useState | Component-scoped values | Input text, modal open |
Key rule: Never duplicate server data in Zustand or useState. If data comes from an API, it lives in TanStack Query.
4.3 Custom Hooks
Section titled “4.3 Custom Hooks”The frontend uses a layered hook architecture for chat:
useChat— Orchestrates the entire chat experience. Coordinates persistence, streaming, and user input.useChatStream— Wraps the Vercel AI SDKuseChathook. Handles SSE streaming, message assembly, and tool execution (web search).useChatPersistence— TanStack Query mutations for saving/loading messages and chat metadata.useChatsQuery— Query hooks for chat CRUD operations (list, create, delete, pin, archive).
4.4 Component Philosophy
Section titled “4.4 Component Philosophy”Components follow an 8-section structure:
- Server state queries
- Client state (Zustand)
- Derived values (computed in render)
- Event handlers
- Effects (rare — DOM sync only)
- Early returns
- Render helpers
- JSX
Constraints:
- No TypeScript —
.jsxcomponents,.jsutilities - No
useCallback— event handlers don’t need memoization - Derive, don’t duplicate — computable values are expressions, not state
- Composition over configuration — pass
children, not boolean flags
5. Backend Architecture
Section titled “5. Backend Architecture”5.1 Server Entry (server/src/index.js)
Section titled “5.1 Server Entry (server/src/index.js)”The Hono app is assembled with middleware and routes:
- Security headers — Applied to all responses (HSTS, CSP, X-Frame-Options)
- Logger — Request logging
- Body size limit — 50MB cap on
/api/* - CORS — Origin-restricted (production:
APP_URLonly; dev: localhost only) - Route mounting — Each domain has its own router
- Static file serving — In production, serves
frontend/distand SPA fallback - models.dev cache initialization — Fetches provider metadata on startup
5.2 Middleware
Section titled “5.2 Middleware”| Middleware | Applied To | Purpose |
|---|---|---|
securityHeaders | * | Security headers on all responses |
logger | * | Request logging |
ensureSession | /api/* (except auth/version) | Validates session cookie, attaches user |
requireRole('admin') | /api/admin/* | RBAC enforcement |
createRateLimiter | Specific routes | Per-endpoint rate limiting |
5.3 Route Handlers
Section titled “5.3 Route Handlers”Each route file exports a Hono router instance mounted in index.js:
| Router | Base Path | Auth | Key Operations |
|---|---|---|---|
authRouter | /api/auth | Public | Login, register, logout, session check |
chatsRouter | /api/chats | Session | CRUD chats, stream messages, delete |
adminRouter | /api/admin | Admin | User CRUD, audit logs, stats |
providersRouter | /api/admin/providers | Admin | Provider CRUD, key encryption |
modelsRouter | /api | Mixed | List models, set defaults |
filesRouter | /api/files | Session | Upload, download, delete attachments |
imagesRouter | /api/images | Session | Generate images (DALL-E, FLUX) |
memoryRouter | /api/memory | Session | Get/clear memories, toggle settings |
foldersRouter | /api/folders | Session | Folder CRUD, chat organization |
settingsRouter | /api/settings | Mixed | Public GET, admin PUT |
importRouter | /api/import | Session | ChatGPT JSON import |
versionRouter | /api/version | Public | App version info |
5.4 Database Layer (server/src/lib/db.js)
Section titled “5.4 Database Layer (server/src/lib/db.js)”The entire database layer is a single file exporting dbUtils — a plain object of functions. No ORM. SQLite is accessed directly via bun:sqlite prepared statements.
Schema:
| Table | Purpose |
|---|---|
users | Accounts with role (admin/member/readonly) |
sessions | HTTP-only cookie sessions with expiry |
providers | AI provider configs with encrypted API keys |
models | Available models per provider, enabled flag |
model_metadata | Capabilities (vision, tools, pricing) |
chats | Conversations with soft delete, pin, archive |
messages | Chat messages with role, content, model, metadata |
files | Uploaded file metadata (stored on disk) |
folders | User-defined chat folders |
settings | Key-value app configuration |
user_memories | Extracted facts per user |
audit_log | Security event log |
Migrations: The schema is created on startup with CREATE TABLE IF NOT EXISTS. Schema changes (new columns) are handled by checking PRAGMA table_info and running ALTER TABLE if needed.
Production optimizations:
PRAGMA journal_mode = WAL— Write-ahead logging for concurrencyPRAGMA synchronous = NORMAL— Balanced durability/performancePRAGMA cache_size— Increased page cachePRAGMA temp_store = MEMORY— Temp tables in RAMPRAGMA mmap_size— Memory-mapped I/O
5.5 Provider System (server/src/lib/providerFactory.js)
Section titled “5.5 Provider System (server/src/lib/providerFactory.js)”The provider factory maps provider names to Vercel AI SDK client constructors:
Provider Name → AI SDK Factory─────────────────────────────────────────anthropic → @ai-sdk/anthropicopenai → @ai-sdk/openaiazure → @ai-sdk/azuregoogle → @ai-sdk/googlegoogle-vertex → @ai-sdk/google-vertexmistral → @ai-sdk/mistralgroq → @ai-sdk/groqcohere → @ai-sdk/cohereamazon-bedrock → @ai-sdk/amazon-bedrockxai → @ai-sdk/xaideepseek → @ai-sdk/deepseekcerebras → @ai-sdk/cerebrasfireworks → @ai-sdk/fireworksollama → @ai-sdk/openAI (with custom baseURL)openrouter → @ai-sdk/openAI (with custom baseURL)Unknown providers fall back to OpenAI-compatible mode (custom baseURL).
The getModelInstance() function:
- Looks up the provider in
PROVIDER_FACTORIES - Decrypts the stored API key with
decryptApiKey() - Instantiates the provider client
- Returns the model instance (
provider.chat(modelId)orprovider(modelId))
6. Shared Packages (packages/shared)
Section titled “6. Shared Packages (packages/shared)”The @faster-chat/shared package contains code used by both frontend and backend:
- Constants — Provider defaults, UI limits, file config, prompts, shortcuts
- Types — JSDoc type definitions for IDE support (no TS compiler)
- Utilities — Formatters, provider validation
This ensures both sides agree on contract values without manual sync.
7. Key Design Decisions
Section titled “7. Key Design Decisions”7.1 Why No TypeScript?
Section titled “7.1 Why No TypeScript?”The project uses plain JavaScript (.js/.jsx) with JSDoc types. The rationale:
- Faster iteration — no compile step, no type churn across fast-moving AI SDKs
- Runtime validation at API boundaries (Zod on the server)
- Shared constants and clear contracts reduce type errors
- Tests cover critical paths
Trade-off: Less compile-time safety, but faster development and lower contribution barrier.
7.2 Why SQLite?
Section titled “7.2 Why SQLite?”SQLite is used as the sole database (no PostgreSQL in production yet):
- Single file — trivial backups and migrations
- bun:sqlite is extremely fast (native bindings)
- WAL mode handles concurrent reads during writes
- Fits the self-hosted, single-server deployment model
Future: PostgreSQL is on the roadmap for larger multi-user deployments.
7.3 Why Preact over React?
Section titled “7.3 Why Preact over React?”Preact provides a 3KB runtime vs React’s ~40KB:
- Faster load times, especially on slower connections
- Full React API compatibility via
@preact/compat - Works with the entire React ecosystem (TanStack, AI SDK, etc.)
Vite aliases react → @preact/compat so dependencies work transparently.
7.4 Session-Based Auth
Section titled “7.4 Session-Based Auth”Cookie-based sessions are used instead of JWT:
- Server-side revocation — logout invalidates the session immediately
- No token storage complexity on the client
- Works seamlessly with HTTP-only cookies (XSS resistant)
Sessions are stored in SQLite with automatic expiry cleanup.
7.5 API Key Encryption
Section titled “7.5 API Key Encryption”Provider API keys are encrypted at rest with AES-256-GCM:
- Each key gets a unique IV and auth tag
- The encryption key is in
server/.env(auto-generated on first run) - Keys are never sent to the client
- Brave Search API key uses the same encryption mechanism
Important: server/.env must be backed up — losing the encryption key means all stored API keys are unrecoverable.
8. Feature Architecture
Section titled “8. Feature Architecture”8.1 Streaming Chat
Section titled “8.1 Streaming Chat”The chat uses Vercel AI SDK’s streamText on the backend and useChat (via @ai-sdk/react) on the frontend:
- Frontend sends message history to
/api/chats/:id/stream - Backend builds message array, injects system prompt + memories
- Backend calls
streamText()with the model instance - AI SDK handles provider-specific streaming protocols
- Response streamed as Server-Sent Events (SSE)
- Frontend receives chunks and assembles the message in real-time
- On completion, assistant message is saved to SQLite
8.2 Web Search
Section titled “8.2 Web Search”When enabled and the model supports tools:
- Backend creates
webSearchtool using Brave Search API - AI SDK automatically invokes the tool when the model decides to search
- Tool fetches search results (cached 5 minutes)
- Model receives results and cites sources inline
- Frontend displays source citations as clickable pills
SSRF protection: URL fetching validates DNS and blocks private IP ranges.
8.3 Memory System
Section titled “8.3 Memory System”Cross-conversation memory extracts facts about the user:
- After each assistant response, an async extraction runs
- A cheap/fast model (configurable, e.g. Haiku/GPT-4o-mini) extracts facts
- Facts are deduplicated and stored in
user_memories - On subsequent chats, memories are injected into the system prompt
Gating (three levels):
- Global toggle (admin setting)
- Per-user toggle (user can disable entirely)
- Per-chat opt-out (individual chat can skip memory)
8.4 Image Generation
Section titled “8.4 Image Generation”Separate from chat streaming:
- User selects image generation model in
InputArea - Frontend sends prompt to
POST /api/images/generate - Backend routes to appropriate provider (OpenAI DALL-E, Replicate FLUX, OpenRouter)
- Image generated and saved to
server/data/uploads/generated/ - URL returned to frontend for inline display
8.5 File Attachments
Section titled “8.5 File Attachments”- User uploads file via
POST /api/files - File stored on disk with UUID prefix:
{uuid}_{filename} - Metadata saved to
filestable (size, mime type, hash) - File IDs attached to messages via
file_idsJSON array - Messages with file IDs render attachment previews
9. Security
Section titled “9. Security”| Concern | Mitigation |
|---|---|
| XSS | CSP headers, no inline scripts, React/Preact escaping |
| CSRF | SameSite cookies, origin-restricted CORS |
| Session hijacking | HTTP-only cookies, automatic expiry |
| API key exposure | AES-256-GCM encryption at rest |
| SSRF | DNS validation, private IP blocking |
| Rate limiting | Per-endpoint configurable limits |
| File uploads | Size limits, mime type validation, hash deduplication |
| SQL injection | Parameterized queries (prepared statements only) |
10. Extension Points
Section titled “10. Extension Points”Adding a New AI Provider
Section titled “Adding a New AI Provider”- Add factory to
server/src/lib/providerFactory.js(or use OpenAI-compatible fallback) - Add defaults to
packages/shared/src/constants/providers.js - Add provider metadata to
packages/shared/src/constants/config.js - Register in admin UI if special handling needed
Adding a New Route
Section titled “Adding a New Route”- Create router file in
server/src/routes/ - Export
Honorouter instance - Mount in
server/src/index.jswithapp.route("/api/...", router) - Add corresponding API client in
frontend/src/lib/ - Add TanStack Query hooks in
frontend/src/hooks/
Adding a New Database Table
Section titled “Adding a New Database Table”- Add
CREATE TABLE IF NOT EXISTStoserver/src/lib/db.jsschema section - Add CRUD functions to
dbUtilsobject - Export from
db.js - Use in route handlers
Adding a New Frontend Page
Section titled “Adding a New Frontend Page”- Create component in
frontend/src/pages/authenticated/(orpublic/) - Add route in
frontend/src/router.jsx - Add sidebar link if needed in
frontend/src/components/layout/Sidebar.jsx
11. Development Workflow
Section titled “11. Development Workflow”# Install dependenciesbun install
# Start dev servers (frontend + backend concurrently)bun run dev
# Run server testsbun run test
# Format codebun run format
# Build for productionbun run build
# Start production serverbun run startDev server ports:
- Frontend: http://localhost:3000 (Vite with HMR)
- Backend: http://localhost:3001 (Hono API)
- API proxy:
/api/*on :3000 proxies to :3001
First run:
bun run dev- Visit http://localhost:3000/login
- Register first account (auto-promoted to admin)
- Configure providers in
/admin
12. Testing
Section titled “12. Testing”The server has a comprehensive test suite using Bun’s built-in test runner:
| Test File | Coverage |
|---|---|
auth.test.js | Login, register, session validation |
chats.test.js | Chat CRUD, message streaming |
admin.test.js | User management, role changes |
providers.test.js | Provider CRUD, encryption |
models.test.js | Model listing, defaults |
memory.test.js | Memory extraction, gating |
folders.test.js | Folder CRUD, chat organization |
settings.test.js | App settings, white label |
encryption.test.js | AES-256-GCM encrypt/decrypt |
middleware.test.js | Auth middleware, rate limiting |
smoke.test.js | Server startup health check |
Run with: bun run test
For deployment details, see docker-setup.md and caddy-https-setup.md. For feature specs, see the features/ directory.