52 KiB
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
- Conventions
- Auth
- Channels & Conversations
- Personas
- Knowledge Bases
- Notes
- Workspaces & Git
- Projects
- Memory
- Providers & Routing
- Models & Preferences
- Notifications
- Extensions
- User Profile & Settings
- Teams & Access Control
- Platform Administration
- Export & Utility
- WebSocket Protocol
- 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 <meta> 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:
{ "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:
{
"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
{
"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
{ "username": "jdoe", "password": "..." }
Response:
{
"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
{ "refresh_token": "opaque-string" }
Returns new access_token and refresh_token. Old refresh token is
invalidated (rotation).
Logout
POST /auth/logout
{ "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:
{
"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
{
"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.
{
"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
{
"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
{ "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
{
"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
{
"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:
{
"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:
{
"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
{
"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:
{
"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
{
"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:
{
"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
{
"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] }:
{
"kb_id": "uuid",
"kb_name": "Product Docs",
"enabled": true,
"document_count": 12
}
Set (replace all):
PUT /channels/:id/knowledge-bases
{ "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
{ "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:
{
"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
{
"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
{ "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:
{
"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:
{
"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):
{
"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:
{
"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:
{
"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:
{
"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:
{
"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] }:
{
"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:
{
"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
{
"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 /modelsexists as an alias. The alias is unused by the frontend and will be removed pre-1.0.
Returns { "models": [UserModel objects] }:
{
"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:
{
"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" }
{
"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
{
"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
{
"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:
{
"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
{
"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:
{
"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
{ "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
{
"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
pingevery 54 seconds - Client must respond with
pongwithin 60 seconds or disconnection - Max message size: 4 KB
- Write deadline: 10 seconds
18.2 Event Envelope
All messages are JSON:
{
"type": "event.type",
"payload": { ... }
}
18.3 Room Subscription
Clients subscribe to rooms for scoped event delivery:
{ "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.