version. Deletes are tombstones, not physical removal. A stale write is a no-op in storage and a 409 on the HTTP API.
The API modality is state.
→ All modalities
When to use it
Use state when- You have one current document per device (or a small keyset), not thousands of child rows.
- Cloud-authored changes must reach the device (downsync).
- You need GET after PUT,
If-Match/ ETag, or a change event that cannot be lost if the process crashes. - Safety-adjacent values (shadow, interlock, “armed”) must be recorded and propagated. The local device loop still owns hard-real-time trip; the platform owns durable record + fan-out.
UPDATE … WHERE status = 'running') in a dataspace. That belongs in ordinary platform tables.
Backing store
State lives in Postgres as an ordinary table in the project dataspace schema. It is never ClickHouse and never tiered to object storage. You cannot sethot_cold or storage_class on state (rejected).
That shared transaction is why state stays on Postgres. A ClickHouse-backed “state” would put the row in one system and the event in another. For values you will act on, that regression is not acceptable.
Identity
source_id is the producer (device id on the edge path, user/connection on the API path). It is a UUID.
A business key — machine_id, (line_id, station_id), SKU — belongs in natural_key, declared at create as an ordered list of field paths. Order is significant; reordering is a different identity.
- No
natural_key→ one live row persource_id(classic shadow). - With
natural_key→ one live row per(source_id, natural_key). HTTP{row_key}is the natural key, not the UUID.
POST …/dataspaces/{id}/rekey). Changing an existing key can merge rows that are distinct today; that is refused. Create a new dataspace instead.
Meta columns
State does not expose
ingest_ts. Overflow JSON is readable on a hybrid state dataspace (the only modality where the overflow blob is a query surface).
version must be stable across retries. Use the producer’s logical version (for example edge commit_seq). Do not default to wall-clock now: a redelivery would look newer and last-write-wins would become last-arrival-wins.
Schema and layouts
Default layout ishybrid: declared columns are typed; undeclared keys land in overflow and stay readable. That default exists because Postgres schema does not auto-grow. Nothing infers new columns from overflow the way ClickHouse telemetry promotion does.
Schema changes go through the control API (proposals / review when sync is attached). The write plane will not
ALTER state tables on its own. Column cap is 1000. Over-cap or typed mismatch → orphan + error to the caller.
HTTP row CRUD
State is the only modality with synchronous JSON row APIs:{row_key} is the declared natural key, or source_id when there is none.
Predicate
PATCH/DELETE use a structured filter (equality, in, ranges on declared columns). Raw SQL WHERE is not accepted. An empty filter is rejected; whole-dataspace updates must set "where": {"all": true}. Each changed row gets its own version bump and its own outbox event. max_rows caps fan-out.
Change events and edge
On a successful commit the write plane emits a row-changed event with the full row (deletes carry identity + version). Delivery is at-least-once. Idempotency key for consumers: dataspace + source + version (and natural key when present). Edge:- Upsync and downsync. A cloud write to a sync-tagged state dataspace fans out to devices.
- Authority is per table:
device(cloud is a replica) orcloud(cloud is source of truth). - Offline upsync is safe: an older
versionis a no-op. - Attaching any sync target forces schema promotion policy = review.
Joining state into analytics queries
State lives in Postgres and your history lives in the analytics engine, but an analytics query can read a state dataspace directly, in the same query, without copying anything. That closes the gap that used to force you to duplicate current values into a telemetry dataspace just so a report could see them. You can now write the rollups people actually ask for:- Average reading per machine, labelled by that machine’s current mode rather than by an id.
- Yesterday’s throughput per line, restricted to the lines whose current state is
running. - A daily report that pairs history with the settings in force when it runs.
Reference the dataspace by name, as you would anywhere else. Physical table names are not a supported query surface.
On a self-hosted deployment this connection is configured once at install time. If a query that joins state against history returns rows from the history side and nothing from the state side, that connection is the first thing to check.
Create
One row per device (shadow-style):Related
- Dataspaces overview
- Schema and layouts
- Mutable — many current rows per device, ClickHouse
- Document · Vector — the two content modalities built on this same keyed-row shape
- Edge data sync
- API introduction