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 13:42:23 +00:00

9.8 KiB

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 (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.

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:

// 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):

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_idprovider_config_id in channel and completion request/response bodies. Frontend updated to match.