Skip to content

Entity model

Devices on their own are a flat list. The entity model is how CORE-M gives them structure and shares them safely: assets group and locate devices, typed relations connect everything into a navigable graph, and customers get tightly-scoped, sub-tenant access through entity views and dashboard assignments.

Everything here is tenant-scoped and stored in Aerospike namespace devices.

An asset is a first-class entity representing a logical or physical grouping — a site, building, production line, machine, vehicle, or meter. Assets live in devices/assets, keyed by {tenant_id}:{asset_id}, and carry name, type, label, tags, metadata, status, and version.

  • Unique name per type per tenant. Creating a second site named “Factory 1” in a tenant that already has one is rejected with ALREADY_EXISTS (and no event is published). The same name under a different type is fine.
  • Queryable by type and tag. You can list assets filtered by type and tag (e.g. type=site, region=eu), paginated with next_page_token.
  • Soft delete. Deleting an asset sets status="deleted"; it is excluded from normal lists but retained for audit and retrievable with include_deleted=true, which returns deleted_at and deleted_by.

Creating an asset publishes entity.asset.created.T1.{asset_id} and writes an audit event.

Relations connect entities — devices, assets, customers, dashboards, and entity views — into a directed graph. Each relation has a type and a direction, stored in devices/relations keyed by {tenant_id}:{from_type}:{from_id}:{relation_type}:{to_type}:{to_id}.

Relation typeTypical use
containsAn asset contains sub-assets or devices
managesOne entity manages another
assigned_toA dashboard assigned to a customer/user
located_atA device located at an asset
monitorsA device monitors a target
depends_onA dependency edge between entities

Creating A1 --contains--> D1 writes a relation record, builds reverse-lookup indexes (so you can query “devices contained by A1”), and publishes entity.relation.created.T1.A1.D1.

For relation types where cycles are forbidden (such as a contains hierarchy), the system rejects edges that would close a loop. If A1 contains A2, an attempt to create A2 --contains--> A1 is rejected with FAILED_PRECONDITION and the response identifies the cycle path, so you can see exactly which edges conflict.

Relations are navigable in both directions and across depth. Asking for the descendants of A1 filtered to entity_type=device — given A1 contains A2 and A2 contains D1, D2 — returns D1 and D2, each with its relation path back to A1. That path is what dashboards and enrichment nodes use to render and reason about hierarchy.

flowchart TD
  A1["Asset: Factory 1<br/>(site)"] -->|contains| A2["Asset: Line A<br/>(line)"]
  A1 -->|contains| A3["Asset: Line B<br/>(line)"]
  A2 -->|contains| D1["Device: Boiler 1"]
  A2 -->|contains| D2["Device: Boiler 2"]
  A3 -->|contains| D3["Device: Press 1"]
  D1 -.located_at.-> A2

A customer is a tenant-scoped entity that receives restricted access — think of an end client of your tenant who should see only their own equipment, never the whole tenant. Customers live in devices/customers keyed by {tenant_id}:{customer_id}.

Customer users are auth users with authority customer_user and one or more customer memberships. Their access is fundamentally narrower than a member:

Inviting a customer user creates an auth user with authority="customer_user", assigns them to the customer, and audits the invite.

An entity view is the mechanism that grants a customer access to a specific slice of an entity’s data — and nothing more. Views live in devices/entity_views keyed by {tenant_id}:{view_id} and carry entity_type, entity_id, customer_id, field_mask, telemetry_key_allowlist, start_time, end_time, and version.

Three controls define the slice:

  • field_mask — which fields of the entity are visible. A view that masks out config and API key means a customer reading device D1 through it sees no config, no API key, no credentials, no internal metadata, and no unrelated telemetry keys.
  • telemetry_key_allowlist — which metrics are readable. A view allowing ["temperature", "humidity"] lets the customer query only those keys for the device.
  • start_time / end_time — the validity window.

Dashboards can be assigned to customers (and users) with view or edit permission via an assigned_to relation.

  1. Assign. Assigning dashboard DB1 to customer C1 with permission="view" creates DB1 --assigned_to--> C1. Customer users for C1 now see DB1 in their list but cannot edit it.

  2. Revoke. Revoking deletes the relation. Active WebSocket sessions for C1 immediately receive a dashboard_access_revoked event, and subsequent dashboard API reads return PERMISSION_DENIED.

So revocation is real-time: an in-flight viewer doesn’t keep the dashboard open until they refresh — their live session is told to drop access at once.

Assets, customers, relations, and entity views all support soft delete with audit history. Deleting a customer is the broadest case: it sets status="deleted", disables the customer’s assignments and entity views, drops customer-user access at their next request, and writes an audit event listing the disabled entity_view_ids and dashboard_ids. Deleted entities are retained for audit and excluded from default list queries.

flowchart LR
  cust["Customer C1"] -->|has| cu["Customer user CU1"]
  ev["Entity view EV1<br/>field_mask + allowlist + expiry"] -->|customer_id| cust
  ev -->|entity_id| dev["Device D1"]
  db["Dashboard DB1"] -->|assigned_to| cust
  cu -.reads through.-> ev
  cu -.sees.-> db

A customer user reaches data only along these edges: through an entity view that names their customer and a device, or through a dashboard assigned to their customer. Anything outside that path is denied.