Docs
/
what you can do

Hosting

<!-- GENERATED by ops/tools/skill-sync — do not edit by hand. Run pnpm skills:sync. -->

<!-- GENERATED by ops/tools/skill-sync — do not edit by hand. Run pnpm skills:sync. -->

Hosting — what you can do

/admin/d/hosting · pillar imperium · entitlement imperium.hosting

Build, publish and serve websites - pages, appearance, SEO, domains, DNS and edge rules.

Fetch this, never remember it

These change per install and per release. Call for them - a list written here would be a future lie.

WhatCallWhy
render modes (what a site CAN be)list_modesGET /index/modesgrows every release, and a tenant can add its own x- modes - any list written down is a future lie
one mode's exact data contractget_mode_schemathe fields a mode accepts are its own; guessing them produces a payload that validates as empty
theme presetsGET /api/hosting/theme-presetspresets and the custom-CSS capability key are read together, per install
this operator's sites and their statuslist_my_sitesGET /api/hosting/sitesnever assume a site exists, and never assume its status - draft and published behave differently
what this plan allows and how much is leftGET /api/hosting/accessreturns the live capability plus used/remaining, so a quota refusal can be predicted instead of hit

How to actually do things here

Written by hand, because the order and the traps are the part no file can derive.

Create a website and publish it

  1. GET /api/hosting/access first - it says whether this operator may host at all and how many sites are left.
  2. Choose the mode by CALLING, never from memory: list_modes for the catalog (each row carries a group: core, content, app, distribution, commerce), then get_mode_schema(mode) for the exact fields and a starter template.
  3. POST /api/hosting/sites with {id, appName, mode, data}. The id IS the subdomain. The site is created draft.
  4. Edit the home page with set_site_content ({appName, mode, data}); it updates the preview only.
  5. save_site persists. save_site({publish: true}), or POST /api/hosting/sites/:id/publish, takes it live.

Trap: Publish re-validates the site data AND every page before it will go live - a draft may hold work in progress, a published site may not. A failure comes back as 400 invalid_routes, which is about a PAGE, not the home page.

Add pages to a site

  1. list_pages to see what is there. Paths are absolute: /, /menu, /blog/:slug.
  2. add_page with a componentKey. The usual one is mode:<type> - a whole render mode used as a page, whose props ARE that mode's data payload, so call get_mode_schema(<type>) first.
  3. set_page_content to edit one, remove_page to drop one. All three write immediately.
  4. Set guard to public, auth or membership to decide who may see it.

Trap: [critical] The HOME page and every OTHER page are different lanes. Home is config.mode + config.data and needs save_site. Every other page is a route that persists on write. Sending routes or components to PUT /api/hosting/sites/:id answers 400 use_route_lane.

Restyle a site without touching its content

  1. get_appearance returns the current look AND the full contract - every field and its allowed values. Call it before writing.
  2. list_theme_presets names the whole-site looks a site can start from.
  3. set_appearance takes only what changes; pass null for a key to clear that key.
  4. save_site to persist - appearance is editor state until then.

Trap: Every non-colour field is a closed enum, so an invented value is refused rather than approximated. appearance: null CLEARS the whole block and lets the tenant default show through; undefined leaves it alone. Never write config.theme - it is legacy and operator-only.

Point a host at something the user already runs

  1. This is the target axis, not the mode axis. Open the site editor's Serves tab.
  2. Pick the kind: proxy (an origin they run elsewhere), bundle (a build they upload to this site), redirect (send visitors somewhere else), config (the default - an Imperium page).
  3. For proxy, POST /api/hosting/sites/:id/proxy-check first - it is an SSRF-safe reachability probe.
  4. Write it with PUT /api/hosting/sites/:id carrying {target: {...}}.

Trap: static and server targets, and a proxy marked trustedOrigin, are operator-only and answer 403 for a member. Deploying an uploaded build is deliberately TWO calls: upload into builds/<id>/, then set {target:{kind:"bundle", dir:"builds/<id>"}}.

Tabs

Every one is a real URL. A tab with its own section below carries a real job.

