Skip to content

Qirava Cloud docs

Control plane data model (_cp_* catalogs)

Qirava Cloud is itself a Qirava DMS — "a DMS that manages other DMSes." Everything it knows about the fleet (customers, machines, plans, money, and an audit trail) lives in seven schemaless catalog tables whose names all start with _cp_. These tables live in the control DMS's OWN store, completely separate from any tenant's _sys_* catalogs: a tenant never sees them, and the control plane never reaches into a tenant's data — it only allocates resources and tracks lifecycle here. Reads go through the planner; writes go through execute; the same L1→L3 funnel the open-source core enforces. This page documents each catalog, its exact row shape, and how the data is created and read.

What the `_cp_*` catalogs are#

There are exactly seven control-plane catalogs. They are created on first boot and are entirely distinct from a tenant's _sys_* system tables. The control plane is the only thing that ever touches them; tenants are isolated and never see them. Because they hold lifecycle and resource bookkeeping (not customer data), the control plane being a separate authority domain from any tenant is preserved at the storage level.

ConstantTable namePurpose
CP_TENANTS_cp_tenantsOne row per managed DMS instance (the customer's box)
CP_NODES_cp_nodesOne row per bare-metal node the placer bin-packs against
CP_PLANS_cp_plansThe per-unit pricing plan (the slider IS the plan)
CP_SUBSCRIPTIONS_cp_subscriptionsLinks a tenant to a plan
CP_USAGE_cp_usageMetered usage samples (the meter's stream)
CP_INVOICES_cp_invoicesGenerated invoices
CP_AUDIT_cp_auditAppend-only trail of every control-plane action

Why schemaless#

Every _cp_* table is created SCHEMALESS. The control plane owns the row shape in code, so it can evolve a row's fields (add a column, change a default) without a DDL migration. The shapes below are the fields the code reads and writes today; a schemaless row may carry exactly these keys and nothing forces a column to be present (missing string fields read as empty, missing integers read as 0).

rust
// From cp.rs — the create is idempotent: a table is only created when absent.
for t in CP_TABLES {
    if !existing.contains(*t) {
        let _ = db.execute(&format!("CREATE TABLE {t} SCHEMALESS"));
    }
}

Row shapes (field by field)#

Each catalog's exact row shape, transcribed from the source. (standalone|cluster) and (dynamic|fixed) denote the only accepted values for that field.

_cp_tenants
{ id, owner, plan, mode(standalone|cluster), storage(dynamic|fixed), threads, mem_gb, storage_gb, replicas, status, node_id, created_us }. status is one of active, pending_capacity, suspended, terminated. node_id is the placed node (empty when no headroom). plan is always "metered". replicas is 1 for standalone, ≥2 for cluster.
_cp_nodes
{ id, provider, cap_threads, cap_mem_gb, cap_storage_gb, alloc_threads, alloc_mem_gb, alloc_storage_gb, status }. cap_* is the node's total capacity; alloc_* is how much the placer has reserved; status is online/offline.
_cp_plans
{ id, label, price_thread_hr, price_mem_gb_hr, price_storage_gb_mo, price_bw_gb }. Prices are stored as strings (one source of truth for billing).
_cp_subscriptions
{ id, tenant_id, plan_id, status, created_us }. id is sub-<tenant_id>; status is active or cancelled.
_cp_usage
{ id, tenant_id, ts_us, thread_sec, mem_gb_hours, storage_gb, bw_gb }. A metered sample; id is use-<now_us>.
_cp_invoices
{ id, tenant_id, period, total, status, created_us }. id is inv-<now_us>; total is a string dollar amount; status is due (or paid).
_cp_audit
{ id, ts_us, operator, action, tenant_id, node_id, result }. id is aud-<now_us>. One row per control-plane mutation.

The planner reads, `execute` writes#

The control plane reaches its catalogs exactly the way any client reaches a DMS. Reads are planner queries (read_records / read_records_bound); writes are execute statements (the L1→L3 write funnel). The console is a CLIENT — it never bypasses this. The helpers in cp.rs are thin wrappers around those two paths.

  • all_rows(db, table)SELECT * FROM <table> (planner read; surfaces every schemaless field).
  • row_by_id(db, table, id)SELECT * FROM <table> WHERE id = :id LIMIT 1 (bound query).
  • row_absent(db, table, id)SELECT id FROM <table> WHERE id = :id LIMIT 1 returns empty (used for idempotent seeds/inserts).
  • Every catalog mutation is a db.execute("INSERT …") or db.execute("UPDATE …").

Default seed (idempotent)#

On boot, seed_defaults inserts the single per-unit plan and two simulated bare-metal nodes, but only when each row's id is absent — so it is safe to run on every restart. The two nodes are the SIMULATED fleet: a real deployment registers actual OVH/Hetzner nodes; here they model the headroom the placer reasons about.

Request

INSERT INTO _cp_plans { id: "metered", label: "Pay-as-you-go (per unit)", price_thread_hr: "0.012", price_mem_gb_hr: "0.006", price_storage_gb_mo: "0.10", price_bw_gb: "0.02" }

INSERT INTO _cp_nodes { id: "node-eu-1", provider: "hetzner", cap_threads: 64, cap_mem_gb: 256, cap_storage_gb: 8000, alloc_threads: 0, alloc_mem_gb: 0, alloc_storage_gb: 0, status: "online" }

INSERT INTO _cp_nodes { id: "node-eu-2", provider: "ovh", cap_threads: 32, cap_mem_gb: 128, cap_storage_gb: 4000, alloc_threads: 0, alloc_mem_gb: 0, alloc_storage_gb: 0, status: "online" }

Response

-- after seeding, reading back the plan:
SELECT * FROM _cp_plans
-- yields exactly one row:
{ id: "metered", label: "Pay-as-you-go (per unit)",
  price_thread_hr: "0.012", price_mem_gb_hr: "0.006",
  price_storage_gb_mo: "0.10", price_bw_gb: "0.02" }

Output

SELECT * FROM _cp_nodes  -- two rows: node-eu-1 (hetzner), node-eu-2 (ovh), both online, alloc_* all 0

A real provisioned tenant row#

This is the row a cloud.provision call writes, transcribed from the provision flow. After provisioning a standalone tenant acme with 2 threads / 4 GB / 20 GB, the catalog holds:

Request

-- provision (executed internally by cloud.provision):
INSERT INTO _cp_tenants { id: "acme", owner: "acme-corp", plan: "metered", mode: "standalone", storage: "dynamic", threads: 2, mem_gb: 4, storage_gb: 20, replicas: 1, status: "active", node_id: "node-eu-1", created_us: <now> }
INSERT INTO _cp_subscriptions { id: "sub-acme", tenant_id: "acme", plan_id: "metered", status: "active", created_us: <now> }

-- read it back:
SELECT * FROM _cp_tenants WHERE id = :id LIMIT 1   (:id = "acme")

Response

{ id: "acme", owner: "acme-corp", plan: "metered",
  mode: "standalone", storage: "dynamic",
  threads: 2, mem_gb: 4, storage_gb: 20,
  replicas: 1, status: "active", node_id: "node-eu-1",
  created_us: <microseconds> }

Output

-- the placed node's allocation is bumped to match the cap:
SELECT alloc_threads FROM _cp_nodes WHERE id = "node-eu-1"  -- 2