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