TabPathGroupWhat it is for
domains/admin/hosted-domains-Every domain connected to this account.
setup--Connect a domain the operator owns, and prove they own it.
sites--Which of your websites this domain serves - one row per hostname, and the one place a hostname ALIAS is set.
dns--The records this domain needs, and whether they are live yet.
danger--Disconnect this domain, or release the claim so somebody else can take it.
hosted-sites/admin/hosted-sites-Every website this operator has, with its status.
site-wizard/admin/site-wizard-The guided path from nothing to a first site.
manage--The mode's own records - posts, links, files, images, releases. Only present for a mode that has them.
content--The page this site serves at its root: its mode, and that mode's data payload.
pages--Every page that is not the home page.
static--Hand-written .html and .md files this site serves as real pages.
appearance--Preset, colours, type, corners, logo - the whole look of the site.
seo--Title, description, keywords and the social card image.
domains--The subdomain this site answers on, and any custom domains attached to it.
analytics--Who visited this site, and when.
target--What this host actually serves: an Imperium page, a proxied app, an uploaded bundle, or a redirect.
files--Upload a build for this site to serve.
connect--Point this host at something the operator already runs, and test that it answers.
mcp--Serve this site as a tool server that outside agents can call.
assets--Records and assets scoped to this site.
code--The whole site as one editable config - import and export it as a single file.
shield--Per-site edge rules - who may reach this site at all, and how often.
danger--Unpublish, rename and delete.
blueprints/admin/blueprints-Every blueprint this install offers and every one you own, and which of your sites is running each. A blueprint is a whole set-up — pages, portals, staff screens and the tables under them — so it sits ABOVE Page Builder, which edits one page of one.
page-builder/admin/page-builder-Compose a page visually, then bind it to a route as page:<slug>.
static-pages/admin/static-pages-Hand-written files any site can serve.
apps-registry/admin/apps-registry-The apps a site can point at.
deploy/admin/deploy-Push a build out.
client-routes/admin/client-routes-The PLATFORM app's own routes - a different scope from a site's pages. Do not cross them.
view-modes/admin/view-modes-Tenant-authored x- render modes, which is why the mode catalog must always be fetched.
page-views/admin/page-views-Traffic across every site.
global-settings/admin/global-settings-Defaults every site under this tenant inherits.
configs/admin/configs-The raw config documents behind every site.

Tabs in focus

setup - Setup

Connect a domain the operator owns, and prove they own it.

Read with: list_domains | check_domain

Write with: connect_domain

Trap: Connecting writes no DNS anywhere - Leumas only READS it to prove ownership, so the person must publish the TXT record themselves. A domain serves NOTHING until it is verified; reporting a connect as done is the misleading half of a true sentence.

sites - Websites

Which of your websites this domain serves - one row per hostname, and the one place a hostname ALIAS is set.

Read with: list_hostnames | list_websites | get_alias_contract

Write with: bind_hostname | unbind_hostname | POST /api/hosting/zones/:zoneId/bindings

  1. A subdomain of a PROVED domain needs no DNS work at all - the wildcard record set up during verification already covers it. Say so; people expect to have to do something.
  2. bind_hostname points one address at one website. Several labels at once is a single act in the UI and several calls here.
  3. Prefer an ALIAS over a second website when the only difference is what it looks like or which kind of page it shows: pass overrides to bind_hostname.

Trap: [critical] An alias is an ALLOWLIST of six presentation keys (mode, data, routes, appearance, skin, seo). Publish status, ownership, what the host serves and baseApiUrl belong to the website and are refused BY NAME - a binding must never be a way around the site lane. A mode override must bring its own data, or a payload shaped for one renderer reaches another that has never heard of it.

hosted-sites - My Websites

Every website this operator has, with its status.

Address: /admin/hosted-sites

Read with: list_my_sites | GET /api/hosting/sites

Write with: start_new_website | open_site_editor

content - Home

The page this site serves at its root: its mode, and that mode's data payload.

Read with: get_site_overview | list_modes | get_mode_schema | open_editor_tab

Write with: set_site_content | save_site

  1. get_site_overview FIRST - it returns the current mode, publish state, subdomain, SEO and the unsaved payload.
  2. Changing mode? get_mode_schema(newMode) for the field contract and a template before you write anything.
  3. set_site_content with only what changes.
  4. save_site - nothing above this line has persisted.

Trap: This is the ONLY page that needs save_site. Also: asking for a template for a mode that does not exist silently returns the default template, so an empty-looking result is not proof the mode is real - check the catalog.

pages - Pages

Every page that is not the home page.

Read with: list_pages | GET /api/hosting/sites/:id/routes

Write with: add_page | set_page_content | remove_page | POST /api/hosting/sites/:id/routes

  1. list_pages for what exists and each page's id.
  2. add_page with an absolute path and a componentKey.
  3. set_page_content edits one; remove_page drops one.

