Menu

Admin extensions

An admin extension adds screens for your site's content to the MicroHost admin UI (for example a table of posts with create, edit, publish and delete). You describe the screens declaratively; the data comes from private handlers in your own release. No HTML, CSS or script of yours runs in the admin UI: it renders a closed set of native elements. The full contract is in docs/design/admin-extensions-spec.md; the validator is crates/core/src/admin_extensions.rs. The working example is examples/blog.

Status. Implemented: validation, descriptor hashing, the enable flow, the signed private bridge and the admin UI pages. An extension declaration never grants capabilities; the private handlers use the same grants as the rest of the site. Declarations that fail validation are dropped with a warning instead of failing the deploy.

Pieces

Piece What it is
Declaration The admin block of the manifest: extensions with queries, actions and pages. Written in microhost.json, or with defineExtension in microhost.config.js.
Private route An exact path under /_admin/ that you declare as an ordinary manifest route and point to a handler script. The public router never serves /_admin.
Private handler A script (convention: api/_admin/*.js) that verifies the call with verifyAdminRequest and answers with JSON.
Enablement An administrator turns the extension on for the site, bound to the descriptor's hash.

Declaration reference

defineExtension({
  id: "blog", label: "Blog", icon: "book-open",
  queries: [{ id: "list", route: "/_admin/posts", input: {...}, output: {...} }],
  actions: [{ id: "publish", route: "/_admin/posts", input: {...} }],
  pages:   [{ id: "posts", label: "Posts", elements: [...] }],
})

Extension. id ([a-z][a-z0-9_-]{0,39}), label (at most 80 characters), optional icon, 1 to 8 pages, and queries plus actions together at most 32. At most 4 extensions per site. Icons come from a fixed list: file-text, list, plus, pencil, trash-2, check, x, book-open, calendar, tag.

Request (query or action). Fields: id, method, route, input, output, minRole, timeoutMs, write.

Page. id, label (at most 80), optional icon, routeParams (at most 4 ids; they become URL parts of the page), elements (at most 24 per page including nested tab content).

Elements (closed set; unknown properties invalidate the extension):

Type Properties
heading text, level (2 or 3), optional icon
text text (at most 2,000 characters; plain text)
empty-state text
key-value query, input, items: [{ label, key }] (at most 16); the query must return an object
table query (a rows query), input, columns (at most 12), rowActions (at most 6), pagination, sort: { key, direction }
form fields (at most 16), optional prefillQuery (an object query), input, submit
button action (an action reference)
tabs tabs: [{ id, label, elements }] (at most 8)

A complete real declaration is examples/blog/microhost.config.js with its page layouts in examples/blog/admin-pages.js.

The private handler

The control plane calls your handler through the private route with a signed request:

export default async function handle(request, ctx) {
  const admin = verifyAdminRequest(request, ctx);   // throws AdminRequestError on any failure
  // admin: { userId, role, workspace, site, extension, name, operation, issuedAt }
  const input = await request.json();               // the validated input object (also for GET)
  if (admin.operation === "list") { /* ... */ }
  if (admin.operation === "publish") {
    if (admin.role === "viewer") return Response.json({ error: "forbidden" }, { status: 403 });
    /* ... */
  }
  return Response.json({ ok: true });
}

Enabling and descriptor hashes

An extension is off until an administrator enables it for the site. In the UI that is Website, Extensions; the API is PUT /admin/api/v1/sites/{id}/extensions/{ext} with { "enabled": true, "descriptorHash": "<hash>" } (needs developer-level code access). The descriptor hash is the SHA-256 of the canonical validated declaration.

Validation warnings you may see (stable codes, no free text): admin_invalid_schema, admin_schema_unsupported, admin_unknown_field, admin_limit_exceeded, admin_duplicate_id, admin_invalid_route, admin_invalid_reference, admin_invalid_element. A warning means that extension was dropped; the deploy still succeeded.