# Build-knowledge — Imperium hosting (Studio dashboard + production serving)

- Configs are declarative mode+data JSON (not JSX), so there is NO server-side compilation / vm / AST validation. The safe schema is the config. Resolving a host = a DB read + a gate check.


> How a Leumas tenant hosts their own Imperium sites — set up, edit (any mode, live preview), publish to
> production, connect external apps — and how a white-label buyer runs the same thing in their own
> ecosystem. This is the build-knowledge entry for the hosting spine.

## The mental model

```
config doc (api.db('configs'))  =  one subdomain
  { id(=subdomain), appName, mode, data, seo, hosting:{ ownerUserId, tenantId, status, subdomain, domains } }
        │
Studio ▸ Hosting ──-> /api/hosting/* ← @leumas/hosting (owner+tenant CRUD + publish lifecycle)
  useCapability('imperium.hosting')                 gated by ↓
                                              @leumas/entitlements  (policy → capabilities + quotas, DATA)
visitor {sub}.leumas.tech ──-> /api/hosting/resolve?host= ← same @leumas/hosting core (published+entitled gate)
                                              → DynamicPage renders mode+data  (+ per-config SEO/sitemap/robots)
standalone white-label:  @leumas/imperium-server  imports the SAME @leumas/hosting core (one core, two deployments)
```

- **Configs are declarative `mode`+`data` JSON** (not JSX), so there is NO server-side compilation /
  vm / AST validation. The safe schema *is* the config. Resolving a host = a DB read + a gate check.

## The three new units

