Co-authored-by: Jeffrey Smith <jasafpro@gmail.com> Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
12 KiB
Extension Surfaces — Authoring Guide
Version: v0.30.2
Audience: Developers building custom surfaces for Chat Switchboard
Prerequisite: Admin access to install surfaces
See also: PACKAGES.md for the .pkg archive format, SDK.md for the sw.* API
What Is an Extension Surface?
An extension surface is a custom full-page UI that runs inside the
Chat Switchboard shell. It gets its own URL (/s/your-surface), a
link in the sidebar, and full access to the platform's authenticated
API, theme system, and UI components.
Examples of what you can build:
- A monitoring dashboard that polls external APIs
- A custom form builder for workflow intake
- A kanban board backed by channel data
- A cost tracking view using usage data
- A project timeline visualization
Extension surfaces are installed as .surface archives via the admin
panel. No code changes to the platform are required.
Quick Start
1. Create the Archive
A .surface file is a zip archive with this structure:
my-dashboard/
├── manifest.json ← required: surface metadata
├── js/
│ └── main.js ← required: entry point script
└── css/
└── main.css ← optional: styles
The top-level directory name must match the id in manifest.json.
2. Write the Manifest
{
"id": "my-dashboard",
"title": "My Dashboard",
"route": "/s/my-dashboard",
"auth": "authenticated",
"layout": "single",
"version": "1.0.0",
"description": "A custom dashboard surface."
}
| Field | Required | Description |
|---|---|---|
id |
yes | Unique identifier. Lowercase, alphanumeric + hyphens. Must match the archive's top-level directory name. |
title |
yes | Human-readable name shown in the sidebar nav. |
route |
no | URL path. Defaults to /s/{id} if omitted. Must start with /s/. |
auth |
no | Auth requirement. Only "authenticated" is supported currently. Default: "authenticated". |
layout |
no | Layout preset. Only "single" is supported currently. Default: "single". |
version |
no | Semver string for your own tracking. Not enforced. |
description |
no | Shown in the admin surfaces panel. |
3. Write the Entry Point
js/main.js runs after all platform scripts have loaded. Your code
mounts into the #extension-mount container:
(function() {
'use strict';
var mount = document.getElementById('extension-mount');
if (!mount) return;
var manifest = window.__MANIFEST__ || {};
var user = window.__USER__ || {};
mount.innerHTML = '<h1>Hello, ' + esc(user.display_name || user.username) + '!</h1>';
function esc(s) {
var el = document.createElement('span');
el.textContent = s;
return el.innerHTML;
}
})();
4. Package and Install
zip -r my-dashboard.surface my-dashboard/
Then in the admin panel: Admin → Surfaces → Upload and select the
.surface file. The surface is immediately available — no restart
required. A link appears in the sidebar.
Platform Globals
Your main.js runs in the full platform context. These globals are
available when your script executes:
Window Injections
| Global | Type | Description |
|---|---|---|
window.__MANIFEST__ |
Object | Your surface's manifest with route, layout, etc. Keys are lowercase (JSON serialization from Go). Access as manifest.id, manifest.route, manifest.title. |
window.__USER__ |
Object | Authenticated user: { username, display_name, role, id, email }. |
window.__BASE__ |
String | URL base path (e.g. "/dev" or ""). Prepend to all internal links. |
window.__VERSION__ |
String | Platform version string. |
window.__SURFACE__ |
String | Your surface ID. |
API Module
API provides authenticated fetch wrappers. Auth headers and token
refresh are handled automatically.
// GET request — returns parsed JSON
var data = await API._get('/api/v1/channels');
// POST request — body is JSON-serialized automatically
var result = await API._post('/api/v1/channels', {
name: 'my-channel',
type: 'channel'
});
// PUT request
await API._put('/api/v1/channels/' + id, { name: 'renamed' });
All methods return a Promise that resolves to parsed JSON. On 401, tokens are refreshed automatically and the request is retried.
Important: The methods are _get, _post, _put — prefixed with
underscore. There is no API.get().
UI Module
UI provides platform UI primitives:
// Toast notification — types: 'success', 'error', 'info', 'warning'
UI.toast('Operation completed', 'success');
UI.toast('Something went wrong', 'error');
// Navigate to settings
UI.openSettings('general');
Theme Module
Theme manages dark/light mode:
// Current resolved theme: 'dark' or 'light'
var mode = Theme.resolved();
// Check via DOM attribute
var isDark = document.documentElement.getAttribute('data-theme') === 'dark';
Your CSS should use CSS custom properties (see below) rather than querying Theme directly. Properties update automatically on theme change.
Events Module
Events is the platform event bus (WebSocket-backed):
// Subscribe to events
Events.on('chat.message.created', function(data) {
console.log('New message:', data);
});
// Emit local-only events
Events.emit('my-surface.updated', { key: 'value' });
ChatPane Component
If your surface needs an embedded chat, you can mount a ChatPane:
var container = document.createElement('div');
container.style.height = '400px';
mount.appendChild(container);
ChatPane.create(container, { role: 'assist' });
CSS Custom Properties
Your CSS should use the platform's CSS custom properties for consistent theming. These update automatically when the user switches themes.
Colors
| Property | Dark Default | Light Default | Usage |
|---|---|---|---|
--bg |
#0e0e10 |
#f7f7fa |
Page background |
--bg-surface |
#18181b |
#ffffff |
Card/panel background |
--bg-raised |
#222227 |
#eff0f3 |
Elevated element background |
--bg-hover |
#2a2a30 |
#e5e6eb |
Hover state background |
--border |
#2e2e35 |
#d5d6dc |
Default borders |
--border-light |
#3a3a42 |
#c2c3cb |
Subtle borders |
--text |
#e8e8ed |
#1a1a2e |
Primary text |
--text-2 |
#9898a8 |
— | Secondary text |
--text-3 |
#6b6b7b |
— | Tertiary/muted text |
--accent |
#6c9fff |
— | Accent color (links, highlights) |
--accent-hover |
#84b0ff |
— | Accent hover state |
--accent-dim |
rgba(108,159,255,0.12) |
— | Accent tinted background |
--danger |
#ef4444 |
— | Error/destructive actions |
--success |
#22c55e |
— | Success indicators |
--warning |
#eab308 |
— | Warning indicators |
Layout
| Property | Default | Usage |
|---|---|---|
--radius |
8px |
Default border radius |
--radius-lg |
12px |
Large border radius |
--font |
'DM Sans', ... |
Primary font stack |
--mono |
'JetBrains Mono', ... |
Monospace font stack |
--transition |
180ms ease |
Default transition timing |
Example
.my-card {
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: 16px;
color: var(--text);
}
.my-card-subtitle {
color: var(--text-2);
font-size: 13px;
}
.my-card:hover {
background: var(--bg-hover);
}
Do not use --bg-secondary, --text-secondary, or
--text-tertiary — these do not exist. Use --bg-surface, --text-2,
and --text-3 respectively.
DOM Structure
When your surface loads, the page DOM looks like this:
<body data-surface="my-dashboard">
<div class="surface">
<div class="surface-inner">
<div id="extension-surface" class="extension-surface">
<div class="user-menu-wrap"> ... </div> ← platform nav
<div id="extension-mount" class="extension-mount">
← YOUR CONTENT GOES HERE
</div>
</div>
</div>
</div>
<script>window.__MANIFEST__ = {...};</script>
<script>window.__USER__ = {...};</script>
<script src="/js/api.js"></script>
<script src="/js/ui-core.js"></script>
... platform scripts ...
<script src="/surfaces/my-dashboard/js/main.js"></script> ← YOUR SCRIPT
</body>
The #extension-mount div uses flex: 1 and overflow: auto, so it
fills all available vertical space. You can set its content to anything.
Admin API Reference
Surfaces are managed via the admin API. All endpoints require admin authentication.
Install a Surface
POST /api/v1/admin/surfaces/install
Content-Type: multipart/form-data
file: <.surface or .zip file>
The archive must contain a manifest.json with id and title.
Static assets (js/, css/, assets/) are extracted to the server's
storage directory. The surface is registered in the database and
enabled immediately.
Response:
{
"id": "my-dashboard",
"title": "My Dashboard",
"source": "extension",
"enabled": true
}
List All Surfaces
GET /api/v1/admin/surfaces
Returns all surfaces (core + extension) with their enabled state.
Enable / Disable
PUT /api/v1/admin/surfaces/:id/enable
PUT /api/v1/admin/surfaces/:id/disable
Disabled surfaces redirect to the chat surface. The sidebar link is
hidden. Core surfaces (chat, admin) cannot be disabled.
Uninstall
DELETE /api/v1/admin/surfaces/:id
Removes the surface registration and cleans up extracted static assets. Core surfaces cannot be deleted.
List Enabled (Non-Admin)
GET /api/v1/surfaces
Returns enabled surfaces with minimal info (id, title, route). Used by the sidebar to render nav links. Available to all authenticated users.
Platform CSS Classes
Your extension can use these CSS classes from the platform's
primitives.css without importing anything:
Buttons
<button class="btn-primary">Primary Action</button>
<button class="btn-secondary">Secondary</button>
<button class="btn-danger">Destructive</button>
<button class="btn-ghost">Ghost</button>
<button class="btn-small">Small</button>
Badges
<span class="badge">Default</span>
<span class="badge badge-success">Success</span>
<span class="badge badge-danger">Error</span>
<span class="badge badge-warning">Warning</span>
Toast (via JS)
UI.toast('Message here', 'success'); // green
UI.toast('Message here', 'error'); // red
UI.toast('Message here', 'info'); // blue
UI.toast('Message here', 'warning'); // yellow
Tips
Always wrap in an IIFE. Your script shares the global scope with
platform scripts. Use (function() { ... })(); to avoid collisions.
Use esc() for user-generated content. The platform does not
provide a global escaping function. Define your own:
function esc(s) {
var el = document.createElement('span');
el.textContent = s;
return el.innerHTML;
}
Prepend __BASE__ to internal links. The platform may be deployed
under a path prefix (e.g. /dev):
var base = window.__BASE__ || '';
link.href = base + '/settings/general';
Check for globals before using them. If your surface might be previewed outside the platform:
if (typeof API !== 'undefined' && API._get) {
// safe to make API calls
}
if (typeof UI !== 'undefined' && UI.toast) {
UI.toast('Works!', 'success');
}
Size limit: Archive upload is capped at 50 MB. Only js/, css/,
and assets/ directories are extracted from the archive.
Cache busting: Static assets are served with ?v={platform_version}
query parameter. When the platform is upgraded, caches are invalidated
automatically.
Full Example
See the included hello-dashboard.surface archive for a complete
working example that demonstrates:
- Manifest structure
- Mounting into
#extension-mount - Reading
__MANIFEST__and__USER__ - Using
UI.toast()for notifications - Using
API._get()for authenticated requests - Using CSS custom properties for theming
- Proper
esc()function for XSS safety