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
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com> Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
234 lines
7.3 KiB
Markdown
234 lines
7.3 KiB
Markdown
# 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 | 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.
|
|
|
|
```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.
|
|
|
|
## 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`<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](PERMISSIONS-AND-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.
|