Skip to content

Domain model

The concepts every screen and every service is built on. Table names are in docs/architecture/data-model.md.

Device versus entity

A device is hardware: an OpenCollar, a gate sensor, a weather station. It exists at server level and has no project column. An entity is the real-world object you care about: an animal, a vehicle, a gate, a weather station as a place. Entities belong to a project and have a type (entity_types) that carries the icon and an attribute schema, so administrators add new kinds of entities without a code change. Types hold sub-types one level deep (ADR 0025): Wildlife holds Elephant, Vehicles holds 4x4, and an entity references the most specific row, so the icon and the EarthRanger subject subtype follow. Every server is seeded with the standard catalogue of six types and a sub-type per icon; a project hides what it does not need under Project admin, Settings, and an entity may override its type's icon.

The two are linked by assignments with a validity range:

  • device_project_assignments: which project owned the device from when until when.
  • device_entity_assignments: which entity the device monitored from when until when.

Both use a half-open range [start, end) and a database exclusion constraint, so a device can never be in two projects or on two entities at the same moment. A device may carry no assignment while it sits in inventory or repair.

Attribution uses canonical time

Every canonical record (position, measurement, state, event, contact) is attributed to the project and entity that were assigned to the device at the device-origin time of the record, not at the moment the record arrived. A raw log uploaded on 20 August that contains a GPS fix from 15 July belongs to the project that owned the device on 15 July. The single function that answers this is shared.domain.assignments.resolve_attribution. The resolved ids are stored on the record for fast queries; the assignment tables stay the source of truth for audits and recomputation.

Assignment dates and records before an assignment

An assignment starts at a moment you choose. The dialogs offer the moments the system knows: the device's first data (its earliest record, sighting or raw log), the day it joined the project, now, or a date; a date means the start of that day in the project's timezone. Records that arrived before the device's earliest assignment have no project or entity and stay invisible to the project (a raw log downloaded weeks after the collar went out is the usual case). The device page says how many such records exist and offers to move the assignment start back to the first data; that rewrites the attribution of the records in between and recomputes the current state. The same repair exists for entity assignments once the project assignment covers the period. Creating an assignment, or moving a device, attributes the records already inside the new range the same way, so choosing "since the device's first data" at creation needs no repair afterwards. The repair and the move run in the background as a job the page follows; another assignment change made meanwhile follows in a job of its own, nothing waits.