- **`@leumas/entitlements`** (`shared/packages/entitlements`) — the swappable membership-ACCESS axis
  (sibling to PassNode's monetization axis). Plans **declare** capabilities (booleans) + quota limits
  (numbers) as DATA in the Studio-editable `entitlement_policies` collection (one doc per plan/service),
  with per-user overrides in `membership.metadata`. Resolver merges active memberships (OR booleans, MAX
  limits). Backend: `entitlements.requireCapability(cap)` / `checkQuota(user, cap, count)` / `forUser`.
  Frontend: `@leumas/features` `useCapability(cap)` / `useLimit(cap)` / `<RequireCapability cap fallback>`
  off `GET /entitlements/me`. Wired in `app.js` beside PassNode (load + refresh on the `/db` change-feed).
  Change a rule = edit a doc; **zero code change**.
- **`@leumas/hosting`** (`shared/engines/plugins/hosting`) — the hosting core: `router.js` (`/api/hosting`
  owner+tenant CRUD, publish/unpublish/suspend, subdomain claim, domains, proxy-check, telemetry),
  `resolve.js` (host→published+owner-entitled config or suspension fallback; strips secrets),
  `seo.js` (meta + sitemap.xml + sitemap-index + robots.txt), `serve.js` (public resolve + SEO routers),
  `proxy.js` (SSRF-safe reverse proxy — denies RFC1918/loopback/link-local/ULA/metadata/CGNAT, DNS-rebind
  pin, strips visitor Cookie/Authorization, timeout/size caps, gated by `imperium.reverseProxy`; plus
  `createProxyUpgradeHandler` so **WebSockets** — HMR, socket.io — survive the proxy under the same rules),
  `targets.js` (what a host serves: `config` · `proxy` · `server` · `static` · `bundle` · `redirect`),
  `localApps.js` (the `server` kind's supervisor — spawns a member's app from a folder+port, restarts it
  with backoff, keeps a log ring; **self-host only**, inert unless the deployment passes `allowSpawn`),
  `subdomains.js` (normalize/reserved/host helpers).

  **`mode` vs `target` — the name collision worth knowing.** A *mode* decides what a page renders;
  a *target* decides whether a page is rendered at all. `mode:'server'` frames someone's app in an
  iframe. `target.kind:'server'` **is** the app, served as the domain itself. Both exist; they are not
  alternatives to each other. The dispatcher is mounted by leumas-api **and** imperium-server, so a
  target behaves identically on an appliance and on a white-label box. Guard: `pnpm smoke:hosting`.
- **`@leumas/imperium-server`** (`shared/services/imperium-server`) — the standalone white-label edition;
  `createImperiumApp()` reuses the SAME `@leumas/hosting` resolve/SEO, injects per-subdomain SEO into the
  web bundle's index.html, exposes `/healthz`+`/readyz`, and sits behind the wildcard `deploy/Caddyfile`
  (`*.leumas.tech` → identical worker pool, preserve Host). **v1.0**: `bin/create-imperium.mjs` scaffolds an
  instance (runtime layout + generated secrets + seeded `__global` and first site, idempotent), `README.md` is
  the host-your-own-infra guide, entitlements come from a Leumas-signed offline token on a licensed appliance
  or local policies + committed defaults when self-hosted. Package it with
  `node ops/tools/exe-builder/build-imperium.mjs` (pnpm deploy **hoisted** — symlinked deps would break the
  moment the folder is copied) → a relocatable ~117 MB bundle with `start.cmd`/`start.sh`.

## Nested multi-tenancy

`config.hosting.ownerUserId` + `config.hosting.tenantId` (`req.user.tenantId` from `users.metadata`, default
`'leumas'`). Scope in the hosting router: **end-user** sees own · **tenant-admin** (role `tenant_admin` or
capability `imperium.tenantAdmin`) sees the whole tenant · **Leumas admin** sees all. A white-label buyer is a
tenant whose members host under `imperium.*` capabilities their plan grants.

## Studio surfaces (`products/leumas-studio/src/admin/hosting/`)

**`WebsitesHome`** ("My Websites": cards with live `DynamicPage` thumbnails, status, quota, draft-preview
links) · **`CreateSiteWizard`** (`/admin/site-wizard`: template-or-blank → live subdomain claim → schema form
beside the live preview → publish) · **`SiteEditor`** (tabs: Content · SEO · Domains · Connect · Shield ·
Analytics · Danger) · **`GlobalSettings`** (the `__global` layer) · **`ViewModes`** (the view-type index +
custom-type upload) · `OperatorTab` (admin cross-lane inventory) · `PreviewFrame` · `HostingPaywall`.
The whole domain is gated by ONE `<RequireCapability cap="imperium.hosting">`. `ConfigManager` survives as the
operator raw-JSON lane — it writes through the hosting router, never `api.db('configs')`.

## Modes — the data contract

Every mode's `data` shape is DECLARED once in `config-registry/src/dataSchemas.js` (descriptor fields). That
single source drives: the generated Studio form (`@leumas/ui` `SchemaForm`), **server-side validation on every
save/publish** (`validateData` → 400 with a field-level error list), the new-site template, and the docs.
Every built-in mode has a renderer, and a guard asserts both directions of that — no mode without a
renderer, no schema without a mode. **The count is deliberately not written here.** It was "29" in
this file while the real number was 34, and the same stale figure is repeated in four other places;
a number in prose is a future lie. The live list is at
[docs.leumas.tech/modes](https://docs.leumas.tech/modes), generated from `MODES` at build time.

**User-uploaded view types**: a doc in the `view_modes` collection carries its own field schema + default
template + sandboxed HTML renderer (`x-` namespace, `CustomModePage`). It flows through the SAME validation,
SchemaForm and DynamicPage path as a built-in — zero new engine code. Anonymous visitors fetch them from the
PUBLIC `GET /api/hosting/view-modes` (`/db` is auth-gated and would 401 a first-time visitor).

## Serving guarantees

A config serves only when **published** (hosted lane) or explicitly **`firstParty: true`** (operator lane) —
an unclaimed raw doc 404s. Served output is the merge `__global` → `__global--<tenantId>` → site (local wins,
arrays concat), behind a TTL read-cache that every write busts. Drafts are visible only through a signed
30-minute `preview-token`, noindexed. Storage-backed modes (folder/gallery/exe) read a per-site root under
`IMPERIUM_STORAGE_ROOT`, traversal-guarded; raw filesystem roots are self-host-only and env-allowlisted.

## Extend it

- New capability/quota → add a key to an `entitlement_policies` doc (data). Read it via `useCapability`/
  `useLimit` (frontend) or `requireCapability`/`getLimit` (backend).
- New built-in mode → add the page shell, register it in `DynamicPage` (or a package `registerModes.js`), and
  add its `dataSchemas.js` entry (fields + template). `check-imperium` FAILS on a mode without either.
- New view type without touching code → upload it in Studio ▸ **View Modes**.
- Agents drive it through the `imperium` adapter (auto MCP + functioncalls) and the Leviathan page
  capabilities on the Websites surfaces.
- Guards: `pnpm check:imperium` (schemas, renderers, templates, write-lane, component-registry drift) ·
  `pnpm smoke:imperium` (full e2e through the standalone server).
- Migration ledgers: `ops/migration/imperium-hosting.json`, `imperium-modes.json`, `imperium-configs.json`.


---
Source: shared/services/knowledge/build-knowledge/imperium-hosting.md
Canonical: https://docs.leumas.tech/p/how-to/imperium-hosting
