# 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: ```json { "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](STARLARK-REFERENCE) for query API. ```json "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. ```json "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. ```json { "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. ```json { "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](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`: ```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`): ```javascript 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`