Qirava Cloud docs
Billing & metering
Qirava Cloud bills on a single pay-as-you-go plan: there are no fixed tiers, just a per-unit price for each resource a tenant consumes. The cloud.generate_invoice function turns a tenant's current resource cap into an invoice, writing both a usage sample to _cp_usage and an invoice to _cp_invoices. The invoice rows are real and durable; the metering itself is currently simulated — usage is synthesized from the tenant's cap over a nominal 24-hour period, where a production meter would instead stream live per-tenant counters. This page covers the plan model, the exact pricing formula, the two catalogs that billing writes, the role and error rules, and a faithful request/response example.
The per-unit plan#
The control plane seeds exactly one plan, with id metered, on first boot. The slider in the Console IS the plan — a customer chooses how many threads, how much memory, and how much storage they want, and is charged per unit. The prices are stored as strings in _cp_plans so that billing reads a single source of truth rather than hardcoding rates.
| Plan field | Seeded value | Unit |
|---|---|---|
| id | metered | — |
| label | Pay-as-you-go (per unit) | — |
| price_thread_hr | 0.012 | per thread, per hour |
| price_mem_gb_hr | 0.006 | per GB memory, per hour |
| price_storage_gb_mo | 0.10 | per GB storage, per month |
| price_bw_gb | 0.02 | per GB bandwidth |
The invoice formula#
cloud.generate_invoice computes the invoice total from the tenant's CURRENT cap, the plan rates, a fixed 24-hour period, and the replica count. Threads and memory are billed by the hour; storage is billed by the month, so the 24h slice of the monthly storage price is the rate divided by 30. The whole sum is multiplied by the number of replicas, because a cluster runs that many copies of the per-instance cap.
hours = 24.0
replicas = max(tenant.replicas, 1)
total = ( threads * price_thread_hr * hours
+ mem_gb * price_mem_gb_hr * hours
+ storage_gb * price_storage_gb_mo / 30.0 )
* replicas
total_str = format total with 2 decimals // e.g. "4.91"Worked arithmetic for a tenant with 4 threads, 8 GB memory, 30 GB storage, 1 replica, at the seeded rates: threads = 4 × 0.012 × 24 = 1.152; memory = 8 × 0.006 × 24 = 1.152; storage = 30 × 0.10 / 30 = 0.10; sum = 2.404; total_str = "2.40".
What gets written: _cp_usage and _cp_invoices#
A single call to generate_invoice writes two rows. First a usage sample to _cp_usage — the data point a real meter would stream — then the invoice itself to _cp_invoices with status "due". Both ids are time-based (use-<micros> and inv-<micros>).
- _cp_usage row
- { id: "use-<micros>", tenant_id, ts_us, thread_sec: threads*hours*3600*replicas, mem_gb_hours: mem_gb*hours*replicas, storage_gb, bw_gb: 0 }
- _cp_invoices row
- { id: "inv-<micros>", tenant_id, period: "last 24h (simulated)", total: <total_str>, status: "due", created_us }
- _cp_audit row
- { operator, action: "generate_invoice", tenant_id, node_id, result: <total_str> }
| _cp_usage field | Value for 4t/8g/30s, 1 replica |
|---|---|
| thread_sec | 4 * 24 * 3600 * 1 = 345600 |
| mem_gb_hours | 8 * 24 * 1 = 192 |
| storage_gb | 30 |
| bw_gb | 0 |
Roles and where it runs#
generate_invoice requires the cloud admin role (rank 2) or higher; the check is deny-by-default inside the function. In the Cloud Console it is the Generate invoice form on the Billing screen (/cloud/billing), which itself is admin+. The form POSTs to /cloud/actions/generate-invoice; that handler verifies the session and CSRF token, injects the operator's role into ctx, and calls the function through the runtime — the Console can never do anything the operator's cloud role cannot.
- Required field
- id — the tenant id to invoice.
- Required role
- admin or higher (ctx.role).
- Console screen
- /cloud/billing (admin+) lists invoices newest-first and hosts the Generate invoice form.
Worked example: generate an invoice#
Below is the underlying function call and its real response envelope, transcribed from the source. The tenant billme has 4 threads, 8 GB memory, 30 GB storage, 1 replica. Unlike the lifecycle functions, generate_invoice returns a custom payload (invoice_id, total, ok) rather than the standard tenant-state shape.
Request
cloud.generate_invoice
ctx = { user: "op", role: "admin" }
input = { id: "billme" }Response
{
"tenant_id": "billme",
"invoice_id": "inv-1751020800000000",
"total": "2.40",
"simulated": "PLANNED meter: usage synthesized from cap × period; a real meter streams per-tenant counters into _cp_usage",
"ok": true
}Output
Console flash (PRG redirect to /cloud/billing?flash=...):
Generated invoice for 'billme' — PLANNED meter: usage synthesized from cap × period; a real meter streams per-tenant counters into _cp_usage
(invoice_id's numeric suffix is the microsecond timestamp at call time; the value above is illustrative)The two rows the call wrote can be read back through the planner:
Request
SELECT id, tenant_id, period, total, status FROM _cp_invoices WHERE tenant_id = "billme"Response
[ {
"id": "inv-1751020800000000",
"tenant_id": "billme",
"period": "last 24h (simulated)",
"total": "2.40",
"status": "due"
} ]Simulated meter vs. real metering#
The invoice and usage rows are real and durable — they go through the same execute write funnel and planner gate as everything else in the control plane. What is simulated is the metering input: instead of measuring actual consumption, the function synthesizes a 24h period at the tenant's configured cap. A production meter would continuously stream per-tenant counters (CPU-seconds, memory GB-hours, storage GB, bandwidth GB) into _cp_usage, and invoicing would aggregate those real samples over a real billing period.
| Aspect | Today (simulated) | Planned (real) |
|---|---|---|
| Usage source | cap × fixed 24h period | streamed per-tenant counters |
| Period | "last 24h (simulated)" | real billing window |
| Bandwidth | always 0 in the sample | measured bw_gb |
| Invoice/usage rows | real + durable | real + durable (unchanged) |
| Payment | PLANNED (not collected) | PLANNED (not collected) |
Error conditions#
| Condition | Response code | Message |
|---|---|---|
| caller role below admin / unauthenticated | AccessDenied | cloud role 'admin' or higher required |
| missing or empty id | InvalidInput | missing required field 'id' |
| no such tenant | InvalidInput | no tenant '<id>' |