This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/docs/EXTENSION-GUIDE.md
Jeffrey Smith 68713bf539
All checks were successful
CI/CD / detect-changes (pull_request) Successful in 5s
CI/CD / test-runners (pull_request) Has been skipped
CI/CD / e2e-smoke (pull_request) Has been skipped
CI/CD / test-frontend (pull_request) Successful in 6s
CI/CD / test-go-pg (pull_request) Successful in 2m42s
CI/CD / test-sqlite (pull_request) Successful in 2m58s
CI/CD / build-and-deploy (pull_request) Successful in 1m33s
v0.8.5: Extension composability — slots, contributes, lib.require relaxation
Manifest declarations for slots (host surfaces declare injection points)
and contributes (extensions declare UI contributions). lib.require()
relaxed from library-only to any package with exports. Admin slots
aggregation endpoint. SDK renderAll() helper. 7 new tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 11:14:04 +00:00

12 KiB
Raw Blame History

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 no Functions exported for cross-package calls via lib.require()
depends no Array of package IDs this package depends on
slots no Named UI injection points for host surfaces
contributes no Slot contributions into other surfaces
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 14096. 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").

Extension Composability

Extensions compose with each other through three mechanisms: manifest-declared slots (UI injection points), contributions (UI components injected into those slots), and cross-package function calls via lib.require().

Slots — Host Surfaces Declare Injection Points

A surface declares named slots in its manifest where other extensions can inject UI components:

{
  "id": "notes",
  "type": "surface",
  "slots": {
    "toolbar-actions": {
      "description": "Toolbar action buttons",
      "context": {
        "noteId": "string",
        "getContent": "function — returns note body",
        "setContent": "function — replaces note body"
      }
    }
  }
}

The surface renders slot contents using the SDK helper:

html`<div class="toolbar">
    ${sw.slots.renderAll('notes:toolbar-actions', {
        noteId: note.id,
        getContent: () => editor.getValue(),
        setContent: (text) => editor.setValue(text),
    })}
</div>`

Contributions — Extensions Inject UI

An extension declares which slots it contributes to:

{
  "id": "note-dictate",
  "type": "extension",
  "contributes": {
    "notes:toolbar-actions": {
      "label": "Dictate",
      "icon": "🎤",
      "description": "Voice-to-text dictation"
    }
  }
}

In its JavaScript, it registers the component:

sw.slots.register('notes:toolbar-actions', {
    id: 'note-dictate',
    priority: 200,
    component: ({ noteId, setContent, getContent }) => {
        // ... component implementation
        return html`<button class="sw-btn sw-btn--ghost">🎤</button>`;
    }
});

Contributions are soft-coupled — install order doesn't matter. The admin can view all slots and contributors at Admin > Packages or via GET /api/v1/admin/slots.

Cross-Package Function Calls

Any package that declares exports in its manifest can be called by other packages via lib.require(). The caller declares the dependency:

{
  "id": "note-ai",
  "depends": ["llm-bridge"],
  "permissions": ["api.http"]
}

In Starlark:

llm = lib.require("llm-bridge")
result = llm.complete([{"role": "user", "content": prompt}])

The called function runs with the target package's permissions, not the caller's. This is the same security model as library packages.

Slot Naming Convention

Slot names follow {host-package-id}:{slot-name}. The colon separates the namespace from the slot. Standard slots for first-party packages:

Slot Host Use Case
notes:toolbar-actions notes Dictation, AI tools, formatting
notes:note-footer notes Related items, AI summary
chat:composer-tools chat Image gen, file attach
chat:message-actions chat Reactions, translate, bookmark
chat:image-actions chat Regen, edit, upscale

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:

  1. At page load, the backend scans all enabled packages for config_section entries targeting the current surface.
  2. Matching sections are injected into the page as __CONFIG_SECTIONS__.
  3. The frontend dynamically imports the component module and renders it as an additional tab/section.
  4. 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

  1. Build the package: cd packages && bash build.sh my-package
  2. Upload the .pkg file via Admin > Packages > Install.
  3. Grant permissions in Admin > Packages > Permissions.
  4. Enable the package.
  5. 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.