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/docs/ARCHITECTURE.md
2026-02-26 21:19:55 +00:00

275 lines
14 KiB
Markdown

# Architecture — Chat Switchboard v0.11
## 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 (provider API sync, per-provider authoritative) → heuristic inference (regex patterns on model ID). No static model table — the same model can have different capabilities on different providers. The catalog is populated by auto-fetch on provider creation and manual refresh.
6. **Channels as Execution Context**: Channels are the universal container for conversation state — messages, tool activity, notes, and artifacts all hang off a channel. Today channels are single-user direct chats. The architecture anticipates multi-participant channels (team members, anonymous visitors, AI personas) for workflow execution, without requiring changes to the message tree, tool framework, or streaming infrastructure.
## Workflow Architecture (Future — v0.21.0+)
The platform's existing primitives (teams, personas, channels, tools, notes)
compose into a workflow engine where the channel is the execution context
that moves through defined stages.
### Conceptual Model
```
Team
└─ Workflow (team-admin defined)
├─ name, description
├─ entry conditions (public link, team-internal, API trigger)
└─ Stages[]
├─ persona_id (which AI drives this stage)
├─ assignment_team_id (who can be assigned)
├─ form_template (structured note schema, optional)
└─ transitions[] (conditions → next stage)
Channel (workflow instance)
├─ workflow_id + current_stage
├─ participants[]
│ ├─ anonymous visitor (mTLS fingerprint / session token)
│ ├─ AI persona (per-stage, from workflow definition)
│ └─ assigned team member (claimed or auto-routed)
├─ messages (existing tree structure)
├─ notes (channel-scoped artifacts, intake forms)
└─ tool activity (existing execution framework)
```
### How Existing Primitives Map
| Existing Primitive | Workflow Role |
|-------------------|---------------|
| **Persona** (system prompt + model) | Drives a workflow stage — the AI knows what to collect, when to route |
| **Team** (members + roles) | Owns the workflow definition; members are assignable to channels |
| **Channel** (messages + tree) | Execution instance of a workflow; full conversation history |
| **Notes + Tools** | Structured data collection; the persona's system prompt *is* the form definition, the note *is* the filled form |
| **EventBus + WebSocket** | Real-time notifications for assignment, stage transitions, new messages |
### Design Constraints for Current Development
These invariants keep the workflow path open without building it prematurely:
- **Channels**: Don't assume single-owner. If touching channel queries, keep
room for `team_id`, `type` (beyond `direct`), and multi-participant access
patterns alongside `user_id`.
- **Notes**: Currently user-scoped. Future channel-scoped notes (attached to
a conversation, not a personal notebook) need a `channel_id` FK option.
- **Personas**: Already team-scoped. Don't couple to "user picks from dropdown"
— a workflow stage references a persona programmatically.
- **Tool ExecutionContext**: Already carries `UserID` + `ChannelID`. Will need
`TeamID` and `WorkflowID` — the struct is easily extended.
- **Auth**: mTLS anonymous users need identity to participate in channels.
Lightest version: a `participants` table that can reference a `user_id` or
an opaque session identifier (cert fingerprint). Don't assume every channel
participant has a row in `users`.
## 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 # Heuristic detection + resolution
│ └── 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. 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
## 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 encrypted with AES-256-GCM. Two-tier: org keys use `ENCRYPTION_KEY` env var, personal BYOK keys use per-user encryption keys (admins cannot recover)
- **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
## File Storage
Blob storage for attachments, extracted text, and (future) knowledge base documents.
**Interface**: `storage.ObjectStore``Put`, `Get`, `Delete`, `DeletePrefix`, `Exists`, `Healthy`, `Stats`, `Backend`. All handlers use the interface; backend is selected at startup.
**Backends**:
| Backend | Config | Use Case |
|---------|--------|----------|
| **PVC** | `STORAGE_BACKEND=pvc` + `STORAGE_PATH` | Single-node, dev, docker-compose. Local filesystem with atomic writes (temp+rename). |
| **S3** | `STORAGE_BACKEND=s3` + `S3_ENDPOINT`, `S3_BUCKET`, credentials | Multi-node production. MinIO, Ceph RGW, AWS S3. Uses minio-go v7. |
| **Auto** | `STORAGE_BACKEND=` (empty) | Tries PVC at `STORAGE_PATH`; disables if not writable. |
**Key layout**: `attachments/{channel_id}/{attachment_id}_{filename}`. S3 prefix (`S3_PREFIX`) prepended for shared buckets.
**PVC always mounted**: Even with S3 backend, the PVC mount at `STORAGE_PATH` is needed for the extraction queue's local scratch directory (`processing/{id}/status.json`). With S3, the PVC can be small (1Gi) — only transient coordination state, not blobs.
**Admin panel**: Settings → Storage tab shows backend type, health status, file count, total size, endpoint/bucket (S3) or path (PVC), orphan detection and cleanup.
## 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.