Trap: These write IMMEDIATELY - no save_site. A componentKey with an unrecognised prefix is re-read as a built-in and then refused with a message about the wrong thing, so check the prefix (mode:, page:, dynamic:, library:, ssr:, static:) before believing the error. And a non-public page is NEVER answered by the fast server-rendered plane - gating a page also hides it from crawlers and link previews, which is usually not what "members only" was meant to buy.

appearance - Appearance

Preset, colours, type, corners, logo - the whole look of the site.

Read with: get_appearance | list_theme_presets | GET /api/hosting/theme-presets

Write with: set_appearance | save_site

  1. get_appearance - it returns the current values AND the contract, so there is no need to guess a field name.
  2. list_theme_presets if the user wants a whole look rather than one colour.
  3. set_appearance with only the keys that change.
  4. save_site.

Trap: Colours are a grammar (hex, rgb, hsl, oklch, color-mix) and anything unparseable is dropped silently. Everything else is a closed list. Custom CSS answers 402 without the capability - that means "not bought", not "not allowed".

seo - SEO

Title, description, keywords and the social card image.

Read with: get_site_overview

Write with: set_seo | save_site

Trap: Editor state - it does not persist until save_site.

domains - Domains

The subdomain this site answers on, and any custom domains attached to it.

Read with: GET /api/hosting/sites/:id/domains

Write with: POST /api/hosting/sites/:id/domains | POST /api/hosting/sites/:id/domains/:host/verify

  1. Set the hostnames, then read them back - the response carries the DNS records the user must add.
  2. Once the records are in place, run the verify call.

Trap: A domain that is attached but not verified serves nothing and reports no error on the site itself - check here, not the front end. The site id IS the subdomain, so renaming one is a different, operator-only call.

analytics - Analytics

Who visited this site, and when.

Read with: GET /api/hosting/sites/:id/telemetry | GET /api/hosting/activity

target - Serves

What this host actually serves: an Imperium page, a proxied app, an uploaded bundle, or a redirect.

Read with: get_target | GET /api/hosting/sites/:id

Write with: set_target | PUT /api/hosting/sites/:id | POST /api/hosting/sites/:id/proxy-check

  1. get_target FIRST - it returns the current target AND the exact fields every kind takes, so there is nothing to guess and nothing to remember.
  2. set_target with the WHOLE target including kind. The fields are per-kind, so a partial patch across kinds means nothing.

Trap: [critical] target is NOT mode. mode decides how the home page renders; target decides whether the mode renderer is reached at all - only kind: "config" gets there. An embed mode iframes somebody else's site; a proxy target IS the site, on your origin, with your TLS and cookies. static and server are operator-only.

files - Files

Upload a build for this site to serve.

Read with: GET /api/hosting/modes/:siteId/folder/tree | GET /api/hosting/modes/:siteId/folder/usage

Write with: POST /api/hosting/modes/:siteId/folder/upload | POST /api/hosting/modes/:siteId/folder/dir

Trap: This prefix is the per-site STORAGE lane and has nothing to do with the mode catalog. Uploading a build does not serve it - that is a second call setting target to {kind:"bundle", dir:"builds/<id>"}.

connect - Your app

Point this host at something the operator already runs, and test that it answers.

Write with: POST /api/hosting/sites/:id/proxy-check

Trap: Only actionable for the modes that describe an external app. For everything else the answer lives on the Serves tab.

mcp - MCP

Serve this site as a tool server that outside agents can call.

Read with: GET /mcp/hosted | GET /api/hosting/sites/:id/grants

Write with: POST /api/hosting/sites/:id/grants

Trap: A grant secret is returned ONCE, at creation. Anything beyond a private server answers 402 - a purchase, not a permission.

code - Code

The whole site as one editable config - import and export it as a single file.

Read with: GET /api/hosting/sites/:id/export

Write with: POST /api/hosting/sites/:id/import

Trap: Editing here bypasses every field-level lane above, so the tiered write rules still apply and a refusal will be about a field, not the file.

shield - Shield

Per-site edge rules - who may reach this site at all, and how often.

Read with: get_shield

Write with: set_shield | PUT /api/hosting/sites/:id

Trap: [critical] An EMPTY crawler list means INHERIT the standard one, not "nobody". Writing denyAgents: [] to be explicit removes the scraper protection the site already had. The deny list is also checked BEFORE the welcome list, so one broad pattern there de-indexes the site with nothing to read in a log.

