Skip to content

Data model

Schema as of migration 0001, the base every later migration builds on; the head is 0025 (services/api/alembic/versions/), which added among other things entity_types.parent_id (ADR 0025), location_source and location_fallback_hours on entities and devices with the network record type (ADR 0024), the satellite session columns (ADR 0023), entity groups (ADR 0020) and backup_runs (ADR 0016). Every time column is TIMESTAMPTZ in UTC. Domain objects have UUID primary keys; time-series rows have bigint identities with a composite primary key that includes the time column (decision D31).

Access control

Table Purpose
users Accounts, FastAPI-Users compatible. is_superuser is the server admin flag. password_changed_at invalidates older tokens.
organizations Reserved tenant boundary, not enforced (D21)
projects Access control and operational grouping
project_memberships One role per user per project
invitations Registration is by invitation; the token proves the email
audit_log Who did what to which object, with request and trace ids

Domain

Table Purpose
entity_types Kinds of entities with icon key and JSON schema for attributes
entities The monitored objects, per project, optional geometry
features Sites, zones, geofences, routes with PostGIS geometry
device_types Families with driver key and capabilities
devices Hardware, server level
device_project_assignments tstzrange validity, GiST exclusion per device
device_entity_assignments Same, per device
data_sources External platform accounts, encrypted credentials, capabilities, link templates
data_source_project_scopes Optional project scoping and auto-assign flag
external_identities (data source, external id) to device, null device while unknown
metrics Registry of metric keys with unit, value type and category
device_log_files Raw log files and browser syncs as managed assets: file in the log files bucket, SHA-256 unique per device, status, frame and record counts, period, firmware, the file's trace (architecture 25.6)

Time series (hypertables)

Table Partition column Compression segment Retention
source_events ingested_at data_source_id 730 days by default
positions time device_id none
measurements time device_id none
gateway_receptions time gateway_id none
device_state_history time device_id none

Chunks are 7 days (30 for state history). Compression starts after 7 days. TimescaleDB does not allow foreign keys to a hypertable, so references from canonical rows to their source event carry source_event_id and source_event_ingested_at as plain columns.

Current state lives in regular tables that are updated in the same transaction as the canonical rows: device_current_state, entity_current_state and connectivity_state (per device and data source).

Events and rules

events are domain facts with type, severity, optional geometry and context. alerts are events that need a person, with an open, acknowledged, resolved lifecycle. rules and rule_versions hold versioned rule documents; every event references the version that produced it. Rules are filled in during phase 5.

Traces

processing_traces (one per flow, with class and compact flag), processing_steps (ordered steps with timing, references and an error id) and application_errors (stable error code, severity, retryable and user-actionable flags, technical context).

Indexes

From measured needs only, as the architecture asks. Migration 0001 creates: time plus device, entity and project on positions and measurements (with metric key), GiST on every geometry, unique canonical key plus time on positions and measurements, and the lookups that the admin API and the pipeline use (external id per source, processing status, trace id). BRIN indexes on time were held back for the phase 4 benchmark to justify; it did not (every read path stayed inside budget at 0.2 of the envelope), so there are none. They remain the first thing to try if a time scan becomes expensive.

Enumerations

Enumerated columns are text with a check constraint generated from a StrEnum in shared/enums.py, not PostgreSQL enum types, so a new value is a normal migration.