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.
| What | Call | Why | |
|---|---|---|---|
| render modes (what a site CAN be) | list_modes | GET /index/modes | grows every release, and a tenant can add its own x- modes - any list written down is a future lie |
| one mode's exact data contract | get_mode_schema | the fields a mode accepts are its own; guessing them produces a payload that validates as empty | |
| theme presets | GET /api/hosting/theme-presets | presets and the custom-CSS capability key are read together, per install | |
| this operator's sites and their status | list_my_sites | GET /api/hosting/sites | never assume a site exists, and never assume its status - draft and published behave differently |
| what this plan allows and how much is left | GET /api/hosting/access | returns 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
GET /api/hosting/accessfirst - it says whether this operator may host at all and how many sites are left.- Choose the mode by CALLING, never from memory:
list_modesfor the catalog (each row carries agroup: core, content, app, distribution, commerce), thenget_mode_schema(mode)for the exact fields and a starter template. POST /api/hosting/siteswith{id, appName, mode, data}. TheidIS the subdomain. The site is createddraft.- Edit the home page with
set_site_content({appName, mode, data}); it updates the preview only. save_sitepersists.save_site({publish: true}), orPOST /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
list_pagesto see what is there. Paths are absolute:/,/menu,/blog/:slug.add_pagewith acomponentKey. The usual one ismode:<type>- a whole render mode used as a page, whosepropsARE that mode's data payload, so callget_mode_schema(<type>)first.set_page_contentto edit one,remove_pageto drop one. All three write immediately.- Set
guardtopublic,authormembershipto 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
get_appearancereturns the current look AND the full contract - every field and its allowed values. Call it before writing.list_theme_presetsnames the whole-site looks a site can start from.set_appearancetakes only what changes; passnullfor a key to clear that key.save_siteto 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
- This is the
targetaxis, not the mode axis. Open the site editor's Serves tab. - 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). - For
proxy,POST /api/hosting/sites/:id/proxy-checkfirst - it is an SSRF-safe reachability probe. - Write it with
PUT /api/hosting/sites/:idcarrying{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.
| Tab | Path | Group | What 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
- 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.
bind_hostnamepoints one address at one website. Several labels at once is a single act in the UI and several calls here.- Prefer an ALIAS over a second website when the only difference is what it looks like or which kind of page it shows: pass
overridestobind_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
get_site_overviewFIRST - it returns the current mode, publish state, subdomain, SEO and the unsaved payload.- Changing mode?
get_mode_schema(newMode)for the field contract and a template before you write anything. set_site_contentwith only what changes.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
list_pagesfor what exists and each page's id.add_pagewith an absolutepathand acomponentKey.set_page_contentedits one;remove_pagedrops 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
get_appearance- it returns the current values AND the contract, so there is no need to guess a field name.list_theme_presetsif the user wants a whole look rather than one colour.set_appearancewith only the keys that change.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
- Set the hostnames, then read them back - the response carries the DNS records the user must add.
- 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
get_targetFIRST - it returns the current target AND the exact fields every kind takes, so there is nothing to guess and nothing to remember.set_targetwith the WHOLE target includingkind. 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.
| From | Does | Go to |
|---|---|---|
hosting/hosted-sites | Start 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.
| Tool | Args | Does | |||||
|---|---|---|---|---|---|---|---|
get_site_overview | — | Read this site: id, name, current mode, publish status, subdomain, SEO and the current (unsaved) data payload. Call FIRST before editing. | |||||
list_modes | — | Every 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_schema | mode | A 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_content | appName mode data | Update 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_seo | title description keywords image | Update this site's SEO (title, description, keywords, social image URL). Editor state — persist with save_site. | |||||
list_theme_presets | — | The named looks a site can start from (id, name, description, the appearance each applies). | |||||
get_appearance | — | This site's current look PLUS the full appearance contract (every field and its allowed values). Call before set_appearance. | |||||
set_appearance | preset mode accent background surface text font radius density logo | Change 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_site | publish | Persist the current editor state (the same save the buttons use — validated server-side). publish:true also pushes the site live. | |||||
open_editor_tab | tab | Switch 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_pages | — | This 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_page | path componentKey props label guard navVisible | Add 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_content | id path componentKey props label guard navVisible enabled | Update one page by its id (from list_pages). Pass only what changes — props replaces that page's data payload. | |||||
remove_page | id | Delete 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_target | — | What 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_target | kind origin dir port to | Change 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_gate | id guard | Who 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_shield | — | This 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_shield | enabled maxRpm allowAgents denyAgents blockPaths | Change 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.
| Tool | Args | Does |
|---|---|---|
list_my_sites | — | The signed-in user's Imperium websites: id, name, mode, publish status, subdomain. |
open_site_editor | id | Open a site in the Site Editor (where content/SEO/publish tools become available). |
start_new_website | — | Open 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.
| Tool | Args | Does |
|---|---|---|
list_domains | — | The 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_domain | host | Claim 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_domain | id | Ask 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_domain | id | Open 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.
| Tool | Args | Does |
|---|---|---|
list_hostnames | — | Every hostname on this domain and what it serves — including whether an address is ALIASED (serving the bound website differently from its other addresses). |
list_websites | — | The websites that can be put on this domain (the signed-in account’s own). |
bind_hostname | host siteId overrides | Point 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_hostname | host | Stop serving a hostname. The website itself is untouched — this only removes the address. |
get_alias_contract | — | The 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.