This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/ARCHITECTURE.md
2026-02-23 01:57:28 +00:00

199 lines
9.9 KiB
Markdown

# Architecture — Chat Switchboard v0.9
## Deployment Modes
Three Docker images support different deployment scenarios:
| Image | Dockerfile | Contents | Use Case |
|-------|-----------|----------|----------|
| **Unified** | `Dockerfile` | nginx + Go backend | Dev, docker-compose, single-node |
| **Backend** | `server/Dockerfile` | Go binary only | K8s — scale API independently |
| **Frontend** | `Dockerfile.frontend` | nginx + static files | K8s — scale FE independently |
**Unified** bundles everything in one container. Nginx serves static files and proxies `/api/*` and `/ws` to the Go backend running on `:8080`. Good for development and small deployments.
**Split (Backend + Frontend)** for production K8s: Ingress routes `/api/*` and `/ws` to the backend Service, everything else to the frontend Service. The frontend entrypoint (`docker-entrypoint-fe.sh`) handles `BASE_PATH` injection into `index.html` and dynamic nginx config generation at startup. Supports branding volume mounts at `/branding/`.
```
┌─────────────────────────┐
│ Ingress / Traefik │
│ ├─ /api/* → be-svc:8080 │
│ ├─ /ws → be-svc:8080 │
│ └─ /* → fe-svc:80 │
└─────────────────────────┘
│ │
┌──────────▼──┐ ┌───────▼────────┐
│ Backend │ │ Frontend │
│ (Go :8080) │ │ (nginx :80) │
│ replicas:N │ │ replicas:M │
└──────┬──────┘ └────────────────┘
┌──────▼──────┐
│ PostgreSQL │
└─────────────┘
```
## Design Principles
1. **Persona-as-Trust-Boundary**: A persona (model + config + prompt) is the unit of access control. Users interact with personas, not raw provider configs. Admins control which models are visible; team admins control which personas their team can use.
2. **Roles vs Teams**: Clean separation between vertical permissions (Roles: admin, user) and horizontal visibility (Teams: organizational units). A user's role determines what they can *do*; their team membership determines what they can *see*.
3. **Store Layer**: All database access goes through typed Go interfaces (`store.Stores`). Handlers never write raw SQL. This enables future portability (SQLite for dev, Postgres for prod) and testability (mock stores).
4. **Scope Model**: Provider configs, personas, and model settings all use a three-value `scope` column: `global` (admin-managed, visible to all), `team` (team-admin-managed, visible to team), `personal` (user-managed, visible to owner). The `owner_id` column points to the owning user or team depending on scope.
5. **Capabilities Resolution**: Model capabilities (vision, tool calling, thinking, context window) are resolved through a priority chain: catalog DB → known model table → heuristic inference. This ensures every model has capabilities even if the provider API doesn't report them.
## Package Structure
```
server/
├── main.go # Wiring: stores → handlers → routes
├── config/config.go # Env-based configuration
├── database/
│ ├── database.go # Connection management
│ ├── migrate.go # Auto-migration on startup
│ └── migrations/
│ └── 001_v09_schema.sql # Consolidated schema
├── store/
│ ├── interfaces.go # Store interfaces + shared types
│ └── postgres/ # Postgres implementations
│ ├── stores.go # NewStores() constructor
│ ├── provider.go # ProviderStore
│ ├── catalog.go # CatalogStore
│ ├── persona.go # PersonaStore
│ ├── user.go # UserStore
│ ├── team.go # TeamStore
│ ├── policy.go # PolicyStore
│ ├── audit.go # AuditStore
│ └── ...
├── models/models.go # Shared domain types
├── capabilities/
│ ├── intrinsic.go # Known model table + heuristics
│ └── resolver.go # ModelsForUser() unified resolver
├── handlers/ # HTTP handlers (Gin)
│ ├── auth.go # Login, register, refresh, logout
│ ├── admin.go # User/config/model management
│ ├── channels.go # Channel CRUD
│ ├── messages.go # Message CRUD + forking
│ ├── completion.go # Chat completions (SSE streaming)
│ ├── capabilities.go # Model list + ResolveModelCaps
│ ├── presets.go # Persona CRUD (all scopes)
│ ├── apiconfigs.go # User provider config CRUD (BYOK)
│ ├── teams.go # Team management
│ └── ...
├── providers/ # LLM provider adapters
│ ├── provider.go # Provider interface
│ ├── anthropic.go
│ ├── openai.go
│ ├── openrouter.go
│ └── venice.go
├── middleware/ # Auth, admin, CORS, rate limiting
├── events/ # EventBus + WebSocket hub
└── tools/ # Built-in tool definitions (notes)
```
## Store Layer Pattern
Every store follows the same pattern:
```go
// Interface in store/interfaces.go
type FooStore interface {
Create(ctx context.Context, f *models.Foo) error
GetByID(ctx context.Context, id string) (*models.Foo, error)
Update(ctx context.Context, id string, patch models.FooPatch) error
Delete(ctx context.Context, id string) error
List(ctx context.Context, opts ListOptions) ([]models.Foo, int, error)
}
// Implementation in store/postgres/foo.go
type fooStore struct{ db *sql.DB }
func (s *fooStore) Create(ctx context.Context, f *models.Foo) error { ... }
```
Handlers receive `store.Stores` (a bundle of all store interfaces):
```go
type AdminHandler struct { stores store.Stores }
func (h *AdminHandler) CreateUser(c *gin.Context) {
// ...
err := h.stores.Users.Create(c.Request.Context(), user)
}
```
## Capabilities Resolution Chain
When the system needs to know what a model can do (vision? tools? thinking?):
```
1. model_catalog DB (exact match: model_id + provider_config_id)
↓ miss
2. model_catalog DB (any provider: same model_id)
↓ miss
3. Known model table (static, curated in capabilities/intrinsic.go)
↓ miss
4. Heuristic inference (name-based: "gpt-4-vision" → vision=true)
```
The `capabilities.ModelsForUser()` function combines catalog entries, team personas, and user preferences into a single unified model list for the frontend.
## Scope / Ownership Model
```
scope='global' → owner_id=NULL → Admin-managed, visible to all
scope='team' → owner_id=team.id → Team-admin-managed, visible to team
scope='personal' → owner_id=user.id → User-managed, visible to owner
```
Used by: `provider_configs`, `personas`, `model_catalog` (visibility column adds `enabled`/`disabled`/`team` states on top).
## Schema Migration
Single consolidated migration (`001_v09_schema.sql`) replaces the previous 21 incremental migrations. The Go backend auto-migrates on startup:
1. Creates `schema_migrations` table if absent
2. Checks which migration files have been applied
3. Applies any new `.sql` files in order
For v0.8 → v0.9 upgrades, a future `002_v08_to_v09.sql` migration will handle the transition. Fresh installs use `001_v09_schema.sql` directly.
## Frontend Architecture
Vanilla JavaScript, no build step. Five files with clear responsibilities:
| File | Role |
|------|------|
| `api.js` | HTTP client with token refresh. All backend calls. |
| `app.js` | Application state machine. Business logic. |
| `ui.js` | DOM rendering. All `document.createElement` calls. |
| `events.js` | Labeled event bus with WebSocket bridge. |
| `debug.js` | Admin debug panel (model list, stats, config). |
Communication: `app.js` calls `API.*` methods, updates state, then calls `UI.*` methods to render. `events.js` handles real-time updates via WebSocket. No framework, no virtual DOM, no reactive bindings.
## Security Model
- **Auth**: JWT access tokens (short-lived) + refresh tokens (DB-stored, revocable)
- **Admin Bootstrap**: `SWITCHBOARD_ADMIN_USERNAME`/`PASSWORD` env vars create/update admin on every startup (K8s secret pattern)
- **API Key Storage**: Provider API keys stored in `api_key_enc` column (TODO: at-rest encryption)
- **Policies**: Boolean flags in `global_settings` table control registration, BYOK, team providers, etc.
- **Audit**: All admin operations logged to `audit_log` with actor, action, resource type/ID, and diff
- **Banner**: Environment classification banner configurable via admin settings (text, color, position)
- **CORS**: Configurable allowed origins via env var
- **No sensitive terminology**: Banner system avoids classification-related terms for security compliance
## Backward Compatibility
v0.9 maintains backward-compatible API routes:
| Old Route | New Handler | Notes |
|-----------|-------------|-------|
| `/api/v1/presets` | PersonaHandler | Returns both `personas` and `presets` keys |
| `/api/v1/api-configs` | ProviderConfigHandler | Unchanged route, new implementation |
| `/api/v1/models` | ModelHandler.ListEnabledModels | Alias for `/models/enabled` |
JSON field rename: `api_config_id``provider_config_id` in channel and completion request/response bodies. Frontend updated to match.