11 KiB
Chat Switchboard
A self-hosted AI chat application for enterprise and government environments. Unified interface for multiple AI providers with admin controls, team management, and security features.
Quick Start
# Clone and start
git clone <repo-url> && cd chat-switchboard
docker compose up -d
# Access at http://localhost:3000
# Default admin: admin / admin
Features
- Multi-Provider: Anthropic, OpenAI, OpenRouter, Venice AI — with BYOK support
- Team Management: Roles (admin/user) for vertical permissions, Teams for horizontal visibility
- Personas: Preset configurations (model + system prompt + parameters) at global, team, or personal scope
- Model Catalog: Three-state visibility (enabled/disabled/team-only) with admin controls
- Message Trees: Edit and regenerate with full conversation forking — navigate sibling branches
- Notes: Markdown notes with full-text search, folders, and tags
- Extensions: Browser extension system with custom renderers, tool bridge, and self-contained styling
- Built-in: Mermaid diagrams, KaTeX math, CSV tables, diff viewer, JS sandbox, regex tester
- Server Tools: Calculator and datetime tools (auto-registered, zero config)
- Audit Log: All admin operations logged with actor, action, and resource details
- Security: JWT + refresh tokens, AES-256-GCM API key encryption, optional mTLS, optional OIDC/Keycloak, environment classification banners
- Airgapped: Local vendor files (marked.js, DOMPurify, KaTeX, mermaid.js), no CDN required
- Mobile: Responsive design, PWA manifest
- Real-time: WebSocket event bus for tool bridge, live updates
Architecture
┌─────────────┐ ┌──────────────────────────────┐
│ Browser │────▶│ nginx (port 80) │
│ (vanilla JS)│◀────│ ├─ /api/* → Go backend:8080 │
└─────────────┘ │ ├─ /ws → WebSocket proxy │
│ └─ /* → static files │
└──────────────────────────────┘
│
┌─────────▼─────────┐
│ Go Backend │
│ ├─ handlers/ │
│ ├─ store/postgres/ │
│ ├─ providers/ │
│ └─ capabilities/ │
└─────────┬─────────┘
│
┌─────────▼─────────┐
│ PostgreSQL 16 │
└───────────────────┘
Go backend (vanilla, no framework beyond Gin) with a store layer abstracting all database access. Providers package handles LLM API calls. Capabilities package resolves model features from catalog data, known model tables, and heuristic inference. Server tools (calculator, datetime) auto-register via init(). EventBus + WebSocket hub routes tool calls between backend and browser extensions.
Frontend is vanilla JavaScript — no build step, no bundler. 15 files organized by domain: api.js (HTTP client), app.js (state + init), chat.js (send, regen, edit, branch), events.js (event bus + WebSocket), extensions.js (loader, registry, renderer pipeline, tool bridge), ui-core.js (DOM rendering + streaming), ui-format.js (markdown, code blocks), ui-primitives.js (shared components), ui-settings.js / ui-admin.js (settings and admin panels), notes.js, tokens.js, debug.js, settings-handlers.js, admin-handlers.js.
Configuration
All configuration via environment variables. See server/.env.example for the full list.
| Variable | Default | Description |
|---|---|---|
PORT |
8080 |
Backend listen port |
BASE_PATH |
|
URL prefix (e.g. /chat) |
DB_HOST |
localhost |
PostgreSQL host |
DB_NAME |
chat_switchboard |
Database name |
JWT_SECRET |
(required) | Token signing key |
SWITCHBOARD_ADMIN_USERNAME |
|
Bootstrap admin username |
SWITCHBOARD_ADMIN_PASSWORD |
|
Bootstrap admin password |
ENCRYPTION_KEY |
|
AES key for API key encryption (required if encrypted keys exist) |
SEED_USERS |
|
Dev/test only: user:pass:role,user2:pass2:role2 |
STORAGE_BACKEND |
(auto) | pvc or s3. Auto-detects PVC if not set. |
STORAGE_PATH |
/data/storage |
PVC mount point (also scratch dir for S3 extraction) |
S3_ENDPOINT |
|
S3 endpoint (e.g. http://minio:9000, required for S3) |
S3_BUCKET |
|
S3 bucket name (must exist, required for S3) |
S3_ACCESS_KEY |
|
S3 access key ID |
S3_SECRET_KEY |
|
S3 secret access key |
S3_REGION |
us-east-1 |
S3 region |
S3_PREFIX |
|
Optional key prefix within bucket |
S3_FORCE_PATH_STYLE |
true |
Path-style URLs (required for MinIO, Ceph) |
Deployment
Three Docker images support different scenarios:
| Image | Dockerfile | Use Case |
|---|---|---|
| Unified | Dockerfile |
Dev, docker-compose, single-node |
| Backend | server/Dockerfile |
K8s — scale API pods independently |
| Frontend | Dockerfile.frontend |
K8s — scale FE pods independently |
Docker Compose (development — unified image)
docker compose up -d # start all services
docker compose up -d --build # rebuild after code changes
docker compose --profile dev up # include Adminer DB UI
Kubernetes (split images)
Build and push both images:
docker build -f server/Dockerfile -t your-registry/switchboard-api:0.11.0 server/
docker build -f Dockerfile.frontend -t your-registry/switchboard-fe:0.11.0 .
Backend deployment:
containers:
- name: api
image: your-registry/switchboard-api:0.11.0
ports:
- containerPort: 8080
env:
- name: DB_HOST
value: "postgres-service"
- name: BASE_PATH
value: "/chat" # must match ingress path
- name: JWT_SECRET
valueFrom:
secretKeyRef:
name: switchboard-secrets
key: jwt-secret
Frontend deployment:
containers:
- name: frontend
image: your-registry/switchboard-fe:0.11.0
ports:
- containerPort: 80
env:
- name: BASE_PATH
value: "/chat" # injected into index.html at startup
volumeMounts:
- name: branding
mountPath: /branding
readOnly: true # optional: custom logo, colors
Ingress routes /api/* and /ws to the backend Service, everything else to the frontend Service. The frontend entrypoint generates the nginx config dynamically based on BASE_PATH.
Path-Based Routing
Set BASE_PATH=/chat to serve the application under a subpath. The frontend reads window.__BASE__ injected at container startup, and all API calls are prefixed automatically.
Airgapped / Disconnected
The Docker build bakes in marked.js, DOMPurify, and KaTeX (JS + CSS + fonts) from npm during the vendor stage. Mermaid.js loads dynamically from local vendor with CDN fallback. No CDN calls at runtime in airgapped deployments. The src/vendor/ directory contains local copies as fallback for development without Docker.
Database
PostgreSQL 16+ required. The pgcrypto extension is used for gen_random_uuid().
Schema Management
The Go backend auto-migrates on startup using files in server/database/migrations/. For manual operations:
# Bootstrap database (superuser, creates role + DB)
scripts/db-bootstrap.sh
# Manual migration (usually not needed)
scripts/db-migrate.sh
# Validate schema
scripts/db-validate.sh
Key Tables (v0.11)
| Table | Purpose |
|---|---|
users |
Accounts with role, avatar, settings, encrypted vault (UEK) |
provider_configs |
API provider configurations (scope: global/team/personal) |
model_catalog |
Synced model list with capabilities, visibility, and type |
personas |
Model presets (scope: global/team/personal) |
channels |
Conversations with type (direct/group/channel) |
messages |
Message tree with parent_id for forking, tool_calls JSONB |
teams / team_members |
Organizational units |
notes |
Markdown notes with full-text search |
extensions / extension_user_settings |
Browser extension registry and per-user config |
usage_log / model_pricing |
Token usage tracking and cost calculation |
audit_log |
Admin action audit trail |
user_model_settings |
Per-user model visibility and sort preferences |
API
All endpoints under /api/v1/. Authentication via Authorization: Bearer <token> header.
Auth
POST /auth/register— Register (ifallow_registrationpolicy is true)POST /auth/login— Login, returns access + refresh tokensPOST /auth/refresh— Refresh access tokenPOST /auth/logout— Revoke refresh token
Channels & Messages
GET/POST /channels— List/create conversationsGET/PUT/DELETE /channels/:id— Channel CRUDGET/POST /channels/:id/messages— Message list/createPOST /chat/completions— Stream AI completions (SSE)POST /channels/:id/messages/:msgId/edit— Edit and forkPOST /channels/:id/messages/:msgId/regenerate— Regenerate response
Models
GET /models/enabled— Models available to the user (global + team + personal)GET/PUT /models/preferences— User model visibility settings
Personal Providers (BYOK)
GET/POST /api-configs— User's personal provider configsGET/PUT/DELETE /api-configs/:id— Provider CRUDPOST /api-configs/:id/models/fetch— Refresh models from provider APIGET /api-configs/:id/models— List models for a personal provider
Teams
GET /teams— Teams the user belongs toGET/POST /teams/:id/providers— Team provider management (team admins)GET/POST /teams/:id/presets— Team preset management (team admins)
Admin
GET/POST /admin/users— User managementGET/PUT /admin/settings/:key— Global settings and policiesGET/POST /admin/configs— Global provider configsGET/POST /admin/models/fetch— Model catalog syncGET/PUT /admin/roles/:role— Model role configurationGET /admin/usage— Usage dashboardGET /admin/audit— Audit logGET/POST/PUT/DELETE /admin/extensions— Extension management
Extensions
GET /extensions?tier=browser— List enabled browser extensionsGET /extensions/:id/assets/*path— Serve extension assets (public, no auth)
WebSocket
GET /ws?token=<jwt>— EventBus WebSocket for real-time events and tool bridge
Development
# Backend (requires Go 1.22+)
cd server
cp .env.example .env # edit with your DB credentials
go run .
# Frontend (just serve static files)
# Use any HTTP server pointed at src/
python3 -m http.server 3000 --directory src
License
Proprietary. All rights reserved.