Co-authored-by: Jeffrey Smith <jasafpro@gmail.com> Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
9.4 KiB
Extension Guide
Package Types
| Type | Description |
|---|---|
surface |
A routable UI page rendered in the shell viewport |
extension |
Starlark hooks, tools, API routes, DB tables |
full |
Both surface and extension combined |
library |
Shared code imported by other packages via lib.require() |
workflow |
Bundled workflow definition |
Tiers
| Tier | Runs | Capabilities |
|---|---|---|
browser |
Client-side JS only | DOM access, SDK hooks, no server-side logic |
starlark |
Sandboxed server-side | DB, HTTP, notifications, secrets, API routes, realtime |
sidecar |
Separate container | Full runtime (future) |
manifest.json
Every package has a manifest.json at its root. Example for a surface:
{
"id": "my-surface",
"title": "My Surface",
"type": "full",
"tier": "starlark",
"version": "1.0.0",
"description": "A custom surface with server-side logic.",
"icon": "🔧",
"author": "you",
"route": "/s/my-surface",
"auth": "authenticated",
"permissions": ["db.write"],
"api_routes": [
{"method": "GET", "path": "/items"},
{"method": "POST", "path": "/items"}
],
"db_tables": {
"items": {
"columns": {
"title": "text",
"done": "int"
},
"indexes": [["title"]]
}
},
"settings": {
"page_size": {
"type": "string",
"label": "Items Per Page",
"description": "Number of items shown per page",
"default": "25"
}
}
}
Field Reference
| Field | Required | Description |
|---|---|---|
id |
yes | Unique kebab-case identifier |
title |
yes | Display name |
type |
yes | surface, extension, full, library, workflow |
tier |
yes | browser, starlark, sidecar |
version |
yes | Semver string |
description |
no | Short description |
icon |
no | Emoji icon for sidebar/menu |
route |
surfaces | URL path (e.g., /s/my-surface) |
auth |
no | authenticated (default) or public |
permissions |
no | Sandbox capabilities: db.write, db.read, api.http, notifications, secrets, realtime.publish, connections.read, workflow.access, batch.exec, files.read, files.write, workspace.manage |
api_routes |
no | Array of {method, path} for extension HTTP endpoints |
api_schema |
no | OpenAPI documentation for extension API routes (see below) |
db_tables |
no | Table definitions (see below) |
settings |
no | User-configurable settings schema |
exports |
libraries | Functions exported for other packages |
hooks |
no | Event bus subscriptions |
config_section |
no | Settings/Admin panel injection (see below) |
capabilities |
no | Environment requirements (see below) |
user_permissions |
no | Permissions this extension registers for users (see below) |
gate_permission |
no | Permission checked before on_request executes |
schema_version |
no | Integer for additive schema migrations |
db_tables Schema
Tables are automatically namespaced as ext_{package_id}_{table_name}.
Every table gets an auto-generated id primary key and created_at timestamp.
Column types: text, int, vector(N).
The vector(N) type stores N-dimensional float vectors for similarity
search via db.query_similar(). Storage adapts to the backend:
Postgres + pgvector uses native vector(N) with HNSW indexes,
Postgres without pgvector uses JSONB, SQLite uses TEXT. N can be
1–4096. See the Starlark Reference for query API.
"db_tables": {
"notes": {
"columns": {
"title": "text",
"body": "text",
"creator_id": "text",
"pinned": "int"
},
"indexes": [
["creator_id"],
["pinned"]
]
}
}
api_schema (OpenAPI Documentation)
Extensions can optionally declare an api_schema array in their manifest to provide rich API documentation. Declared routes appear in Swagger UI at /api/docs with parameters, request bodies, and response schemas. Routes without api_schema entries still appear as auto-generated stubs.
"api_schema": [
{
"path": "/items",
"method": "GET",
"summary": "List items",
"description": "Returns paginated items for the current user",
"params": {
"limit": {"type": "integer", "default": 50, "description": "Max results"},
"offset": {"type": "integer", "default": 0}
},
"response": {
"type": "object",
"example": {"data": [{"id": "string", "title": "string"}]}
}
},
{
"path": "/items",
"method": "POST",
"summary": "Create item",
"body": {
"title": {"type": "string", "required": true},
"description": {"type": "string"}
}
}
]
Only path and method are required. All other fields are optional. Malformed entries are logged and skipped without blocking extension loading.
Capabilities
Extensions can declare environment requirements via the capabilities
manifest field. The kernel validates these at install time.
{
"capabilities": {
"required": ["postgres"],
"optional": ["pgvector", "workspace"]
}
}
- required — install is rejected (HTTP 422) if any capability is missing.
- optional — install succeeds with a logged warning. Query at runtime
with
settings.has_capability("pgvector")to adapt behavior.
Detected capabilities: pgvector, workspace, object_storage, s3,
postgres. The admin can view detected capabilities at
Admin > System > Capabilities or via GET /admin/capabilities.
User Permissions
Extensions can register custom permissions that the admin assigns to user groups. This controls access to extension features beyond the sandbox permission model.
{
"user_permissions": ["image-gen.use", "image-gen.admin"],
"gate_permission": "image-gen.use"
}
- user_permissions — on install, these are merged into the kernel's permission registry. On uninstall, they are removed. The admin assigns them to groups in Admin > Groups.
- gate_permission — if set, the kernel checks this permission before
calling
on_request. Unauthorized users get a 403 without the extension code executing.
In Starlark, check permissions inline via req["permissions"] or call
permissions.check(user_id, "image-gen.use").
Starlark Sandbox API
Starlark scripts run server-side with a 1M operation budget and no filesystem access. See the Starlark Reference for the complete module catalog, function signatures, and permission gates.
config_section — Settings Panel Injection
Packages can inject configuration panels into the Settings, Admin, or
Team Admin surfaces. Declare config_section in manifest.json:
{
"config_section": {
"label": "My Config",
"icon": "M12 2L2 7l10 5 10-5-10-5z",
"component": "js/config.js",
"surfaces": ["settings", "admin"],
"category": "system"
}
}
| Field | Required | Description |
|---|---|---|
label |
yes | Navigation label shown in the sidebar or tab list |
icon |
no | SVG path data for the nav icon |
component |
no | JS asset path (default: js/config.js). Must export default a Preact component. |
surfaces |
yes | Target surfaces: "settings", "admin", "team-admin" |
category |
no | Admin surface category tab (default: "system"). Ignored for settings/team-admin. |
How it works:
- At page load, the backend scans all enabled packages for
config_sectionentries targeting the current surface. - Matching sections are injected into the page as
__CONFIG_SECTIONS__. - The frontend dynamically imports the component module and renders it as an additional tab/section.
- The component receives
{ packageId, teamId }as props.
Example component (js/config.js):
const { html } = window;
const { useState, useEffect } = hooks;
export default function MyConfig({ packageId }) {
const [val, setVal] = useState('');
useEffect(() => {
sw.api.ext(packageId).get('/settings').then(r => setVal(r.value));
}, []);
return html`<div>
<label>API Key</label>
<input value=${val} onInput=${e => setVal(e.target.value)} />
<button onClick=${() => sw.api.ext(packageId).put('/settings', { value: val })}>Save</button>
</div>`;
}
Permissions Model
Extensions declare required permissions in manifest.json. The admin
must grant each permission before the extension can use the corresponding
sandbox module. Permission status is visible in Admin > Packages > Permissions.
See Permissions & Groups for the full RBAC model, user permission slugs, and settings cascade.
File Structure
my-package/
manifest.json # required
js/ # browser-side JavaScript
index.js
css/ # stylesheets
styles.css
script.star # Starlark entry point
star/ # additional Starlark modules
assets/ # static assets
migrations/ # schema migration files
Testing Extensions
- Build the package:
cd packages && bash build.sh my-package - Upload the
.pkgfile via Admin > Packages > Install. - Grant permissions in Admin > Packages > Permissions.
- Enable the package.
- If it is a surface, navigate to its route (e.g.,
/s/my-package).
Extension API routes are accessible at /s/{slug}/api/{path} and require an authenticated Bearer token.