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 a7e38bc72a
All checks were successful
CI/CD / detect-changes (push) Successful in 4s
CI/CD / test-runners (push) Has been skipped
CI/CD / test-frontend (push) Successful in 5s
CI/CD / test-go-pg (push) Successful in 2m51s
CI/CD / test-sqlite (push) Successful in 2m51s
CI/CD / build-and-deploy (push) Successful in 39s
Feat v0.7.4 docs surface work (#58)
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com>
Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
2026-04-02 14:46:54 +00:00

7.3 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 Capabilities requested: db.write, http, notifications, secrets, realtime.publish
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)
schema_version no Integer for additive schema migrations

db_tables Schema

Tables are automatically namespaced as ext_{package_id}_{table_name}. Column types: text, int. Every table gets an auto-generated id primary key and created_at timestamp.

"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.

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.