Assets & CMDB
The assets system provides a universal identity layer that maps all infrastructure objects — hosts, clusters, databases, caches — to a single entity model. This enables unified inventory views, compliance scoping, and cross-asset operations.
CMDB Phase 1 complete (hosts + integrations). Phase 2 complete (K8s clusters + 13 resource types). Database CMDB Phase 2 complete (dedicated databases + database_clusters tables). The asset model now covers 7 asset types: host, cluster, database, database_cluster, cache, web_server, and container_runtime.
Overview
Every infrastructure object in Proxima Console gets an asset record in the assets table. The asset acts as a thin universal layer with shared attributes (ownership, status, tags), while type-specific data stays in dedicated tables (hosts, clusters, etc.) or is captured in the asset's metadata JSONB column (for integration-derived assets).
Asset Types
| Asset Type | Source | ref_id Points To | Created By |
|---|---|---|---|
host | Agent inventory | hosts.id | Inventory worker (EnsureForHost) |
cluster | K8s collector | clusters.id | Kubernetes worker (EnsureForCluster) |
database | Agent heartbeat (PostgreSQL collector) | databases.id | Heartbeat worker (syncDatabases) |
database_cluster | Auto-cluster detection | database_clusters.id | Heartbeat worker (detectDatabaseCluster) |
cache | Agent heartbeat (Redis integration) | host_integrations.id | Heartbeat worker (EnsureForIntegration) |
web_server | Agent heartbeat (Nginx integration) | host_integrations.id | Heartbeat worker (EnsureForIntegration) |
container_runtime | Agent heartbeat (Docker integration) | host_integrations.id | Heartbeat worker (EnsureForIntegration) |
Asset Lifecycle
Assets are created and updated automatically — no manual intervention needed.
How Assets Are Created
Host assets are created by the inventory worker every time it processes a host inventory message from the agent. This happens on agent startup and every collection cycle (default: 5 minutes).
The compliance worker also creates assets as a safety net, using EnsureForHostMinimal — this covers edge cases where a healthcheck runs before the first inventory arrives.
Cluster assets are created by the kubernetes worker when it processes K8s inventory messages from the cluster agent. See Kubernetes Inventory.
Integration-derived assets are created by the heartbeat worker via the integration-to-asset bridge (see below).
Integration-to-Asset Bridge
When a host agent sends a heartbeat containing integration collector statuses (PostgreSQL, Redis, Nginx, Docker), the heartbeat worker automatically creates or updates CMDB assets for each integration. This bridges per-host integration monitoring into the unified asset layer.
The flow:
- Agent heartbeat arrives with
collectors[]array (type, name, version, status, enabled). - Heartbeat worker upserts each collector into
host_integrations. - For each collector, worker looks up the
host_integrations.idand callsEnsureForIntegration. - The asset is created with
ref_id = host_integrations.id, inheriting the host's tags and environment.
Integration-to-asset type mapping:
| Integration ID | Asset Type | Display Name |
|---|---|---|
postgresql | database | PostgreSQL |
redis | cache | Redis |
nginx | web_server | Nginx |
docker | container_runtime | Docker |
Unknown integration IDs fall through as-is (extensible fallback).
Asset naming: Integration assets are named "{DisplayName} on {hostname}" — e.g., "PostgreSQL on db-prod-01".
Metadata example for a database asset:
{
"engine": "postgresql",
"host_id": "a1b2c3d4-...",
"version": "16.2",
"collector_name": "postgresql-main"
}
Status Mapping
Asset status is derived from the source entity's lifecycle events.
Host assets:
| Event | Asset Status |
|---|---|
| Inventory received (agent online) | active |
| Stale detector marks host offline | unreachable |
| Heartbeat arrives after being offline | active (restored from unreachable; degraded/maintenance are not overridden) |
| Admin deactivates host via API | inactive |
| Admin activates host via API | active |
| Host deleted via API | decommissioned |
| Host row removed but asset orphaned (e.g. environment-cascade delete) | decommissioned (reconciled by the stale detector) |
Cluster assets:
| Cluster Status | Asset Status |
|---|---|
healthy | active |
degraded | degraded |
unreachable | unreachable |
unknown | active |
Integration assets:
| Condition | Asset Status |
|---|---|
| Integration enabled, collector healthy | active |
Integration enabled, collector status = error | degraded |
| Integration disabled | inactive |
Additional statuses:
maintenance— manually set during planned downtime
Data Model
Asset Fields
| Field | Type | Description |
|---|---|---|
id | UUID | Unique asset identifier |
asset_type | string | Type discriminator: host, cluster, database, database_cluster, cache, web_server, container_runtime |
ref_id | UUID | FK to the source entity (e.g., hosts.id, clusters.id, host_integrations.id) |
client_id | UUID | Owning client (multi-tenant scoping) |
environment_id | UUID | Optional environment scope |
name | string | Human-readable display name (hostname for hosts, cluster name for clusters, "PostgreSQL on host" for integrations) |
status | string | Lifecycle status (see above) |
tags | JSONB | Type-agnostic labels (synced from agent labels for hosts and integration assets) |
metadata | JSONB | Custom fields and annotations (populated for integration assets with engine, version, host_id) |
last_seen_at | timestamp | Last data collection timestamp |
created_at | timestamp | When the asset was first seen |
updated_at | timestamp | Last modification timestamp |
Host Linkage
Hosts have a reverse FK asset_id pointing back to their asset record. This is populated lazily by the inventory worker on first inventory.
-- Find the asset for a host
SELECT a.* FROM assets a WHERE a.asset_type = 'host' AND a.ref_id = :host_id;
-- Or via the reverse FK
SELECT a.* FROM assets a JOIN hosts h ON h.asset_id = a.id WHERE h.id = :host_id;
API Endpoints
All endpoints require authentication and the assets:read permission. Responses are scoped to the caller's allowed clients.
List Assets
GET /api/v1/assets
Query parameters:
| Parameter | Type | Description |
|---|---|---|
type | string | Filter by asset type (e.g., host, cluster, database) |
status | string | Filter by status (comma-separated: active,unreachable) |
client_id | UUID | Filter by client |
environment_id | UUID | Filter by environment |
search | string | Name search (case-insensitive substring match) |
page | int | Page number (default: 1) |
per_page | int | Items per page (default: 50) |
Response:
{
"data": [
{
"id": "a1b2c3d4-...",
"asset_type": "host",
"ref_id": "e5f6g7h8-...",
"client_id": "c1d2e3f4-...",
"name": "web-prod-01",
"status": "active",
"tags": {"env": "production", "role": "web"},
"metadata": {},
"last_seen_at": "2026-04-05T12:00:00Z",
"created_at": "2026-03-01T00:00:00Z",
"updated_at": "2026-04-05T12:00:00Z"
}
],
"meta": {
"total": 42,
"page": 1,
"per_page": 50
}
}
Get Asset
GET /api/v1/assets/:id
Returns a single asset. Returns 404 if the asset doesn't exist or belongs to a client the caller can't access.
Asset Summary
GET /api/v1/assets/summary
Returns counts grouped by type and status:
{
"data": [
{"asset_type": "host", "status": "active", "count": 38},
{"asset_type": "host", "status": "unreachable", "count": 4},
{"asset_type": "cluster", "status": "active", "count": 3},
{"asset_type": "database", "status": "active", "count": 5},
{"asset_type": "database", "status": "degraded", "count": 1}
]
}
Client-Scoped Assets
GET /api/v1/clients/:id/assets
Same query parameters as the main list endpoint (minus client_id). Returns only assets belonging to the specified client.
Environment-Scoped Assets
GET /api/v1/environments/:envID/assets
Same query parameters as the main list endpoint (minus client_id and environment_id). Returns only assets in the specified environment.
PQL Search
The asset.* prefix is available in PQL for searching assets and hosts by their asset attributes:
| Query | What it finds |
|---|---|
asset.type = "host" | All host assets |
asset.type = "database" | All database assets (PostgreSQL instances) |
asset.type = "cache" | All cache assets (Redis instances) |
asset.type = "cluster" | All cluster assets |
asset.status = "active" | Assets with active status |
asset.status = "degraded" | Assets with degraded status (integration errors, unhealthy clusters) |
asset.name ~ "web-*" | Assets with names starting with "web-" |
asset.tags.env = "prod" | Assets with env=prod tag |
Combine with other PQL fields:
host.os = "Ubuntu" AND asset.status = "active" AND asset.tags.tier = "production"
See the PQL Reference for full syntax documentation.
Asset Cleanup and Staleness
Assets are not automatically deleted when their source entity disappears. Instead, status transitions track the lifecycle:
- Hosts — The stale detector marks hosts as
unreachableafter a configurable timeout (default: 5 minutes). The asset status follows. - Clusters — The K8s maintenance worker marks clusters as
disconnectedwhen no inventory arrives for 15 minutes. The asset status is updated tounreachable. - Integration assets — Status is updated on every heartbeat. If the integration is disabled, the asset transitions to
inactive. If the integration collector reports errors, the asset transitions todegraded. - Decommissioning — When a host is deleted via the API, the asset transitions to
decommissionedand stays in the database for auditing.
Permissions
Two permissions control asset access:
| Permission | Granted To | Description |
|---|---|---|
assets:read | All system roles | View assets and summaries |
assets:write | Super Admin, Admin | Create and modify assets |
These permissions are added to system roles automatically via database migration. Custom roles can be configured to include either permission.
Database Schema
assets table
CREATE TABLE assets (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
asset_type TEXT NOT NULL,
ref_id UUID NOT NULL,
client_id UUID NOT NULL REFERENCES clients(id),
environment_id UUID REFERENCES environments(id),
name TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'active',
tags JSONB NOT NULL DEFAULT '{}',
metadata JSONB NOT NULL DEFAULT '{}',
last_seen_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(asset_type, ref_id)
);
Key indexes: client_id, environment_id, (client_id, asset_type), status, GIN on tags, trigram GIN on name, last_seen_at.
Cluster Assets
Kubernetes clusters are the second asset type in the CMDB, added in CMDB-2a. Each cluster gets an assets record with asset_type = 'cluster' and ref_id pointing to clusters.id.
Cluster assets appear alongside host assets in the unified views:
GET /api/v1/assets?type=cluster— Filter the asset list to show only clustersGET /api/v1/assets/summary— Summary now includes{"asset_type": "cluster", "status": "active", "count": N}rows
The cluster status is mapped to an asset status when the KubernetesWorker processes inventory messages:
| Cluster Status | Asset Status |
|---|---|
healthy | active |
degraded | degraded |
unreachable | unreachable |
unknown | active |
See Kubernetes Inventory for full details on the cluster data model and API endpoints.
Database Assets
Database instances and database clusters are the latest asset types in the CMDB, added in Database CMDB Phase 2. Each PostgreSQL instance gets an assets record with asset_type = 'database' and ref_id pointing to databases.id. Logical groups (primary + replicas) get a database_cluster asset with ref_id pointing to database_clusters.id.
See Database Inventory for full details on topology detection, auto-cluster grouping, and API endpoints.