Skip to content

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.

sh
cd qcloud
cargo run          # boots the control DMS + serves the Cloud Console

On 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.

text
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=1800

Output

<!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".

ScreenPathMin cloud role
Overview/cloudany authenticated operator
Tenants/cloud/tenantsadmin
Plans & Subscriptions/cloud/plansadmin
Nodes & Capacity/cloud/nodesadmin
Billing/cloud/billingadmin
Governance/cloud/governancecustodian
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 window
Console page lifecycle: every authenticated screen runs two gates before it reads or renders.

The 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:+…
One mutation, end to end. There is no privileged backdoor: the Console can do nothing the operator's cloud role cannot.
Form action routeMethodCalls cloud.* fnMin role
/cloud/actions/provisionPOSTcloud.provisionadmin
/cloud/actions/scale-verticalPOSTcloud.scale_verticaladmin
/cloud/actions/scale-horizontalPOSTcloud.scale_horizontaladmin
/cloud/actions/switch-modePOSTcloud.switch_modeadmin
/cloud/actions/suspendPOSTcloud.suspendadmin
/cloud/actions/resumePOSTcloud.resumeadmin
/cloud/actions/terminatePOSTcloud.terminatecustodian
/cloud/actions/generate-invoicePOSTcloud.generate_invoiceadmin

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=20

Response

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-store

Output

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:

json
{
  "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.

ScreenBadgeWhat it marks
OverviewSIMULATEDReal DMS-instance spawning, OS-level cgroup caps, live scaling, and real billing/payment are PLANNED.
Tenants (switch mode)NON-DESTRUCTIVEThe on-disk layout is identical across modes, so switching either direction is safe at any time.
PlansPLANNEDThe dynamic pricing slider that sets a tenant's cap and live quote; today the plan is seeded as data and read here.
NodesSIMULATEDPlacement is first-fit bin-packing over node rows; real OVH/Hetzner provisioning, CVM/SEV-SNP setup, and live rebalance are PLANNED.
BillingSIMULATED METERUsage is synthesized from cap × a nominal period and priced at the plan rate; a real meter and real payment are PLANNED.
GovernancePLANNEDMinting cloud operators from the UI and the M-of-N custodian seed ceremony arrive with the governance/crypto backend.