danger - Danger

Unpublish, rename and delete.

Write with: POST /api/hosting/sites/:id/unpublish | DELETE /api/hosting/sites/:id

Trap: Unpublish is ungated and reversible; delete is not, and it also sweeps every zone binding naming this site. Rename is operator-only because the id is the subdomain.

blueprints - Blueprints

Every blueprint this install offers and every one you own, and which of your sites is running each. A blueprint is a whole set-up — pages, portals, staff screens and the tables under them — so it sits ABOVE Page Builder, which edits one page of one.

Address: /admin/blueprints

Trap: Three kinds of row and they are not interchangeable: an OPERATION is a pack, a STARTER is ours and cannot be edited (fork it and the copy is yours), and YOURS is a fork on one of your sites — the only editable kind. A pack’s parts are deliberately NOT listed one by one: a staff workspace installed on its own is a workspace with no public site and no portal.

Ways in

Declared cross-links. Every address is matched against the real route table by check:huds.

FromDoesGo to
hosting/hosted-sitesStart a website/admin/site-wizard

Tools an agent can call here

These are live page capabilities: they exist only while that surface is open, and they call the page's own mutators, so a change made through one is a change the operator can see. A tool missing from this list is not callable — reach for the API or an adapter instead.

hosting:site-editor — Site: …

Source: hosting/model/useSiteCapabilities.js

What the page tells an agent about itself:

