255 lines
13 KiB
Markdown
255 lines
13 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 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.
|