# Surfaces

Dynamic surface lifecycle. Core surfaces are registered at startup.
Extension surfaces are uploaded by admins and served from the
`surface_registry` table.

## User Endpoints

### List Enabled Surfaces

```
GET /surfaces
```

Returns enabled surfaces with minimal nav info (no `source` or `enabled`
fields — these are admin concerns).

```json
{
  "surfaces": [
    { "id": "chat", "title": "Chat", "route": "/" },
    { "id": "editor", "title": "Editor", "route": "/editor" },
    { "id": "hello-dashboard", "title": "Hello Dashboard", "route": "/s/hello-dashboard" }
  ]
}
```

**Auth:** Authenticated.

## Admin Endpoints

### List All Surfaces

```
GET /admin/surfaces
```

Returns all surfaces including disabled ones. Each entry is a full
[Surface Registry Object](#surface-registry-object).

```json
{
  "surfaces": [
    { "id": "chat", "title": "Chat", "manifest": {...}, "enabled": true, "source": "core", "installed_at": "...", "updated_at": "..." },
    { "id": "hello-dashboard", "title": "Hello Dashboard", "manifest": {...}, "enabled": false, "source": "extension", "installed_at": "...", "updated_at": "..." }
  ]
}
```

**Auth:** Platform admin.

### Get Surface

```
GET /admin/surfaces/:id
```

Returns full surface details including manifest.

**Response:** A single [Surface Registry Object](#surface-registry-object).

**Errors:**
- `404` — surface not found (or store error).

**Auth:** Platform admin.

### Install Surface

```
POST /admin/surfaces/install
Content-Type: multipart/form-data
```

Field: `file` — a `.surface` or `.zip` archive (zip containing
`manifest.json` + static assets). The manifest declares `id`, `title`,
`route`, required data loaders, scripts, and CSS.

**Size limit:** 50 MB.

**Manifest validation:** `id` and `title` are required.

**Errors:**
- `400` — no file, wrong extension, too large, invalid zip, missing or
  invalid manifest, missing `id`/`title`.
- `409` — surface ID conflicts with an existing core surface.

**Auth:** Platform admin.

### Enable / Disable

```
PUT /admin/surfaces/:id/enable
PUT /admin/surfaces/:id/disable
```

Disabled surfaces redirect to `/` and hide from navigation.

**Undisableable surfaces:** `chat` and `admin` cannot be disabled
(returns `400`).

**Errors:**
- `400` — attempting to disable `chat` or `admin`.
- `404` — surface not found.

**Auth:** Platform admin.

### Delete Surface

```
DELETE /admin/surfaces/:id
```

Removes the surface, its DB row, and its extracted static assets.
Core surfaces cannot be deleted.

**Errors:**
- `400` — attempting to delete a core surface.
- `404` — surface not found.

**Auth:** Platform admin.

**Defense-in-depth:** The store layer also enforces
`WHERE source = 'extension'` on delete, so even if the handler check
were bypassed, core surfaces are protected.

## Surface Registry Object

```json
{
  "id": "hello-dashboard",
  "title": "Hello Dashboard",
  "manifest": { "route": "/s/hello-dashboard", "scripts": [...], "css": [...] },
  "enabled": true,
  "source": "core|extension",
  "installed_at": "...",
  "updated_at": "..."
}
```

## Internal Store Methods

The `SurfaceRegistryStore` interface exposes methods beyond what the
HTTP API uses directly:

- `Seed(ctx, id, title, source, manifest)` — upserts a surface at
  startup. Does **not** overwrite the `enabled` flag (admin toggle
  survives restarts). Called by the page engine's `SeedSurfaces()`.
- `ListEnabled(ctx) → []string` — returns IDs of all enabled
  surfaces. Used by the page engine for nav rendering
  (`EnabledSurfaceIDs()`), not exposed via HTTP.

## Page Routes

Extension surfaces are served at `/s/:slug` via `RenderExtensionSurface()`.
The page engine does a runtime DB lookup — no server restart needed after install.

Static assets are served from `/surfaces/:id/` (nginx location block in
both unified and split deployment). In unified mode, the Go server
handles this route with path-traversal protection. In split deployment,
nginx serves directly from `/data/surfaces/`.

## Surface Archive Format

A `.surface` file is a **zip** archive containing:

```
manifest.json     — required (must have "id" and "title")
js/               — optional (extracted to /data/surfaces/{id}/js/)
css/              — optional (extracted to /data/surfaces/{id}/css/)
assets/           — optional (icons, images, etc.)
```

The archive may have a single top-level directory prefix (e.g.,
`my-surface/manifest.json`) — the installer strips it when locating
`manifest.json` and when extracting `js/`, `css/`, and `assets/`
directories. Files outside these directories (e.g., `script.js` at
root) are not extracted.

See [EXTENSION-SURFACES.md](../EXTENSION-SURFACES.md) for the full
authoring guide including manifest schema, platform API, and CSS
custom properties.