Call get_site_overview first. Use get_mode_schema before changing a mode (or before setting a mode: page's props) so payloads fit the contract. TWO LANES, do not confuse them: the HOME page is config.mode + config.data — edit it with set_site_content and persist with save_site. Every OTHER page is a route: add_page / set_page_content / remove_page write immediately through the gated lane, so they need no save_site. Make surgical edits. Only pass publish:true when the user asks to go live. Report exactly what changed.
ToolArgsDoes
get_site_overviewRead this site: id, name, current mode, publish status, subdomain, SEO and the current (unsaved) data payload. Call FIRST before editing.
list_modesEvery render mode this site can switch between (type, pageName, group). Call it rather than assuming - the catalog grows per release and a tenant can add its own.
get_mode_schemamodeA mode's data contract — the exact fields its payload supports — plus a default template. Call before set_site_content when changing modes or unsure of a field.
set_site_contentappName mode dataUpdate this site's name, render mode and/or data payload IN THE EDITOR (the live preview updates; nothing persists until save_site). Pass only what changes.
set_seotitle description keywords imageUpdate this site's SEO (title, description, keywords, social image URL). Editor state — persist with save_site.
list_theme_presetsThe named looks a site can start from (id, name, description, the appearance each applies).
get_appearanceThis site's current look PLUS the full appearance contract (every field and its allowed values). Call before set_appearance.
set_appearancepreset mode accent background surface text font radius density logoChange how the site LOOKS — preset, light/dark, colours, typeface, corners, spacing, logo. Editor state, so the preview updates immediately; persist with save_site. Pass only what changes; pass null for a key to clear it.
save_sitepublishPersist the current editor state (the same save the buttons use — validated server-side). publish:true also pushes the site live.
open_editor_tabtabSwitch the editor to a tab: manage (the mode’s own records — posts, links, files — when the mode has them), content (the home page), pages, appearance, seo, domains, connect, target, files, mcp, shield, analytics, assets, code, danger.
list_pagesThis site's pages: address, title, what each shows (componentKey), who can open it, whether it is in the menu. Call before adding or editing one.
add_pagepath componentKey props label guard navVisibleAdd a page. componentKey is usually "mode:<type>" (any render mode as a page) — then props is that mode's data payload, so call get_mode_schema first. Also valid: "page:<slug>" (a Page Builder page) or "library:<path>".
set_page_contentid path componentKey props label guard navVisible enabledUpdate one page by its id (from list_pages). Pass only what changes — props replaces that page's data payload.
remove_pageidDelete a page by its id (from list_pages). The home page is config.mode/data, not a page — use set_site_content for that.
get_targetWhat this host SERVES, and the exact fields each kind takes. kind decides everything else: config (the pages you author here) \proxy (an app already running elsewhere) \server (a local app Imperium starts) \static \bundle (a build uploaded to this site) \redirect. Call before set_target.
set_targetkind origin dir port toChange what this host serves. Pass the whole target including kind — the fields are per-kind, so a partial patch across kinds is meaningless. Writes immediately; static and server are operator-only and will be refused for a member.
set_page_gateid guardWho can open one page: public (anyone) \auth (signed in) \membership (an active membership). Takes a page id from list_pages. NOTE a non-public page is only served after the site's app boots, so a crawler or a link preview sees nothing on it.
get_shieldThis site's edge rules — rate limit, welcome and refusal crawler lists, honeypot paths — plus the full contract (every field and what it means). Call before set_shield.
set_shieldenabled maxRpm allowAgents denyAgents blockPathsChange the edge rules. Pass only what changes. Everything is inert while enabled is false.

2 more tool(s) on this scope are named at runtime (a computed key, or built by a helper), so they cannot be listed here. Call listTools on the live page to see them.

hosting:websites-home — My Websites

Source: hosting/model/useSiteCapabilities.js

What the page tells an agent about itself:

list_my_sites shows what exists; open a site before trying to edit it — the editor page grants the editing tools.
ToolArgsDoes
list_my_sitesThe signed-in user's Imperium websites: id, name, mode, publish status, subdomain.
open_site_editoridOpen a site in the Site Editor (where content/SEO/publish tools become available).
start_new_websiteOpen the New Website wizard (template gallery → subdomain claim → content → publish).

unnamed scope

Source: hosting/model/useZoneCapabilities.js

1 more tool(s) on this scope are named at runtime (a computed key, or built by a helper), so they cannot be listed here. Call listTools on the live page to see them.

hosting:domains — Connected domains

Source: hosting/model/useZoneCapabilities.js

What the page tells an agent about itself:

Connecting a domain writes no DNS anywhere — Leumas only READS it to prove ownership. The person must publish the TXT record themselves, then check_domain confirms it. A domain serves nothing until it is verified. Say that plainly rather than reporting a connect as done. Open a domain to bind hostnames under it; the binding tools live on that page.
ToolArgsDoes
list_domainsThe domains this account has connected: host, whether ownership is proved, and how many hostnames are bound under each. A domain must be verified before anything on it serves.
connect_domainhostClaim a domain this account owns. It starts pending and serves nothing until the TXT record in the result is published and check_domain passes. Idempotent — connecting one already connected returns it.
check_domainidAsk DNS whether the ownership record is published yet, and flip the domain to verified if it is. Safe to call repeatedly; DNS can take minutes to hours to propagate.
open_domainidOpen one domain’s setup checklist, where the DNS records and the hostname map live.

hosting:domain — Domain: …

Source: hosting/model/useZoneCapabilities.js

What the page tells an agent about itself:

Adding a subdomain of a PROVED domain needs no DNS work at all — that is what the wildcard record set up during verification is for. Say so; people expect to have to do something. One website can answer on several hostnames. Prefer an alias over a second website when the difference is only what it looks like or which kind of page it shows. Removing a hostname does not delete the website behind it.
ToolArgsDoes
list_hostnamesEvery hostname on this domain and what it serves — including whether an address is ALIASED (serving the bound website differently from its other addresses).
list_websitesThe websites that can be put on this domain (the signed-in account’s own).
bind_hostnamehost siteId overridesPoint a hostname on this domain at one of the account's websites. A subdomain of a proved domain needs NO DNS work — the wildcard record already covers it. Pass the full hostname (blog.example.com), or the domain itself for the apex.
unbind_hostnamehostStop serving a hostname. The website itself is untouched — this only removes the address.
get_alias_contractThe exact fields a hostname alias may carry, and what each does. Call before passing overrides to bind_hostname.

API

Mounted prefixes that serve this domain: /api/hosting · /api/hosting/modes

The full route table is leumas-capabilities/reference/api-routes.md.

Traps

The authed lane is /api/hosting; the site STORAGE lane is /api/hosting/modes/:siteId/...; the mode CATALOG is GET /index/modes. The middle one is not the last one. Read the status code: 402 means the operator may do this and has not bought it (custom CSS, MCP visibility, quota). 403 means operator-only (static/server targets, trustedOrigin, rename, theme/header/footer). Sub-tab paths under a site are declared WITHOUT the site segment - /admin/hosting/sites/domains is the SHAPE; the running app supplies the id. A site id is its subdomain, and a deleted subdomain becomes claimable again.

Source .claude/skills/leumas-studio/reference/domains/hosting.md (no-git)markdownjson
Generated from the Leumas repository. Every page cites the file it came from.leumas.techllms.txt