Qirava Cloud docs
Cloud Console: RBAC-gated web interface
The Cloud Console is Qirava Cloud's web interface for operating the managed-DMS fleet. It is itself a Quill app served by the control DMS at /cloud — exactly the way the Qirava website and Studio are Quill apps on the qdms engine. There is no separate admin backend: the Console is an ordinary CLIENT of the control plane. It renders server-side HTML pages that read the _cp_* catalogs through the planner, and it mutates the fleet only by POSTing HTML forms to handlers that call the cloud.* orchestration functions with the signed-in operator's role attached. Real persistence is durable; every effect that would touch real infrastructure (booting a tenant DMS, applying OS caps, real billing) is honestly marked as SIMULATED/PLANNED in the UI.
Starting the Console#
The Console ships inside the qcloud binary. Running qcloud boots a control-plane Qirava DMS, ensures the _cp_* catalogs, registers the cloud.* functions, registers the Console handlers and routes, mints a bootstrap cloud custodian on first install, and serves everything on one port. The Console shares the system worker's port — there is no second server.
cd qcloud
cargo run # boots the control DMS + serves the Cloud ConsoleOn first boot the bootstrap cloud custodian's username and password are printed exactly once to stdout. Save them — that account is the root of the cloud authority domain and is how you first sign in.
Qirava Cloud — managed-DMS control plane
shape: a Qirava DMS running the `cloud` app (dogfooded)
engine: one control DMS + executor (auto resource config)
data: durable @ /home/you/.qcloud/qcloud-data
catalogs:_cp_tenants/_cp_nodes/_cp_plans/_cp_subscriptions/_cp_usage/_cp_invoices/_cp_audit
fns: cloud.provision/scale_vertical/scale_horizontal/switch_mode/suspend/resume/terminate/generate_invoice
console: RBAC-gated Cloud Console at http://127.0.0.1:7180/cloud (login at /cloud/login)
note: real infra (DMS spawn, cgroup caps, live scaling, payment) is SIMULATED + clearly badged PLANNED
BOOTSTRAP CLOUD CUSTODIAN (shown once): user=cloud-custodian password=...
listening…- QCLOUD_ADDR
- Bind address. Defaults to 127.0.0.1:7180 (one above the DMS's default so both can run locally).
- QCLOUD_DATA
- Data directory for the durable control-plane store (WAL + recovery). Defaults to $HOME/.qcloud/qcloud-data. Set to the literal value memory for an ephemeral in-memory store.
Logging in#
Open http://127.0.0.1:7180/cloud/login and sign in as a cloud operator. Cloud operators live in the control DMS's own _sys_users table and authenticate exactly like any DMS user — login reuses the engine's shared auth.login governance function (the same one Studio and the API use). There is no special cloud login path beyond the form.
- GET /cloud/login renders the sign-in form (username + password).
- POST /cloud/login calls auth.login. On success it sets the HttpOnly session cookie and redirects (302) to /cloud. On failure it re-renders the form with an error.
- POST /cloud/logout verifies the CSRF token, clears the cookie, and redirects to /cloud/login with a flash message.
On a successful login the session token returned by auth.login is set as the qdms_session cookie: HttpOnly, SameSite=Lax, Path=/, with Max-Age equal to the engine's session idle timeout (30 minutes = 1800 seconds). The cookie name matches what the engine's shared auth.session before-function reads; the meaningful per-app isolation is the domain-separated CSRF token (see Mutation flow), not the cookie name.
Request
POST /cloud/login HTTP/1.1
Content-Type: application/x-www-form-urlencoded
username=cloud-custodian&password=<bootstrap-password>Response
HTTP/1.1 302 Found
Location: /cloud
Content-Type: text/html; charset=utf-8
Cache-Control: no-store
Set-Cookie: qdms_session=<token>; HttpOnly; SameSite=Lax; Path=/; Max-Age=1800Output
<!doctype html><meta http-equiv="refresh" content="0;url=/cloud"><a href="/cloud">Continue</a>The shell and screens#
Every authenticated page is wrapped in the CloudShell chrome: a sticky header with the qbrand wordmark lockup (recolored to currentColor so it is theme-adaptive), a dark/light theme toggle, the operator@role badge, and a Logout form; a left sidebar nav; and the main content column. The sidebar is RBAC-filtered — it renders only the screens the signed-in cloud role can reach, and marks the current screen with aria-current="page".
| Screen | Path | Min cloud role |
|---|---|---|
| Overview | /cloud | any authenticated operator |
| Tenants | /cloud/tenants | admin |
| Plans & Subscriptions | /cloud/plans | admin |
| Nodes & Capacity | /cloud/nodes | admin |
| Billing | /cloud/billing | admin |
| Governance | /cloud/governance | custodian |
- Overview
- A read-only fleet summary: stat tiles for Tenants / Active / Suspended / Clusters / Nodes / Invoices, plus a control-plane note and the honest SIMULATED disclosure.
- Tenants
- Lists managed tenant DMSes (id, owner, mode ×replicas, storage, cap, status pill, node) with per-row actions, plus forms to provision, scale vertically, scale horizontally, and switch mode.
- Plans & Subscriptions
- Shows the per-unit pricing plan and the subscription rows. Plan editing (the dynamic pricing slider) is badged PLANNED.
- Nodes & Capacity
- The bare-metal fleet with allocation/cap headroom per resource. Placement is first-fit bin-packing over these rows; real node provisioning and rebalance are PLANNED.
- Billing
- Invoices, the latest metered-usage samples, and a form to generate an invoice from a (simulated) meter.
- Governance
- Custodian-only: the cloud authority-domain explainer, the live list of cloud operators (from _sys_users), and the control-plane audit trail (_cp_audit).
GET /cloud/tenants
|
v
[ auth.session before-fn ] validates qdms_session cookie, injects ctx.{user, role}
|
v
require_user(rec) -- no session? -> 302 /cloud/login
|
v
require_access(...) -- role lacks capability? -> 403 page (not blank)
|
v
_cp_* planner reads -> render CloudShell(document) -> HTML
|
v
[ auth.session_extend after-fn ] slides the idle windowThe mutation flow: form-POST to cloud.* to PRG#
The Console never mutates state directly. Each action is a server-rendered HTML <form method="post"> whose target is a /cloud/actions/* route. The handler resolves the operator from the request ctx, verifies a session-bound CSRF token, does a cheap client-side role pre-check, then calls the matching cloud.* function through the runtime with the operator's role injected into ctx.role. The function re-checks that role server-side, deny-by-default, and persists to _cp_*. Finally the handler issues a Post/Redirect/Get (PRG) redirect back to the originating screen carrying a ?flash= (success) or ?error= (failure) message.
<form POST /cloud/actions/provision> (CSRF hidden field + named fields)
|
v
1. SESSION resolve ctx.{user, role}; none -> 302 /cloud/login
2. CSRF verify _csrf == HMAC-SHA256(session_token, "qcloud-csrf")
3. PRE-CHECK role_rank(role) >= role_rank("admin") (fast UI gate only)
4. CALL cloud.provision via runtime.execute, ctx.role attached
-> re-checks role (deny-by-default) -> writes _cp_* -> _cp_audit
5. PRG 302 /cloud/tenants?flash=Provisioned+tenant+'acme'+—+PLANNED:+…| Form action route | Method | Calls cloud.* fn | Min role |
|---|---|---|---|
| /cloud/actions/provision | POST | cloud.provision | admin |
| /cloud/actions/scale-vertical | POST | cloud.scale_vertical | admin |
| /cloud/actions/scale-horizontal | POST | cloud.scale_horizontal | admin |
| /cloud/actions/switch-mode | POST | cloud.switch_mode | admin |
| /cloud/actions/suspend | POST | cloud.suspend | admin |
| /cloud/actions/resume | POST | cloud.resume | admin |
| /cloud/actions/terminate | POST | cloud.terminate | custodian |
| /cloud/actions/generate-invoice | POST | cloud.generate_invoice | admin |
The form-POST handler builds the cloud.* request record by injecting the operator's role and user into a ctx map — exactly the way the worker funnel injects it — never at the top level (so a forged top-level role cannot spoof). On a successful call, the function's returned simulated note is appended to the success flash so the operator sees precisely what was real versus simulated.
Request
POST /cloud/actions/provision HTTP/1.1
Cookie: qdms_session=<token>
Content-Type: application/x-www-form-urlencoded
_csrf=<hmac>&id=acme&owner=acme-corp&mode=standalone&storage=dynamic&threads=2&mem_gb=4&storage_gb=20Response
HTTP/1.1 302 Found
Location: /cloud/tenants?flash=Provisioned+tenant+'acme'+—+PLANNED:+boot+tenant+DMS+on+node-eu-1+(own+WAL/_sys_*/seed),+mint+tenant+custodian+(returned+once)
Content-Type: text/html; charset=utf-8
Cache-Control: no-storeOutput
The Tenants screen re-renders with a green success banner and a new row:
acme | acme-corp | standalone ×1 | dynamic | 2 thr · 4 GB · 20 GB | [active] | node-eu-1 | [→ cluster] [Suspend] [Terminate]Under the hood, cloud.provision returns this envelope; the Console lifts the simulated field into the flash and otherwise relies on the next page read to show the new state:
{
"tenant_id": "acme",
"simulated": "PLANNED: boot tenant DMS on node-eu-1 (own WAL/_sys_*/seed), mint tenant custodian (returned once)",
"status": "active",
"mode": "standalone",
"threads": 2,
"mem_gb": 4,
"storage_gb": 20,
"replicas": 1,
"node_id": "node-eu-1"
}Per-tenant inline actions#
Each non-terminated tenant row carries compact inline action buttons (each a one-field hidden-id form, CSRF-bound). The switch-mode button always offers the opposite of the current mode (both directions are supported). Suspend appears for active or pending_capacity tenants; Resume appears for suspended tenants. Terminate appears only for a cloud custodian and is styled as a destructive (red) button.
- → cluster / → standalone — switch to the other mode (non-destructive).
- Suspend — stop serving (active/pending_capacity tenants); danger-styled.
- Resume — restart in place (suspended tenants only).
- Terminate — custodian-only, destructive; releases the node allocation and cancels the subscription.
SIMULATED and PLANNED badges#
Honesty is a design rule: anything that would require real infrastructure is badged, never faked. The Console renders a dashed accent chip (.qc-sim) for these. The label text varies by context — SIMULATED, PLANNED, SIMULATED METER, or NON-DESTRUCTIVE — and every cloud.* success flash also surfaces the function's own simulated note describing the infra effect it WOULD perform.
| Screen | Badge | What it marks |
|---|---|---|
| Overview | SIMULATED | Real DMS-instance spawning, OS-level cgroup caps, live scaling, and real billing/payment are PLANNED. |
| Tenants (switch mode) | NON-DESTRUCTIVE | The on-disk layout is identical across modes, so switching either direction is safe at any time. |
| Plans | PLANNED | The dynamic pricing slider that sets a tenant's cap and live quote; today the plan is seeded as data and read here. |
| Nodes | SIMULATED | Placement is first-fit bin-packing over node rows; real OVH/Hetzner provisioning, CVM/SEV-SNP setup, and live rebalance are PLANNED. |
| Billing | SIMULATED METER | Usage is synthesized from cap × a nominal period and priced at the plan rate; a real meter and real payment are PLANNED. |
| Governance | PLANNED | Minting cloud operators from the UI and the M-of-N custodian seed ceremony arrive with the governance/crypto backend. |