{
  "schema": "leumas.docs.page/1",
  "id": "how-to:imperium-hosting",
  "slug": "how-to/imperium-hosting",
  "kind": "pages",
  "bucket": "how-to",
  "title": "Build-knowledge — Imperium hosting (Studio dashboard + production serving)",
  "name": "Imperium hosting",
  "eyebrow": "build knowledge",
  "chip": null,
  "summary": "- 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.",
  "keywords": [
    "imperium",
    "imperium hosting",
    "hosting",
    "site modes",
    "publish a website",
    "leumas imperium",
    "how to host a site on leumas",
    "white label hosting"
  ],
  "audience": "both",
  "funnel": {
    "product": null,
    "cta": null
  },
  "body": "# Build-knowledge — Imperium hosting (Studio dashboard + production serving)\n\n> How a Leumas tenant hosts their own Imperium sites — set up, edit (any mode, live preview), publish to\n> production, connect external apps — and how a white-label buyer runs the same thing in their own\n> ecosystem. This is the build-knowledge entry for the hosting spine.\n\n## The mental model\n\n```\nconfig doc (api.db('configs'))  =  one subdomain\n  { id(=subdomain), appName, mode, data, seo, hosting:{ ownerUserId, tenantId, status, subdomain, domains } }\n        │\nStudio ▸ Hosting ──-> /api/hosting/* ← @leumas/hosting (owner+tenant CRUD + publish lifecycle)\n  useCapability('imperium.hosting')                 gated by ↓\n                                              @leumas/entitlements  (policy → capabilities + quotas, DATA)\nvisitor {sub}.leumas.tech ──-> /api/hosting/resolve?host= ← same @leumas/hosting core (published+entitled gate)\n                                              → DynamicPage renders mode+data  (+ per-config SEO/sitemap/robots)\nstandalone white-label:  @leumas/imperium-server  imports the SAME @leumas/hosting core (one core, two deployments)\n```\n\n- **Configs are declarative `mode`+`data` JSON** (not JSX), so there is NO server-side compilation /\n  vm / AST validation. The safe schema *is* the config. Resolving a host = a DB read + a gate check.\n\n## The three new units\n\n- **`@leumas/entitlements`** (`shared/packages/entitlements`) — the swappable membership-ACCESS axis\n  (sibling to PassNode's monetization axis). Plans **declare** capabilities (booleans) + quota limits\n  (numbers) as DATA in the Studio-editable `entitlement_policies` collection (one doc per plan/service),\n  with per-user overrides in `membership.metadata`. Resolver merges active memberships (OR booleans, MAX\n  limits). Backend: `entitlements.requireCapability(cap)` / `checkQuota(user, cap, count)` / `forUser`.\n  Frontend: `@leumas/features` `useCapability(cap)` / `useLimit(cap)` / `<RequireCapability cap fallback>`\n  off `GET /entitlements/me`. Wired in `app.js` beside PassNode (load + refresh on the `/db` change-feed).\n  Change a rule = edit a doc; **zero code change**.\n- **`@leumas/hosting`** (`shared/engines/plugins/hosting`) — the hosting core: `router.js` (`/api/hosting`\n  owner+tenant CRUD, publish/unpublish/suspend, subdomain claim, domains, proxy-check, telemetry),\n  `resolve.js` (host→published+owner-entitled config or suspension fallback; strips secrets),\n  `seo.js` (meta + sitemap.xml + sitemap-index + robots.txt), `serve.js` (public resolve + SEO routers),\n  `proxy.js` (SSRF-safe reverse proxy — denies RFC1918/loopback/link-local/ULA/metadata/CGNAT, DNS-rebind\n  pin, strips visitor Cookie/Authorization, timeout/size caps, gated by `imperium.reverseProxy`; plus\n  `createProxyUpgradeHandler` so **WebSockets** — HMR, socket.io — survive the proxy under the same rules),\n  `targets.js` (what a host serves: `config` · `proxy` · `server` · `static` · `bundle` · `redirect`),\n  `localApps.js` (the `server` kind's supervisor — spawns a member's app from a folder+port, restarts it\n  with backoff, keeps a log ring; **self-host only**, inert unless the deployment passes `allowSpawn`),\n  `subdomains.js` (normalize/reserved/host helpers).\n\n  **`mode` vs `target` — the name collision worth knowing.** A *mode* decides what a page renders;\n  a *target* decides whether a page is rendered at all. `mode:'server'` frames someone's app in an\n  iframe. `target.kind:'server'` **is** the app, served as the domain itself. Both exist; they are not\n  alternatives to each other. The dispatcher is mounted by leumas-api **and** imperium-server, so a\n  target behaves identically on an appliance and on a white-label box. Guard: `pnpm smoke:hosting`.\n- **`@leumas/imperium-server`** (`shared/services/imperium-server`) — the standalone white-label edition;\n  `createImperiumApp()` reuses the SAME `@leumas/hosting` resolve/SEO, injects per-subdomain SEO into the\n  web bundle's index.html, exposes `/healthz`+`/readyz`, and sits behind the wildcard `deploy/Caddyfile`\n  (`*.leumas.tech` → identical worker pool, preserve Host). **v1.0**: `bin/create-imperium.mjs` scaffolds an\n  instance (runtime layout + generated secrets + seeded `__global` and first site, idempotent), `README.md` is\n  the host-your-own-infra guide, entitlements come from a Leumas-signed offline token on a licensed appliance\n  or local policies + committed defaults when self-hosted. Package it with\n  `node ops/tools/exe-builder/build-imperium.mjs` (pnpm deploy **hoisted** — symlinked deps would break the\n  moment the folder is copied) → a relocatable ~117 MB bundle with `start.cmd`/`start.sh`.\n\n## Nested multi-tenancy\n\n`config.hosting.ownerUserId` + `config.hosting.tenantId` (`req.user.tenantId` from `users.metadata`, default\n`'leumas'`). Scope in the hosting router: **end-user** sees own · **tenant-admin** (role `tenant_admin` or\ncapability `imperium.tenantAdmin`) sees the whole tenant · **Leumas admin** sees all. A white-label buyer is a\ntenant whose members host under `imperium.*` capabilities their plan grants.\n\n## Studio surfaces (`products/leumas-studio/src/admin/hosting/`)\n\n**`WebsitesHome`** (\"My Websites\": cards with live `DynamicPage` thumbnails, status, quota, draft-preview\nlinks) · **`CreateSiteWizard`** (`/admin/site-wizard`: template-or-blank → live subdomain claim → schema form\nbeside the live preview → publish) · **`SiteEditor`** (tabs: Content · SEO · Domains · Connect · Shield ·\nAnalytics · Danger) · **`GlobalSettings`** (the `__global` layer) · **`ViewModes`** (the view-type index +\ncustom-type upload) · `OperatorTab` (admin cross-lane inventory) · `PreviewFrame` · `HostingPaywall`.\nThe whole domain is gated by ONE `<RequireCapability cap=\"imperium.hosting\">`. `ConfigManager` survives as the\noperator raw-JSON lane — it writes through the hosting router, never `api.db('configs')`.\n\n## Modes — the data contract\n\nEvery mode's `data` shape is DECLARED once in `config-registry/src/dataSchemas.js` (descriptor fields). That\nsingle source drives: the generated Studio form (`@leumas/ui` `SchemaForm`), **server-side validation on every\nsave/publish** (`validateData` → 400 with a field-level error list), the new-site template, and the docs.\nEvery built-in mode has a renderer, and a guard asserts both directions of that — no mode without a\nrenderer, no schema without a mode. **The count is deliberately not written here.** It was \"29\" in\nthis file while the real number was 34, and the same stale figure is repeated in four other places;\na number in prose is a future lie. The live list is at\n[docs.leumas.tech/modes](https://docs.leumas.tech/modes), generated from `MODES` at build time.\n\n**User-uploaded view types**: a doc in the `view_modes` collection carries its own field schema + default\ntemplate + sandboxed HTML renderer (`x-` namespace, `CustomModePage`). It flows through the SAME validation,\nSchemaForm and DynamicPage path as a built-in — zero new engine code. Anonymous visitors fetch them from the\nPUBLIC `GET /api/hosting/view-modes` (`/db` is auth-gated and would 401 a first-time visitor).\n\n## Serving guarantees\n\nA config serves only when **published** (hosted lane) or explicitly **`firstParty: true`** (operator lane) —\nan unclaimed raw doc 404s. Served output is the merge `__global` → `__global--<tenantId>` → site (local wins,\narrays concat), behind a TTL read-cache that every write busts. Drafts are visible only through a signed\n30-minute `preview-token`, noindexed. Storage-backed modes (folder/gallery/exe) read a per-site root under\n`IMPERIUM_STORAGE_ROOT`, traversal-guarded; raw filesystem roots are self-host-only and env-allowlisted.\n\n## Extend it\n\n- New capability/quota → add a key to an `entitlement_policies` doc (data). Read it via `useCapability`/\n  `useLimit` (frontend) or `requireCapability`/`getLimit` (backend).\n- New built-in mode → add the page shell, register it in `DynamicPage` (or a package `registerModes.js`), and\n  add its `dataSchemas.js` entry (fields + template). `check-imperium` FAILS on a mode without either.\n- New view type without touching code → upload it in Studio ▸ **View Modes**.\n- Agents drive it through the `imperium` adapter (auto MCP + functioncalls) and the Leviathan page\n  capabilities on the Websites surfaces.\n- Guards: `pnpm check:imperium` (schemas, renderers, templates, write-lane, component-registry drift) ·\n  `pnpm smoke:imperium` (full e2e through the standalone server).\n- Migration ledgers: `ops/migration/imperium-hosting.json`, `imperium-modes.json`, `imperium-configs.json`.\n",
  "source": {
    "path": "shared/services/knowledge/build-knowledge/imperium-hosting.md",
    "blobSha": "",
    "commit": "",
    "committedAt": "",
    "provenance": "no-git",
    "bytes": 9028,
    "hash": "adc9bc65753c9376ad8e41a4e9b4ea173139ae6f"
  },
  "urls": {
    "html": "/p/how-to/imperium-hosting",
    "json": "/docs/how-to/imperium-hosting.json",
    "md": "/docs/how-to/imperium-hosting.md"
  },
  "links": {
    "composes": [],
    "usedBy": [],
    "product": [],
    "howTo": [],
    "skills": []
  }
}
