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