# ICD-API — Chat Switchboard Backend API Contract **Version:** 0.22.7 **Updated:** 2026-03-03 **Audience:** Frontend developers, integrators, API consumers > **Organization principle:** Each section is one domain — the complete story. > Every endpoint that touches Knowledge Bases is in §5, every endpoint that > touches Personas is in §4, etc. Admin and user endpoints live together in > their domain section. Cross-cutting platform admin (user management, global > settings, audit) lives in §16. --- ## Table of Contents 1. [Conventions](#1-conventions) 2. [Auth](#2-auth) 3. [Channels & Conversations](#3-channels--conversations) 4. [Personas](#4-personas) 5. [Knowledge Bases](#5-knowledge-bases) 6. [Notes](#6-notes) 7. [Workspaces & Git](#7-workspaces--git) 8. [Projects](#8-projects) 9. [Memory](#9-memory) 10. [Providers & Routing](#10-providers--routing) 11. [Models & Preferences](#11-models--preferences) 12. [Notifications](#12-notifications) 13. [Extensions](#13-extensions) 14. [User Profile & Settings](#14-user-profile--settings) 15. [Teams & Access Control](#15-teams--access-control) 16. [Platform Administration](#16-platform-administration) 17. [Export & Utility](#17-export--utility) 18. [WebSocket Protocol](#18-websocket-protocol) 19. [Appendix: Enums](#19-appendix-enums) --- ## 1. Conventions ### Base URL ``` {scheme}://{host}{BASE_PATH}/api/v1 ``` `BASE_PATH` is empty by default. In path-routed K8s deployments (e.g. `/staging/`), all routes shift accordingly. The frontend reads `BASE_PATH` from a `` tag injected by the Go template engine. ### Authentication JWT bearer tokens. Every request to a `protected` or `admin` route requires: ``` Authorization: Bearer {access_token} ``` **Access token:** HS256 JWT, 15-minute TTL. Claims: `user_id`, `email`, `role`, `exp`, `iat`, `jti`. **Refresh token:** Opaque, DB-stored, 7-day TTL. Used to obtain new access tokens without re-login. **WebSocket fallback:** `?token={access_token}` query parameter when the `Authorization` header isn't available. **Cookie sync:** On every token save/refresh, the frontend writes `sb_token` as a cookie. Go template page routes read this cookie via `AuthOrRedirect` middleware for server-rendered surfaces. ### Authorization Tiers | Tier | Middleware | Who | |------|-----------|-----| | Public | none | Anyone (health, login, register, public settings) | | Authenticated | `Auth()` | Any logged-in user | | Team Admin | `RequireTeamAdmin()` | Admin of the specific team | | Platform Admin | `RequireAdmin()` | Users with `role = "admin"` | ### Error Envelope All errors return: ```json { "error": "human-readable message" } ``` Standard HTTP status codes: 400 (bad request), 401 (not authenticated), 403 (not authorized), 404 (not found), 409 (conflict), 500 (server error), 502 (upstream provider failure). ### Pagination Envelope Endpoints that paginate return: ```json { "data": [...], "page": 1, "per_page": 50, "total": 142, "total_pages": 3 } ``` Query params: `?page=1&per_page=50`. Not all list endpoints paginate — some return `{ "data": [...] }` or a bare array in a named key. ### ID Format All resource IDs are UUIDv4 strings, generated application-side via `store.NewID()` (Go `uuid.New()`). This ensures compatibility across both Postgres and SQLite backends. ### Timestamps ISO 8601 with timezone: `"2025-06-15T14:30:00Z"`. Stored as `TIMESTAMPTZ` (Postgres) or `TEXT` (SQLite). ### Naming Note The domain concept is **Persona** (identity + system prompt + model + KB bindings + grant scoping). API routes currently use `/personas` for historical reasons. A pre-1.0 rename to `/personas` is planned. This document uses "Persona" for the concept and shows the current route paths as-is. --- ## 2. Auth Four endpoints. No token required for any of them. ### Register ``` POST /auth/register ``` ```json { "username": "jdoe", "password": "...", "email": "jdoe@example.com", // optional "display_name": "Jane Doe" // optional } ``` Gated by `allow_registration` policy. Returns same shape as Login. ### Login ``` POST /auth/login ``` ```json { "username": "jdoe", "password": "..." } ``` **Response:** ```json { "access_token": "eyJ...", "refresh_token": "opaque-string", "user": { "id": "uuid", "username": "jdoe", "email": "jdoe@example.com", "display_name": "Jane Doe", "role": "user", "avatar_url": "/api/v1/profile/avatar?v=1234", "created_at": "...", "last_login": "..." } } ``` ### Refresh ``` POST /auth/refresh ``` ```json { "refresh_token": "opaque-string" } ``` Returns new `access_token` and `refresh_token`. Old refresh token is invalidated (rotation). ### Logout ``` POST /auth/logout ``` ```json { "refresh_token": "opaque-string" } ``` Revokes the refresh token. Returns `{ "message": "logged out" }`. --- ## 3. Channels & Conversations The **channel** is the universal conversation container. Messages, tool activity, attachments, and KB bindings all hang off a channel. Today channels are single-user direct chats; the architecture anticipates multi-participant channels for v0.23.0. ### 3.1 Channel CRUD **List channels** — paginated, sorted by last activity. ``` GET /channels?page=1&per_page=50 ``` Returns pagination envelope. Each channel: ```json { "id": "uuid", "user_id": "uuid", "title": "My Chat", "type": "direct", "description": "", "model": "claude-sonnet-4-20250514", "provider_config_id": "uuid|null", "system_prompt": "", "project_id": "uuid|null", "folder": "string|null", "tags": ["tag1", "tag2"], "created_at": "...", "updated_at": "..." } ``` **Create channel:** ``` POST /channels ``` ```json { "title": "New Chat", "type": "direct", "description": "", "model": "claude-sonnet-4-20250514", "provider_config_id": "uuid", "system_prompt": "", "folder": null, "tags": [] } ``` On create, if `model` and `provider_config_id` are provided, the channel model roster (`channel_models`) is auto-populated with an initial entry. **Get channel:** ``` GET /channels/:id ``` Returns single channel object. **Update channel:** ``` PUT /channels/:id ``` Accepts partial updates — only fields present in the body are changed. Same field set as create. **Delete channel:** ``` DELETE /channels/:id ``` Cascades: deletes messages, attachments, channel_models, channel_kbs. ### 3.2 Message Tree Switchboard uses a **tree** model for messages, not a flat list. Each message has a `parent_id` (NULL for root). Editing creates a sibling branch; regeneration creates an alternative sibling. The **cursor** tracks which child is "active" at each branch point. **Get active path** — the primary endpoint for loading chat history: ``` GET /channels/:id/path ``` Returns the messages along the active branch from root to leaf, following the cursor at each fork. This is what the UI renders. ```json { "messages": [ { "id": "uuid", "channel_id": "uuid", "parent_id": "uuid|null", "role": "user|assistant|system|tool", "content": "...", "model": "claude-sonnet-4-20250514|null", "provider_config_id": "uuid|null", "thinking": "...|null", "tool_calls": [...], "tool_results": [...], "attachments": [...], "has_siblings": true, "sibling_index": 0, "sibling_count": 2, "created_at": "..." } ] } ``` **List all messages** — includes all branches, flat: ``` GET /channels/:id/messages ``` Legacy/debug endpoint. Returns every message in the channel regardless of branch. Not used by the standard frontend. **Create message:** ``` POST /channels/:id/messages ``` ```json { "role": "user", "content": "Hello", "parent_id": "uuid|null", "attachments": ["attachment-uuid-1"] } ``` **Edit message** — creates a sibling at the same level: ``` POST /channels/:id/messages/:msgId/edit ``` ```json { "content": "Revised question" } ``` Creates a new message with the same `parent_id` as the original. Automatically updates the cursor to point to the new sibling. **Regenerate** — creates an alternative assistant response: ``` POST /channels/:id/messages/:msgId/regenerate ``` ```json { "model": "claude-sonnet-4-20250514", "provider_config_id": "uuid" } ``` Creates a new empty assistant message as a sibling of `msgId`, then streams the completion into it. The cursor is updated. **Update cursor** — switch to a different branch: ``` PUT /channels/:id/cursor ``` ```json { "message_id": "uuid", "child_id": "uuid" } ``` Sets which child is active at the `message_id` branch point. Subsequent `GET /channels/:id/path` will follow the new branch. **List siblings** — all children of a message's parent: ``` GET /channels/:id/messages/:msgId/siblings ``` Returns `{ "siblings": [...] }` with abbreviated message objects (id, role, content preview, created_at). ### 3.3 Completions & Streaming The single most complex endpoint. Sends a message to an LLM provider and streams the response via Server-Sent Events. ``` POST /chat/completions ``` **Request:** ```json { "channel_id": "uuid", "message": "User's message text", "model": "claude-sonnet-4-20250514", "provider_config_id": "uuid", "persona_id": "uuid|null", "parent_id": "uuid|null", "tools_enabled": true, "tool_ids": ["web_search", "calculator"], "attachments": ["attachment-uuid"], "extra_body": {} } ``` `channel_id` is required. `chat_id` is accepted as a deprecated alias (maps to `channel_id`; frontend no longer sends it — removal planned). `extra_body` is a freeform JSON object merged into the provider request (provider-specific parameters like `temperature`, `reasoning`, etc.). **Persona resolution:** If `persona_id` is set, the handler loads the persona's system prompt, model, provider_config_id, and KB bindings. Per-message `model`/`provider_config_id` override the persona's defaults. **Routing resolution:** After config resolution, the routing policy evaluator (`evaluateRouting()`) may redirect to a different provider based on active policies (see §10.4). The winning provider config's credentials are loaded and used for the actual API call. **SSE Stream Format:** The response is `Content-Type: text/event-stream`. Events: ``` data: {"content":"Hello"} data: {"content":" world"} data: {"reasoning":"Let me think..."} event: tool_use data: {"id":"call_1","name":"web_search","input":{"query":"..."}} event: tool_result data: {"id":"call_1","content":"Search results..."} data: {"content":"Based on the search..."} event: finish data: {"message_id":"uuid","model":"claude-sonnet-4-20250514","usage":{"input_tokens":150,"output_tokens":42}} data: [DONE] ``` | Event | `event:` field | Payload | |-------|---------------|---------| | Content delta | _(unnamed)_ | `{ "content": "text" }` | | Reasoning delta | _(unnamed)_ | `{ "reasoning": "text" }` | | Tool invocation | `tool_use` | `{ "id", "name", "input" }` | | Tool result | `tool_result` | `{ "id", "content" }` | | Stream complete | `finish` | `{ "message_id", "model", "usage" }` | | End sentinel | _(unnamed)_ | `[DONE]` (literal string) | | Error mid-stream | `error` | `{ "error": "message" }` | **Tool cycle:** Tool use and tool result events can repeat up to `max_tool_iterations` (configured server-side). The handler executes tools, feeds results back to the model, and continues streaming. **Response headers:** ``` X-Switchboard-Provider: providerID/configID X-Switchboard-Route: policy-name (when routing policy is active) ``` **List available tools:** ``` GET /tools ``` Returns `{ "tools": [...] }` where each tool has `name`, `description`, `parameters` (JSON Schema), and `category`. ### 3.4 Multi-Model Roster Channels can have multiple AI models. The `channel_models` table tracks the roster. `@mention` in message content routes to specific models. ``` GET /channels/:id/models → { "models": [...] } POST /channels/:id/models ← { "model": "...", "provider_config_id": "...", "display_name": "..." } PATCH /channels/:id/models/:modelId ← { "display_name": "..." } DELETE /channels/:id/models/:modelId ``` Each roster entry: ```json { "id": "uuid", "channel_id": "uuid", "model": "claude-sonnet-4-20250514", "provider_config_id": "uuid", "display_name": "Claude", "is_default": true, "created_at": "..." } ``` ### 3.5 Channel Utilities **Summarize channel** (compaction): ``` POST /channels/:id/summarize ``` Triggers conversation summarization via the utility model role. Returns `{ "message": "summarized", "summary_id": "uuid" }`. **Generate title:** ``` POST /channels/:id/generate-title ``` Uses the utility model to auto-generate a title from the first exchange. Returns `{ "title": "Generated Title" }`. ### 3.6 Attachments File attachments are channel-scoped. Upload stores the file in blob storage; a background extraction pipeline processes documents (PDF, DOCX, XLSX, etc.) into searchable text. **Upload:** ``` POST /channels/:id/attachments Content-Type: multipart/form-data ``` Field: `file`. Returns the attachment object. **List by channel:** ``` GET /channels/:id/attachments ``` Returns `{ "attachments": [...] }`. **Get metadata:** ``` GET /attachments/:id ``` ```json { "id": "uuid", "channel_id": "uuid", "user_id": "uuid", "filename": "report.pdf", "content_type": "application/pdf", "size": 1048576, "storage_key": "attachments/chan-id/att-id_report.pdf", "extracted_text_key": "...|null", "metadata": {}, "created_at": "..." } ``` **Download:** ``` GET /attachments/:id/download ``` Returns the file with appropriate `Content-Type` and `Content-Disposition` headers. **Delete:** ``` DELETE /attachments/:id ``` --- ## 4. Personas A **Persona** is the unit of access control and AI identity: system prompt + model + provider config + KB bindings + grant scoping. Users interact with Personas, not raw provider configs. Admins curate which Personas are available; team admins create team-scoped Personas. Three scopes, three route namespaces, one domain: | Scope | Routes | Who manages | |-------|--------|-------------| | Personal | `GET/POST/PUT/DELETE /personas` | The user | | Team | `GET/POST/PUT/DELETE /teams/:teamId/personas` | Team admin | | Global (admin) | `GET/POST/PUT/DELETE /admin/personas` | Platform admin | All three return the same Persona object shape: ```json { "id": "uuid", "name": "Code Reviewer", "description": "Reviews code for bugs and style", "system_prompt": "You are a senior code reviewer...", "model": "claude-sonnet-4-20250514", "provider_config_id": "uuid|null", "scope": "personal|team|global", "owner_id": "uuid|null", "is_default": false, "avatar_url": "/api/v1/personas/uuid/avatar?v=123", "created_at": "...", "updated_at": "..." } ``` ### 4.1 Personal Personas Gated by `allow_user_personas` policy. ``` GET /personas → { "personas": [...], "personas": [...] } POST /personas ← { "name", "description", "system_prompt", "model", ... } PUT /personas/:id ← partial update DELETE /personas/:id ``` **Note:** List response currently sends both `"personas"` and `"personas"` keys (identical arrays) for backward compatibility. The `"personas"` key is what the frontend reads. Dual keys will be collapsed pre-1.0. ### 4.2 Team Personas ``` GET /teams/:teamId/personas → { "personas": [...], "personas": [...] } POST /teams/:teamId/personas ← same shape PUT /teams/:teamId/personas/:id DELETE /teams/:teamId/personas/:id ``` ### 4.3 Admin (Global) Personas ``` GET /admin/personas → { "personas": [...], "personas": [...] } POST /admin/personas ← same shape PUT /admin/personas/:id DELETE /admin/personas/:id ``` ### 4.4 Persona KB Bindings A Persona can have Knowledge Bases bound to it. When a channel uses that Persona, the bound KBs are automatically scoped into `kb_search` tool calls. Users never see or manage the underlying KBs directly — they talk to the Persona. **Get bindings:** ``` GET /personas/:id/knowledge-bases → { "data": [KB objects] } GET /teams/:teamId/personas/:id/knowledge-bases → { "data": [...] } GET /admin/personas/:id/knowledge-bases → { "data": [...] } ``` **Set bindings** (replace all): ``` PUT /personas/:id/knowledge-bases PUT /teams/:teamId/personas/:id/knowledge-bases PUT /admin/personas/:id/knowledge-bases ``` ```json { "kb_ids": ["kb-uuid-1", "kb-uuid-2"], "auto_search": { "kb-uuid-1": true, "kb-uuid-2": false } } ``` `auto_search`: when `true`, the KB is always searched (prepended to context). When `false`, it's available via the `kb_search` tool but not auto-injected. ### 4.5 Persona Avatars ``` POST /personas/:id/avatar ← multipart/form-data (field: "avatar") DELETE /personas/:id/avatar POST /admin/personas/:id/avatar DELETE /admin/personas/:id/avatar ``` ### 4.6 Resource Grants See §15.3 for the grant system. Personas use grants to control who can see and use them beyond their base scope. --- ## 5. Knowledge Bases The **Knowledge Base** (KB) is the RAG system: upload documents → chunk → embed (pgvector / app-level cosine for SQLite) → search via `kb_search` tool during completions. ### 5.1 KB CRUD ``` GET /knowledge-bases → { "data": [KB objects] } POST /knowledge-bases ← { "name", "description", "scope", "team_id" } GET /knowledge-bases/:id → KB object PUT /knowledge-bases/:id ← partial update DELETE /knowledge-bases/:id ``` KB object: ```json { "id": "uuid", "name": "Product Docs", "description": "Internal product documentation", "scope": "personal|team|global", "owner_id": "uuid|null", "team_id": "uuid|null", "discoverable": true, "embedding_model": "text-embedding-3-small", "chunk_size": 512, "chunk_overlap": 50, "document_count": 12, "created_at": "...", "updated_at": "..." } ``` ### 5.2 Documents **Upload:** ``` POST /knowledge-bases/:id/documents Content-Type: multipart/form-data ``` Field: `file`. Supported: PDF, DOCX, TXT, MD, HTML, CSV, XLSX, PPTX. Returns the document object with `status: "pending"`. **List:** ``` GET /knowledge-bases/:id/documents ``` Returns `{ "data": [document objects] }`. **Status** (poll during processing): ``` GET /knowledge-bases/:id/documents/:docId/status ``` Status progression: `pending → chunking → embedding → ready | error`. **Delete:** ``` DELETE /knowledge-bases/:id/documents/:docId ``` ### 5.3 Search ``` POST /knowledge-bases/:id/search ``` ```json { "query": "How do I configure SSO?", "limit": 5 } ``` Returns `{ "results": [{ "content", "score", "document_id", "metadata" }] }`. ### 5.4 Rebuild Re-chunks and re-embeds all documents: ``` POST /knowledge-bases/:id/rebuild ``` ### 5.5 Channel KB Bindings A channel can have KBs explicitly bound to it (in addition to any KBs coming from the active Persona or parent Project). **Get:** ``` GET /channels/:id/knowledge-bases ``` Returns `{ "data": [ChannelKB objects] }`: ```json { "kb_id": "uuid", "kb_name": "Product Docs", "enabled": true, "document_count": 12 } ``` **Set** (replace all): ``` PUT /channels/:id/knowledge-bases ``` ```json { "kb_ids": ["kb-uuid-1", "kb-uuid-2"] } ``` ### 5.6 Discoverable KBs The `discoverable` flag controls whether a KB appears in the user's KB picker. Non-discoverable KBs are hidden from direct access but still searchable through Persona bindings (enterprise KB mode). **List discoverable** (for KB picker UI): ``` GET /knowledge-bases-discoverable ``` Returns `{ "data": [KB objects] }` — only KBs the user can see (personal + team + discoverable global). **Toggle discoverability** (admin-enforced in handler): ``` PUT /knowledge-bases/:id/discoverable ``` ```json { "discoverable": false } ``` ### 5.7 KB Resolution Chain When a completion fires, KBs are resolved in priority order: ``` Persona KBs → Project KBs → Channel KBs → Personal KBs ``` The `BuildKBHint` function assembles all applicable KBs into the system prompt, and the `kb_search` tool searches across all of them. ### 5.8 Persona KB Bindings See §4.4. Persona-KB binding is the primary enterprise mechanism for controlled KB access. --- ## 6. Notes **Obsidian-style** knowledge graph with `[[wikilinks]]`, backlinks, graph visualization, and folder organization. Notes are user-scoped (personal notebook). Future: channel-scoped notes for workflow artifacts. ### 6.1 CRUD ``` GET /notes → paginated list of noteListItem POST /notes ← { "title", "content", "folder" } GET /notes/:id → full note object PUT /notes/:id ← { "title", "content", "folder" } DELETE /notes/:id ``` Note object: ```json { "id": "uuid", "user_id": "uuid", "title": "Meeting Notes", "content": "# Meeting Notes\n\nDiscussed [[Project Alpha]]...", "folder": "work", "source_message_id": "uuid|null", "created_at": "...", "updated_at": "..." } ``` `source_message_id` links a note back to the chat message that created it (provenance for AI-generated notes). `noteListItem` is the same but with `content` truncated or omitted. ### 6.2 Search **Full-text search:** ``` GET /notes/search?q=project+alpha ``` Uses `plainto_tsquery` (Postgres) or `LIKE` fallback (SQLite). Returns `{ "data": [searchResult objects] }` with match highlights. **Title search** (lightweight, for wikilink autocomplete): ``` GET /notes/search-titles?q=proj ``` Returns `{ "data": [{ "id", "title" }] }`. ### 6.3 Graph **Full graph topology:** ``` GET /notes/graph ``` ```json { "nodes": [{ "id": "uuid", "title": "Note Title" }], "edges": [{ "source": "uuid", "target": "uuid" }], "unresolved": [{ "source": "uuid", "target_title": "Missing Note" }] } ``` Edges are directed: source contains `[[target_title]]`. Unresolved edges have a `target_title` but no matching note (dangling wikilink). When a note with that title is created, the edge auto-resolves. **Backlinks** (who links to this note): ``` GET /notes/:id/backlinks ``` Returns `{ "data": [note objects] }` — all notes containing `[[this note's title]]`. ### 6.4 Folders ``` GET /notes/folders ``` Returns `{ "folders": ["work", "personal", "archive"] }` — distinct folder values across all user notes. ### 6.5 Bulk Operations ``` POST /notes/bulk-delete ``` ```json { "ids": ["uuid-1", "uuid-2"] } ``` --- ## 7. Workspaces & Git The **Workspace** is a virtual filesystem for the editor surface. Each workspace has a root directory, file operations, optional Git integration, and full-text indexing for code search. ### 7.1 Workspace CRUD ``` GET /workspaces → { "data": [...] } POST /workspaces ← { "name", "owner_type", "owner_id" } GET /workspaces/:id → workspace object PATCH /workspaces/:id ← partial update DELETE /workspaces/:id ``` Workspace object: ```json { "id": "uuid", "name": "my-project", "owner_type": "user|project", "owner_id": "uuid", "status": "active|archived", "storage_bytes": 1048576, "file_count": 42, "created_at": "...", "updated_at": "..." } ``` ### 7.2 File Operations All paths are relative to the workspace root. ``` GET /workspaces/:id/files?path=/ → { "files": [entries] } GET /workspaces/:id/files/read?path=/a.md → { "content": "...", "size": 123 } PUT /workspaces/:id/files/write ← { "path": "/a.md", "content": "..." } DELETE /workspaces/:id/files/delete ← { "path": "/a.md" } POST /workspaces/:id/files/mkdir ← { "path": "/src" } ``` File entry: ```json { "name": "main.go", "path": "/src/main.go", "is_dir": false, "size": 4096, "modified": "..." } ``` ### 7.3 Archive **Download** (tar.gz of entire workspace): ``` GET /workspaces/:id/archive/download ``` **Upload** (restore from tar.gz): ``` POST /workspaces/:id/archive/upload Content-Type: multipart/form-data ``` ### 7.4 Reconcile, Stats, Index **Reconcile** (sync filesystem state with DB metadata): ``` POST /workspaces/:id/reconcile ``` **Stats:** ``` GET /workspaces/:id/stats ``` Returns `{ "file_count", "storage_bytes", "indexed_files", ... }`. **Index status:** ``` GET /workspaces/:id/index-status ``` Returns indexing progress for full-text search. ### 7.5 Git Integration All Git operations are workspace-scoped. ``` POST /workspaces/:id/git/clone ← { "url", "branch", "credential_id" } POST /workspaces/:id/git/pull POST /workspaces/:id/git/push GET /workspaces/:id/git/status → { "files": [{ "path", "status" }] } GET /workspaces/:id/git/diff → { "diff": "..." } POST /workspaces/:id/git/commit ← { "message", "files": [...] } GET /workspaces/:id/git/log → { "commits": [...] } GET /workspaces/:id/git/branches → { "branches": [...], "current": "main" } POST /workspaces/:id/git/checkout ← { "branch": "feature-x" } ``` ### 7.6 Git Credentials User-owned, encrypted credential storage for Git operations. Encrypted with the user's UEK (same as BYOK keys — admins cannot recover). ``` POST /git-credentials ← { "name", "auth_type", "token"|"username"+"password"|"ssh_key" } GET /git-credentials → { "data": [GitCredentialSummary] } DELETE /git-credentials/:id ``` `auth_type`: `https_pat`, `https_basic`, or `ssh_key`. Summary response (never exposes encrypted data): ```json { "id": "uuid", "name": "GitHub PAT", "auth_type": "https_pat", "created_at": "..." } ``` --- ## 8. Projects **Projects** are organizational containers that group channels, knowledge bases, notes, and files. They follow the scope model (personal, team, global). ### 8.1 Project CRUD ``` GET /projects → { "data": [...] } POST /projects ← { "name", "description", "scope", "team_id", "persona_id", "system_prompt" } GET /projects/:id → project object PUT /projects/:id ← partial update DELETE /projects/:id ``` Project object: ```json { "id": "uuid", "name": "Q3 Research", "description": "...", "scope": "personal|team|global", "owner_id": "uuid", "team_id": "uuid|null", "persona_id": "uuid|null", "system_prompt": "...|null", "is_archived": false, "channel_count": 5, "created_at": "...", "updated_at": "..." } ``` ### 8.2 Channel Association ``` GET /projects/:id/channels → { "data": [channel objects] } POST /projects/:id/channels ← { "channel_id": "uuid" } DELETE /projects/:id/channels/:channelId PUT /projects/:id/channels/reorder ← { "channel_ids": ["uuid1", "uuid2"] } ``` A channel can only belong to one project. `POST` performs an atomic move if the channel is already in a different project. ### 8.3 KB Association ``` GET /projects/:id/knowledge-bases → { "data": [KB objects] } POST /projects/:id/knowledge-bases ← { "kb_id": "uuid" } DELETE /projects/:id/knowledge-bases/:kbId ``` KBs bound to a project are automatically available to every channel in that project (see §5.7 resolution chain). ### 8.4 Note Association ``` GET /projects/:id/notes → { "data": [note objects] } POST /projects/:id/notes ← { "note_id": "uuid" } DELETE /projects/:id/notes/:noteId ``` ### 8.5 Project Files ``` GET /projects/:id/files → { "files": [...] } POST /projects/:id/files ← multipart/form-data ``` Project-level file uploads (distinct from workspace files and channel attachments). ### 8.6 Admin Project Management ``` GET /admin/projects → { "data": [...] } DELETE /admin/projects/:id ``` Cross-instance visibility for platform admins. BYOK personal projects remain private (admin can delete but not read content). --- ## 9. Memory **Long-term memory** extracted from conversations. The system identifies facts, preferences, and context about the user and stores them for injection into future conversations. ### 9.1 User Memory ``` GET /memories → { "data": [memory objects] } GET /memories/count → { "count": 42 } PUT /memories/:id ← { "content": "updated text" } DELETE /memories/:id POST /memories/:id/approve → approve a pending memory POST /memories/:id/reject → reject a pending memory ``` Memory object: ```json { "id": "uuid", "user_id": "uuid", "persona_id": "uuid|null", "scope": "user|persona", "content": "User prefers Go for backend work", "source_channel_id": "uuid", "status": "pending|approved|rejected", "confidence": 0.85, "created_at": "...", "updated_at": "..." } ``` `scope`: `user` memories apply across all conversations. `persona` memories are specific to interactions with that Persona. ### 9.2 Admin Memory Review ``` GET /admin/memories/pending → { "data": [memory objects with user info] } POST /admin/memories/bulk-approve ← { "ids": ["uuid1", "uuid2"] } ``` Platform admin can review and bulk-approve pending memories across all users. --- ## 10. Providers & Routing The **multi-provider** system. One or more LLM providers are configured, each with their own API keys, endpoints, and model catalogs. The routing layer decides which provider handles each request. ### 10.1 User BYOK Provider Configs Gated by `allow_user_byok` policy. Personal API keys are encrypted with the user's UEK (per-user encryption key, Argon2id-derived). Platform admins cannot recover personal keys. ``` GET /api-configs → { "configs": [safeConfig objects] } POST /api-configs ← { "name", "provider", "endpoint", "api_key", ... } GET /api-configs/:id → safeConfig PUT /api-configs/:id ← partial update (api_key optional) DELETE /api-configs/:id ``` `safeConfig` — API keys are **never** returned: ```json { "id": "uuid", "name": "My OpenAI", "provider": "openai", "endpoint": "https://api.openai.com/v1", "model_default": "gpt-4o", "scope": "personal", "owner_id": "uuid", "is_active": true, "has_key": true, "config": {}, "headers": {}, "settings": {}, "created_at": "..." } ``` **List models for a user config:** ``` GET /api-configs/:id/models ``` Returns `{ "models": [catalog entries] }`. **Fetch/sync models from provider API:** ``` POST /api-configs/:id/models/fetch ``` Calls the provider's model list API, upserts into the local catalog. ### 10.2 Admin Global Provider Configs ``` GET /admin/configs → { "configs": [configWithKey objects] } POST /admin/configs ← { "name", "provider", "endpoint", "api_key", "config", "headers", "settings", "is_private" } PUT /admin/configs/:id ← partial update DELETE /admin/configs/:id ``` `configWithKey` is the same as `safeConfig` but comes from `ListGlobal` — still redacts API keys, just adds the `has_key` flag. Admin configs use the `ENCRYPTION_KEY` env var (not per-user UEK). `is_private`: when true, the config is available for admin-created Personas but not directly selectable by users. ### 10.3 Model Catalog (Admin) The catalog is populated by fetching from provider APIs and stores model metadata (capabilities, context window, pricing). ``` GET /admin/models → { "models": [catalog entries] } PUT /admin/models/:id ← { "visibility", "display_name", ... } PUT /admin/models/bulk ← { "provider_config_id", "visibility" } DELETE /admin/models/:id POST /admin/models/fetch ← { "provider_config_id": "uuid|empty" } ``` **Fetch** with empty `provider_config_id` syncs ALL active global providers. Returns: ```json { "message": "models synced", "added": 5, "updated": 12, "total": 47 } ``` Or for multi-provider fetch: `{ "added", "updated", "total", "errors": [...] }`. **Bulk visibility** sets all models for a provider (or all models globally if no `provider_config_id`) to the specified visibility (`enabled`, `disabled`, `team`). ### 10.4 Provider Health Real-time health tracking per provider config. The health accumulator records success/failure/latency on every completion, flushes to DB every 60 seconds. **Get all:** ``` GET /admin/providers/health ``` Returns `{ "data": [ProviderHealth objects] }`: ```json { "provider_config_id": "uuid", "provider_config_name": "OpenAI Production", "status": "healthy|degraded|down", "error_rate": 0.02, "avg_latency_ms": 450, "timeout_rate": 0.01, "rate_limit_count": 3, "last_check": "...", "last_error": "...|null" } ``` **Get single:** ``` GET /admin/providers/:id/health ``` **Auto-disable:** After `PROVIDER_AUTO_DISABLE_THRESHOLD` consecutive "down" windows (default: 3), the provider is automatically deactivated. ### 10.5 Capability Overrides Admin can override any model capability detected by the catalog or heuristic layer. ``` GET /admin/capability-overrides → { "data": [...] } GET /admin/models/:id/capabilities → capabilities for one model PUT /admin/models/:id/capabilities ← { "field": "value" } DELETE /admin/models/:id/capabilities/:overrideId ``` Override fields: `supports_vision`, `supports_tools`, `supports_thinking`, `context_window`, `max_output_tokens`, etc. ### 10.6 Routing Policies Policy-based request routing. Evaluated after model/config resolution, before provider dispatch. **Admin CRUD:** ``` GET /admin/routing/policies → { "data": [...] } GET /admin/routing/policies/:id → policy object POST /admin/routing/policies ← { "name", "scope", "team_id", "priority", "policy_type", "config", "is_active" } PUT /admin/routing/policies/:id DELETE /admin/routing/policies/:id ``` Policy object: ```json { "id": "uuid", "name": "Prefer Anthropic", "scope": "global|team", "team_id": "uuid|null", "priority": 10, "policy_type": "provider_prefer|team_route|cost_limit|model_alias", "config": {}, "is_active": true } ``` | Policy type | Config | Behavior | |------------|--------|----------| | `provider_prefer` | `{ "providers": ["cfg-1", "cfg-2"] }` | Ordered fallback list | | `team_route` | `{ "providers": ["cfg-1"] }` | Restrict team to specific providers | | `cost_limit` | `{ "max_cost_per_request": 0.50 }` | Heuristic cost cap | | `model_alias` | `{ "alias": "fast", "target_model": "...", "target_config": "..." }` | Alias → provider+model rewrite | **Dry-run test:** ``` POST /admin/routing/test ``` ```json { "model": "claude-sonnet-4-20250514", "user_id": "uuid", "team_id": "uuid|null" } ``` Returns the ranked candidate list with health status for each. ### 10.7 Provider Types Registry of supported provider types with metadata. ``` GET /admin/provider-types ``` Returns `{ "types": [...] }` with name, display name, default endpoint, profile schema, and supported features per type. --- ## 11. Models & Preferences What models are available to the current user and how they control visibility. ### 11.1 Enabled Models The primary endpoint for populating model selectors: ``` GET /models/enabled ``` > **Note:** `GET /models` exists as an alias. The alias is unused by > the frontend and will be removed pre-1.0. Returns `{ "models": [UserModel objects] }`: ```json { "id": "composite-id", "model_id": "claude-sonnet-4-20250514", "provider_config_id": "uuid", "provider_config_name": "Anthropic", "provider": "anthropic", "display_name": "Claude Sonnet 4", "model_type": "chat|embedding|image", "context_window": 200000, "max_output_tokens": 8192, "supports_vision": true, "supports_tools": true, "supports_thinking": true, "supports_streaming": true, "input_price_per_m": 3.00, "output_price_per_m": 15.00, "provider_status": "healthy|degraded|down|null", "scope": "global|team|personal", "source": "catalog|heuristic" } ``` The capabilities on each model are resolved through the three-tier chain: catalog DB → heuristic inference → admin overrides (see §10.5). ### 11.2 User Model Preferences Users can hide models they don't want to see. ``` GET /models/preferences → { "preferences": { "model-id": { "hidden": true } } } PUT /models/preferences ← { "model_id": "...", "hidden": true } POST /models/preferences/bulk ← { "preferences": { "id1": { "hidden": true }, "id2": { ... } } } ``` --- ## 12. Notifications Real-time notification infrastructure with WebSocket delivery and optional email transport. ### 12.1 CRUD ``` GET /notifications → paginated list GET /notifications/unread-count → { "count": 5 } PATCH /notifications/:id/read → mark one as read POST /notifications/mark-all-read → mark all as read DELETE /notifications/:id ``` Notification object: ```json { "id": "uuid", "user_id": "uuid", "type": "role.fallback|memory.extracted|system.announcement|...", "title": "Role Fallback Triggered", "body": "Primary model unavailable, using fallback", "metadata": {}, "read": false, "created_at": "..." } ``` ### 12.2 Preferences Users control per-type delivery: ``` GET /notifications/preferences → { "preferences": [...] } PUT /notifications/preferences/:type ← { "in_app": true, "email": false } DELETE /notifications/preferences/:type → reset to default ``` Resolution: specific type pref → user wildcard `*` pref → system default (`in_app=true`, `email=false`). --- ## 13. Extensions Plugin system with three tiers: Browser JS (client-side), Starlark sandbox (server-side, future), Sidecar containers (server-side, future). Currently only Browser tier is implemented. ### 13.1 User Extensions ``` GET /extensions → { "extensions": [...] } POST /extensions/:id/settings ← { "enabled": true, "config": {...} } GET /extensions/:id/manifest → full manifest.json GET /extensions/tools → { "tools": [tool schema objects] } ``` ### 13.2 Admin Extension Management ``` GET /admin/extensions → { "extensions": [...] } POST /admin/extensions ← { manifest + script content } PUT /admin/extensions/:id ← updated manifest/script DELETE /admin/extensions/:id ``` ### 13.3 Asset Serving ``` GET /extensions/:id/assets/*path ``` Public (no auth required). Serves static assets (icons, CSS, JS) from extension bundles. --- ## 14. User Profile & Settings ### 14.1 Profile ``` GET /profile → profileResponse PUT /profile ← { "display_name", "email" } ``` ```json { "id": "uuid", "username": "jdoe", "email": "jdoe@example.com", "display_name": "Jane Doe", "role": "user|admin", "avatar_url": "...", "created_at": "...", "last_login": "..." } ``` ### 14.2 Avatar ``` POST /profile/avatar ← multipart/form-data (field: "avatar") DELETE /profile/avatar ``` ### 14.3 Password ``` POST /profile/password ``` ```json { "current_password": "...", "new_password": "..." } ``` On password change, the UEK is re-wrapped with the new password (BYOK keys remain accessible without re-encryption). ### 14.4 App Settings ``` GET /settings → { "settings": {...} } PUT /settings ← { "theme", "editor_keybindings", ... } ``` User-level preferences (theme, keybindings, default model, etc.). --- ## 15. Teams & Access Control ### 15.1 My Teams ``` GET /teams/mine → { "teams": [...] } ``` Returns teams the current user is a member of. ### 15.2 Team Administration Team-admin-scoped routes (require `RequireTeamAdmin` middleware): **Team CRUD (platform admin):** ``` GET /admin/teams → { "teams": [...] } POST /admin/teams GET /admin/teams/:id PUT /admin/teams/:id DELETE /admin/teams/:id ``` **Members:** ``` GET /admin/teams/:id/members → { "members": [...] } POST /admin/teams/:id/members ← { "user_id", "role" } PUT /admin/teams/:id/members/:memberId ← { "role" } DELETE /admin/teams/:id/members/:memberId ``` **Team-scoped routes** (team admin, not platform admin): ``` GET /teams/:teamId/members POST /teams/:teamId/members ← { "user_id", "role" } PUT /teams/:teamId/members/:memberId DELETE /teams/:teamId/members/:memberId ``` **Team Providers:** ``` GET /teams/:teamId/providers → { "configs": [...] } POST /teams/:teamId/providers ← { "name", "provider", "endpoint", "api_key", ... } PUT /teams/:teamId/providers/:id DELETE /teams/:teamId/providers/:id GET /teams/:teamId/providers/:id/models → { "models": [...] } ``` **Team Models:** ``` GET /teams/:teamId/models → { "models": [...] } ``` Available models for this team (global + team-scoped). **Team Personas:** See §4.2. **Team Roles:** ``` GET /teams/:teamId/roles → { "roles": [...] } PUT /teams/:teamId/roles/:role ← { "permissions": {...} } DELETE /teams/:teamId/roles/:role ``` **Team Audit:** ``` GET /teams/:teamId/audit → paginated audit log GET /teams/:teamId/audit/actions → { "actions": [...] } (distinct action types) ``` **Team Usage:** ``` GET /teams/:teamId/usage → { "totals": {...}, "results": [...] } ``` ### 15.3 Groups & Resource Grants Groups are ACL containers that decouple access from team membership. A resource grant controls who can see a Persona or KB beyond its base scope. **My Groups:** ``` GET /groups/mine → { "groups": [...] } ``` **Admin Group CRUD:** ``` GET /admin/groups → { "groups": [...] } POST /admin/groups ← { "name", "scope", "team_id" } GET /admin/groups/:id PUT /admin/groups/:id DELETE /admin/groups/:id ``` **Group Members:** ``` GET /admin/groups/:id/members → { "members": [...] } POST /admin/groups/:id/members ← { "user_id" } DELETE /admin/groups/:id/members/:userId ``` **Resource Grants:** ``` GET /admin/grants/:type/:id → { "grant": {...} } PUT /admin/grants/:type/:id ← { "grant_type": "team_only|global|groups", "group_ids": [...] } DELETE /admin/grants/:type/:id ``` `:type` is `persona` or `kb`. `:id` is the resource ID. Grant types: | `grant_type` | Visibility | |--------------|-----------| | `team_only` | Only the owning team | | `global` | All authenticated users | | `groups` | Members of specified groups | --- ## 16. Platform Administration Cross-cutting admin operations that don't belong to a specific domain. ### 16.1 User Management ``` GET /admin/users → { "users": [...] } POST /admin/users ← { "username", "password", "email", "role" } PUT /admin/users/:id/role ← { "role": "admin|user" } PUT /admin/users/:id/active ← { "active": false } DELETE /admin/users/:id POST /admin/users/:id/reset-password ← { "new_password": "..." } POST /admin/users/:id/vault/reset → destroys user's UEK (BYOK keys lost) ``` ### 16.2 Global Settings & Policies Settings are key-value pairs in `global_settings`. Policies are boolean flags that gate features. ``` GET /admin/settings → all settings GET /admin/settings/:key → single setting PUT /admin/settings/:key ← { "value": ... } ``` The handler auto-detects: if the value is a string and the key is a known policy name, it writes to the `policies` table. Otherwise it writes to `global_config` as JSON. **Public settings** (non-admin, for FE bootstrapping): ``` GET /settings/public ``` ```json { "banner": { "enabled": true, "text": "DEVELOPMENT", "bg": "#007a33", "fg": "#ffffff", "position": "both" }, "branding": { "instance_name": "Switchboard", "logo_url": "...", "tagline": "..." }, "has_admin_prompt": true, "storage_configured": true, "paste_to_file_chars": 2000, "policies": { "allow_registration": "true", "allow_user_byok": "true", "allow_user_personas": "true" } } ``` ### 16.3 Banner Configuration The environment banner is stored as a single `global_config` entry under the key `"banner"`. It's a simple object: ```json { "enabled": true, "text": "DEVELOPMENT", "position": "both|top|bottom", "bg": "#007a33", "fg": "#ffffff" } ``` Set via `PUT /admin/settings/banner` with `{ "value": { ... } }`. The admin UI provides a color picker, position selector, and preset dropdown. The Go template base layout reads the banner from `PageData` and renders top/bottom strips with CSS custom properties. ### 16.4 Platform Stats ``` GET /admin/stats ``` ```json { "users": 42, "channels": 156, "messages": 12847 } ``` ### 16.5 Storage & Vault **Storage status:** ``` GET /admin/storage/status → { "backend", "healthy", "file_count", "total_bytes", ... } GET /admin/storage/orphans → { "count": 3 } POST /admin/storage/cleanup → removes orphaned blobs GET /admin/storage/extraction → extraction queue status ``` **Vault:** ``` GET /admin/vault/status → { "encryption_key_set", "user_vaults_count", ... } ``` ### 16.6 Audit Log ``` GET /admin/audit?page=1&per_page=50&action=user.create&actor_id=uuid GET /admin/audit/actions → { "actions": ["user.create", "policy.update", ...] } ``` Audit entry: ```json { "id": "uuid", "actor_id": "uuid", "action": "user.create", "resource_type": "user", "resource_id": "uuid", "metadata": {}, "created_at": "..." } ``` ### 16.7 Usage & Pricing ``` GET /admin/usage → { "totals", "results" } GET /admin/usage/teams/:id → team-specific usage GET /admin/usage/users/:id → user-specific usage GET /admin/pricing → { "data": [...] } PUT /admin/pricing ← { "provider", "model", "input_per_m", "output_per_m" } DELETE /admin/pricing/:provider/:model ``` **Personal usage** (non-admin): ``` GET /usage → { "totals", "results" } ``` Scoped to the user's BYOK provider usage only. ### 16.8 Roles (Platform) ``` GET /admin/roles → { "roles": [...] } GET /admin/roles/:role PUT /admin/roles/:role ← { "permissions": {...} } POST /admin/roles/:role/test ← test role permissions ``` ### 16.9 Email Test ``` POST /admin/notifications/test-email ``` ```json { "to": "admin@example.com" } ``` Sends a test email using the configured SMTP settings. --- ## 17. Export & Utility ### 17.1 Health Check ``` GET /health → { "status": "ok" } ``` Public, no auth. Also available at `/api/v1/health`. ### 17.2 Export Convert markdown to PDF or DOCX via pandoc: ``` POST /export ``` ```json { "content": "# My Document\n\nBody text...", "format": "pdf|docx", "filename": "my-document" } ``` Returns the binary file with appropriate Content-Type. Requires `pandoc` in the container (available in the unified and backend Docker images). --- ## 18. WebSocket Protocol Real-time bidirectional event bus. Single connection per client. ### 18.1 Connection ``` ws://{host}{BASE_PATH}/ws?token={access_token} ``` **Lifecycle:** - Server sends `ping` every 54 seconds - Client must respond with `pong` within 60 seconds or disconnection - Max message size: 4 KB - Write deadline: 10 seconds ### 18.2 Event Envelope All messages are JSON: ```json { "type": "event.type", "payload": { ... } } ``` ### 18.3 Room Subscription Clients subscribe to rooms for scoped event delivery: ```json { "type": "subscribe", "payload": { "room": "channel:uuid" } } { "type": "unsubscribe", "payload": { "room": "channel:uuid" } } ``` ### 18.4 Event Routing Table | Prefix | Direction | Description | |--------|-----------|-------------| | `channel.created` | → client | New channel created | | `channel.updated` | → client | Channel metadata changed | | `channel.deleted` | → client | Channel removed | | `message.created` | → client | New message in subscribed channel | | `message.updated` | → client | Message content changed | | `message.deleted` | → client | Message removed | | `cursor.updated` | → client | Branch cursor moved | | `typing.start` | ↔ both | User/AI started typing | | `typing.stop` | ↔ both | Typing ended | | `model.changed` | → client | Channel model roster changed | | `persona.updated` | → client | Persona metadata changed | | `kb.updated` | → client | Knowledge base changed | | `kb.document.status` | → client | Document processing status update | | `note.created` | → client | Note created | | `note.updated` | → client | Note content changed | | `note.deleted` | → client | Note removed | | `notification.new` | → client | New notification | | `notification.read` | → client | Notification marked read | | `memory.extracted` | → client | New memory extracted | | `memory.status` | → client | Memory approved/rejected | | `role.fallback` | → client | Role fallback triggered | | `workspace.updated` | → client | Workspace state changed | | `project.updated` | → client | Project metadata changed | | `extension.updated` | → client | Extension config changed | | `settings.updated` | → client | Global settings changed | | `user.updated` | → client | User profile changed | Direction: `→ client` = server-to-client only, `← client` = client-to-server only, `↔ both` = bidirectional. --- ## 19. Appendix: Enums ### Channel Types `direct` (current default), `group` (v0.23.0), `workflow` (v0.25.0) ### Message Roles `user`, `assistant`, `system`, `tool` ### Scopes `personal`, `team`, `global` ### Visibility (model catalog) `enabled`, `disabled`, `team` ### User Roles `admin`, `user` ### Team Member Roles `admin`, `member` ### Provider Types `openai`, `anthropic`, `openrouter`, `venice` (extensible via registry) ### Model Types `chat`, `embedding`, `image` ### Memory Scopes `user`, `persona` ### Memory Statuses `pending`, `approved`, `rejected` ### Workspace Owner Types `user`, `project` ### Workspace Statuses `active`, `archived` ### Index Statuses `pending`, `indexing`, `ready`, `error` ### KB Document Statuses `pending`, `chunking`, `embedding`, `ready`, `error` ### Provider Health Statuses `healthy`, `degraded`, `down` ### Routing Policy Types `provider_prefer`, `team_route`, `cost_limit`, `model_alias` ### Extension Tiers `browser` (implemented), `starlark` (future), `sidecar` (future) ### Grant Types `team_only`, `global`, `groups` ### Resource Grant Scopes `persona`, `kb` ### Notification Types `role.fallback`, `memory.extracted`, `system.announcement`, plus extensible via notification service. ### Git Auth Types `https_pat`, `https_basic`, `ssh_key` ### Policies | Key | Default | Description | |-----|---------|-------------| | `allow_registration` | `"true"` | Allow new user self-registration | | `allow_user_byok` | `"true"` | Allow users to add personal API keys | | `allow_user_personas` | `"true"` | Allow users to create personal Personas | | `allow_team_providers` | `"true"` | Allow team admins to configure team providers | | `kb_direct_access` | `"true"` | Show KB picker in channel (false = strict enterprise mode) | | `require_email` | `"false"` | Require email on registration | | `default_system_prompt` | `""` | Injected into all conversations (admin override) | --- ## Page Routes (Non-API) Server-rendered Go template surfaces. Not REST endpoints — these return HTML pages. | Route | Surface | Description | |-------|---------|-------------| | `/login` | Login | Standalone login/register page | | `/` | Chat | Main chat interface | | `/chat/:chatID` | Chat | Chat with specific channel loaded | | `/editor` | Editor | Workspace file editor | | `/editor/:wsId` | Editor | Editor with specific workspace | | `/notes` | Notes | Notes interface | | `/notes/:noteId` | Notes | Notes with specific note loaded | | `/admin` | Admin | Platform administration | | `/admin/:section` | Admin | Admin with specific section | | `/settings` | Settings | User settings | | `/settings/:section` | Settings | Settings with specific section | All page routes (except `/login`) require authentication via `AuthOrRedirect` middleware (reads `sb_token` cookie). Admin routes additionally require `RequireAdminPage()`. **Dynamic surface routing** for admin-installed extension surfaces is not yet implemented. The current five surfaces are hardcoded in Go route registration. Future: extension manifests declare surface routes, the page engine dynamically registers them with appropriate data loaders. See EXTENSIONS.md for the design direction.