# 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 |
| `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 CPU budget and memory ceiling. Available modules (granted per-permission by admin):

| Module | Permission | API |
|--------|-----------|-----|
| `db` | `db.write` | `db.query(table, filters)`, `db.insert(table, row)`, `db.update(table, id, row)`, `db.delete(table, id)` |
| `http` | `http` | `http.get(url)`, `http.post(url, body)` -- SSRF-safe, no private IPs by default |
| `notifications` | `notifications` | `notifications.send(user_id, title, body)` |
| `secrets` | `secrets` | `secrets.get(connection_type)` -- reads from the credential vault |
| `api` | (implicit) | Registers HTTP routes at `/s/:slug/api/*path` |
| `realtime` | `realtime.publish` | `realtime.publish(channel, event, data)` -- push to WebSocket clients |

The sandbox cannot spawn goroutines, access the filesystem, or import arbitrary packages.

## Permissions Model

Extensions declare required permissions in `manifest.json`. The admin must grant each permission before the extension can use the corresponding module. Permission status is visible in Admin > Packages > Permissions.

Kernel permissions for users/groups: `extension.use`, `extension.install`, `workflow.create`, `workflow.submit`, `admin.view`, `token.unlimited`.

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