The entity is where most people start, so the entity page carries the device side too: the device tracking it today with its health, the history of every device that tracked it, "Assign device" for project admins (the candidates are the project's devices that track nothing at that moment, GET /devices?project_id=&unassigned=true) with the same start choices, and "Release device", which ends the current assignment now. A device already on another entity is released there first; the platform never moves a device between entities silently.

Groups

Groups are folders of entities inside a project, nested as deep as needed (decision D98, ADR 0020): a region, a herd inside it, a family inside that. An entity sits in at most one group; a device belongs to the group of the entity it tracks today, so the devices list filters by group as the entities list does, and filtering by a group includes everything below it. Project admins organize them under Project admin, Groups: the tree on the left, the entities on the right with search and a type filter, a selection moved with "Move to" (which can create the target group on the spot) or dragged onto a group; the Entities list moves a selection the same way. A group has a colour for the map; the entity dialog and the bulk onboarding from Needs attention place entities in a group. Deleting a group leaves its entities in place and ungrouped. On the live map the layers panel has five tabs, Entities, Devices (the device layer, off by default, with or without an entity and a dashed track), Features, Events and Coverage (the gateways with a position, the heard positions and the network locations). Entities lists the groups however deep with the number of entities on the map, each entity with its icon and how long ago it was seen, a search box, a sort by name or last update and a grouped or flat view; every group and entity shows or hides, "only" keeps one group, one toggle per tab shows or hides everything on it beside Fold all, and a row's locate button flies to it. Features and Events do the same per feature type and feature, and per event type, for the last 24 hours; Coverage shows the gateways with a position, the heard positions and the network locations; a track button next to an entity shows its track over the length set on the Tracks card (24 hours by default), the same button the object's panel carries, a heatmap button beside it does the same for a heatmap, and a signal button shows its heard positions (a gateway row has the same for what the gateway heard); the choices are kept per user and project (users.preferences), so another browser opens the same view.

Moving between projects

A device moves to another project from a moment you choose: its first data, the moment it joined its current project, a date, or now. From that moment the device belongs to the new project: the old assignments are cut there, a new one opens, and the records from that moment on, positions, measurements, contacts, state history, events with their alerts, log files and commands, get the new project in the background, the way an assignment repair does. What lies before the moment stays with the old project, whose members keep seeing it; when nothing lies before, the device leaves that project for good. "From now" is a handover: the old project keeps everything until now and sees no new data.

An entity comes along when its whole history moves: every device that ever tracked it is in the move and started tracking it at or after the moment. It keeps its name, its history, its current state and its device assignments, and leaves its group (or joins a group of the new project you choose). An entity that keeps history in the old project, because another device tracked it or this device tracked it before the moment, stays there; the moving device's assignment to it ends at the moment and the device arrives without an entity, which the dialog says before you confirm.

An entity moves whole from its own page or from a selection on the Entities page: its history and its devices come along over the spans they tracked it, so a device that later tracked another animal of the old project keeps that part there (its project assignment is split around the span). A name the new project already has blocks that entity.

Every move shows a preview first, per device and per entity: from when, what comes along, what stays and why, what is skipped. The move runs the same plan. It needs a server admin, or a project admin of every project involved. Moving back does not undo a move: the history follows again.

Correcting a boundary after the fact

A transfer stamps the moment it was made; the moment the device really changed hands is often known later, for example one second after its last uplink in the old project. The device page's Assignments tab lists the device's project assignments and entity assignments, the current one first, and "Edit" on a row changes its dates: the edge of that row and the touching edge of its neighbour move together, so the history keeps no gap and no overlap, and the records between the old and the new moment are attributed again to the project and entity of that time, in the background as every assignment change is. Raw messages never change. The dialog takes From (and To, for a row that ended) as wall-clock times in the project's timezone, offers the device's last live message before the current start ("Use the last uplink": one second after it, so that message stays where it was), takes an optional note, and says in one line what else moves before anything is written. A change is refused when the moment lies in the future, would leave an assignment empty, would cross another assignment, or would put an entity assignment outside the device's project. Where the project and the entity changed at the same moment, the dialog offers, ticked, to move the other assignment along. Every change is recorded with who, when and the note, and the row shows its last change. A project boundary needs a server admin or a project admin of every project involved; an entity boundary needs devices:write in the entities' projects. AI clients can do the same under the AI action policy, with the person's confirmation (decisions D355 to D358, ADR 0041).

Data sources and external identities

A data source is an external platform account: a ChirpStack instance, a KPN ThingPark account, a Traccar server. It carries the adapter that talks to it, its capabilities, encrypted credentials and deep link templates. A data source may be scoped to one or more projects, which is a configuration aid and never an assumption.

An external identity maps (data source, external id), such as a DevEUI, to a device. Incoming data is resolved through this pair. An unknown identity is kept with its source events and shown in Needs Attention; it is never guessed.

Four data levels

Level Where What
Raw source_events The inbound message exactly as received, immutable
Decoded driver output Provider or device specific interpretation
Normalized positions, measurements, device_state_history, events, device_contacts Canonical rows with stable schemas
Aggregated server-side buckets Time buckets and statistics for charts and dashboards

Maps, charts, rules and exports use the normalized level. Provenance always leads back to the raw level.

Processing traces

Every flow through the system (an inbound message, a command, an import, a delivery, an export) has a processing trace with ordered steps. A step that fails records a structured application error with a stable code, a severity, and whether a retry or an administrator can fix it. Routine successful telemetry writes compact traces.

Roles and permissions

Three tiers: server admin (an account flag), project admin and project viewer (a membership row per project). Fine-grained permission keys such as devices:control are defined in code and mapped from the role. See permissions.

Positions and where they come from

A position has a record type. The device's own fixes carry the driver's type (gnss for an OpenCollar). A location a network provides about the device, an Iridium session's estimate or a LoRaWAN network's geolocation, is a position of type network with the network's radius as its accuracy (ADR 0024). Every map, chart, list, rule and export shows the device's own fixes unless a person asks for the network's locations, and the current position of an entity or a device follows its location source setting: the device's fixes, the network's locations, or the device with the network standing in after a period without a fix. Network locations draw on the live map's Coverage tab as circles of their radius. A change of the setting rebuilds the current position at once from the positions held, so it does not wait for the next position to arrive.

Two more kinds exist since phase 30 (ADR 0036). A device that does not move can be given a place by hand on its page: its location source becomes static and nothing it sends moves it afterwards. A sighting by such a placed device writes a position of type proximity for the device it heard, at the reader, with a stated radius, which for an animal wearing only a Bluetooth tag is the only position there will ever be. An estimate never displaces a newer fix a device made itself, but a device that has never fixed at all takes it whatever its location setting says. The map panels name every kind that is not the device's own fix, so an estimate is never read as one. Every such kind is named once, in ESTIMATE_RECORD_TYPES, and readers test against that set rather than against a single name: a reader that knows only network treats the other kinds as fixes, which is how a tag came to look as though it had fixed itself at the reader that heard it. See Bluetooth contacts.