Data Backend
IronFlock provisions a private database for each project, powered by TimescaleDB. Your app defines the data schema; IronFlock creates the tables and starts collecting data the moment a device is added to the app.
How It Works
- Define your data schema in
.ironflock/data-template.yml. - Use the IronFlock SDK to publish data from your edge code.
- IronFlock automatically sets up the database tables in each project where the app is installed.
- Data flows from devices through the messaging system into the project database.
Each project gets its own physical database — there is no data sharing between projects.
The user has full control over the data collected by your app in their project. As a developer, you do not have access to this data.
Defining the Data Schema
Create a data-template.yml file in the .ironflock/ directory:
data:
tables:
- tablename: sensordata
columns:
- id: tsp
name: Timestamp
description: Timestamp of measurement
path: args[0].timestamp
dataType: timestamp
- id: temperature
name: Temperature
description: Temperature reading in Celsius
path: args[0].temperature
dataType: numeric
- id: humidity
name: Humidity
description: Relative humidity percentage
path: args[0].humidity
dataType: numeric
- id: device_id
name: Device ID
description: Source device identifier
path: args[0].device_id
dataType: stringColumn Options
| Field | Description |
|---|---|
id | Internal column identifier (use tsp for timestamp columns) |
name | Human-readable column name shown in boards |
description | Optional description |
path | Path to the value in the published data object (e.g., args[0].temperature) |
dataType | One of: timestamp, numeric, string, boolean |
Table Options
Besides columns, a table takes a few optional keys that control how it is described and how its data ages:
data:
tables:
- tablename: sensordata
description: Environmental readings from the shop floor
chunkTimeInterval: 1 hour
dropAfter: 30 days
columns:
# ...| Field | Description |
|---|---|
tablename | Name of the table |
description | Optional description, shown in the UI and used by AI agents to understand the table |
chunkTimeInterval | Size of the time partitions the table is split into. Defaults to 7 days |
dropAfter | Retention window — partitions older than this are dropped automatically |
downsample | Maintain a pre-aggregated copy for fast long-window charts — see Continuous Downsampling below |
maintainLatestFlagFor | Columns identifying a unique entity — see Tracking the Latest State of an Entity below |
private | Hide this table from other apps — see Sharing Data with Other Apps below |
chunkTimeInterval controls how the time-series data is partitioned on disk. Pick it so that one partition roughly matches what you query at once: high-frequency data collected every second benefits from small chunks (minutes to hours), slow-moving data from large ones (weeks). This is only the app’s default — the project owner can adjust it later on their own data backend.
dropAfter turns the table into a rolling window. Whole partitions older than the given interval are dropped, which is far cheaper than deleting individual rows. The cleanup job runs on a dropAfter / 4 schedule, so a record can outlive its expiry by up to a quarter of the interval before its partition goes. Omit dropAfter to keep data indefinitely.
Both take PostgreSQL interval strings — 30 minutes, 1 hour, 7 days, 6 months.
Continuous Downsampling
Dashboards can ask the database to aggregate data — hourly averages, daily totals, counts per machine. Computing those from raw records is fine for a day and expensive for a year. Add downsample to a table and the platform maintains a continuously updated, pre-aggregated copy of it, then answers long-window queries from that copy instead:
data:
tables:
- tablename: sensordata
dropAfter: 30 days
downsample:
bucket: 1 minute
keepFor: 2 years
paths:
- payload.temperature
columns:
# ...| Field | Description |
|---|---|
bucket | Granularity of the pre-aggregated copy. Defaults to 1 minute |
keepFor | How long to keep the downsampled history. Omit to keep it indefinitely |
paths | JSON field paths to include, in the same notation dashboards use |
bucket is the finest resolution a chart can be served from — a chart asking for buckets much finer than this reads the raw table instead. It accepts fixed-width intervals from 1 second to 1 day that divide a day evenly (1 minute, 5 minutes, 1 hour). The default of 1 minute suits practically every dashboard; a coarser bucket costs less storage and less write throughput.
keepFor is what makes long histories possible at all. Raw records disappear with dropAfter, but the downsampled copy has its own retention: keep raw data for 30 days and downsampled data for 2 years, and a board can still chart two years of hourly averages on a fraction of the storage. Set it longer than dropAfter — the platform rejects the opposite as a misconfiguration.
paths extends downsampling to values inside JSON columns. Numeric columns are included automatically; JSON fields must be named explicitly, since a JSON column has no fixed set of keys. Undeclared fields still work in dashboards — they are simply computed from the raw table.
Everything else about it is automatic. Every numeric column gets its statistics maintained (average, sum, minimum, maximum, first, last and a record count), grouped by the table’s entity key (maintainLatestFlagFor, or the publishing device). Dashboards need no configuration and no awareness of it: a widget queries as usual, and the platform decides per query whether the pre-aggregated copy can answer it — falling back to the raw table transparently when it cannot, for example when a filter references a column the copy does not group by.
Schema changes rebuild the copy. Adding, removing or retyping a column of a downsampled table — or editing the
downsampleblock itself — rebuilds the pre-aggregated copy from the raw table. Anything older thandropAftercannot be reconstructed and is lost. Set the block up together with the table where you can, and treat later schema changes on long-lived tables as a deliberate decision.
Publishing Data from Edge Code
Use the IronFlock SDK to send data from your app:
Python
from ironflock import IronFlock
flock = IronFlock()
flock.publish_to_table("sensordata", {
"timestamp": "2025-01-15T10:30:00Z",
"temperature": 23.5,
"humidity": 62.1,
"device_id": "sensor-001"
})For high-frequency data, send many rows in a single message instead of one round-trip per row using publish_rows_to_table / publishRowsToTable (fire-and-forget) or append_rows_to_table / appendRowsToTable (returns the insert outcome). Each batch is inserted atomically — all-or-nothing. See the SDK reference for details.
Transform Tables
You can define SQL transforms that automatically aggregate or process your raw data:
data:
tables:
- tablename: sensordata
columns:
# ... raw data columns ...
transforms:
- tablename: hourly_averages
materialize: true
schedule: "0 * * * *"
sql: >
SELECT
time_bucket('1 hour', tsp) AS hour,
avg(temperature) AS avg_temp,
avg(humidity) AS avg_humidity
FROM sensordata
GROUP BY hour
columns:
- id: hour
name: Hour
dataType: timestamp
- id: avg_temp
name: Average Temperature
dataType: numeric
- id: avg_humidity
name: Average Humidity
dataType: numeric| Field | Description |
|---|---|
tablename | Name of the derived table |
materialize | If true, results are persisted as a table |
schedule | Cron expression for when the transform runs |
sql | SQL query that computes the transform |
columns | Column definitions for the output |
Transform tables are accessible in boards and via the SDK, just like regular tables.
Tracking the Latest State of an Entity
For tables that represent the current state of real-world entities — machines, assets, production orders — IronFlock supports a pattern called latest-state tracking.
Instead of overwriting a row when something changes, you always append a new row. You declare which columns identify a unique entity, and IronFlock derives the most recent row per entity whenever the table is read. This gives you a full history of every change while also making it easy to query only the current state.
Enable it on a table with maintainLatestFlagFor:
- tablename: machineform
maintainLatestFlagFor: ['machinename']
columns:
- id: tsp
dataType: timestamp
- id: machinename
dataType: string
- id: machinetype
dataType: string
- id: active
dataType: boolean
- id: description
dataType: stringmaintainLatestFlagFor takes a list of columns that together identify a unique entity. Nothing is written into the row itself: IronFlock indexes the table by that entity key plus the timestamp, and picks the newest row per entity at query time. A row that arrives late or out of order can therefore never leave a stale marker behind.
To query only current machine states:
SELECT DISTINCT ON (machinename) *
FROM machineform
ORDER BY machinename, tsp DESCTo view the full history of a specific machine:
SELECT * FROM machineform WHERE machinename = 'Assembly-Line-01' ORDER BY tspYou rarely write that query by hand. Widgets on a board that connect to this table have a latest toggle in their filter settings, so users always see current values without any extra work. From the SDK, request the same mode by adding {"latest": true} to filterAnd — see getHistory.
Upgrading from
latest_flag: earlier versions of IronFlock stored a physical boolean column calledlatest_flag. That column no longer exists — the current state is derived in SQL instead, which keeps it correct when rows arrive out of order. Existing boards and SDK calls that filter onlatest_flag = truekeep working: IronFlock recognises them and applies the latest-state mode. New code should use the latest toggle or the{"latest": true}filter entry.
Soft Deleting Records
IronFlock’s append-only model means records are never physically deleted. Instead, use a deleted boolean column to mark a record as removed. This preserves the full audit trail while hiding deleted records from dashboards.
Add a deleted column to any entity table:
- id: deleted
name: Deleted
dataType: booleanWhen a user deletes a record (for example via a form on the board), your app publishes a new row for that entity with deleted: true. Combined with maintainLatestFlagFor, this new row becomes the latest state.
To query only active (non-deleted) current records:
SELECT * FROM (
SELECT DISTINCT ON (machinename) *
FROM machineform
ORDER BY machinename, tsp DESC
) latest
WHERE deleted IS NULL OR deleted = falseThe deleted check runs after the latest row per machine has been picked. That order matters: filtering deleted rows out first would make the previous, non-deleted row resurface as the current state.
Board widgets and the SDK apply the same order automatically — combine the latest toggle (or {"latest": true}) with a deleted filter and you get exactly this behaviour. Deleted records disappear from the dashboard immediately after the form is submitted, but remain in the database for history and audit purposes.
Sharing Data with Other Apps
Your data backend is private to your app: no other app installed in the project can see your tables. Two optional keys in data-template.yml change that.
To read another app’s data, list the apps you want to read from in a top-level consumes: section — next to data:, not inside it:
consumes:
- app: machine-monitor
reason: "Computes OEE from the monitor's machine state and counter streams"
data:
tables:
- tablename: oee_results
columns:
# ... your app's own tables, as usualapp is the technical name of the providing app, or "*" (quotes required) for every app in the project. reason is shown to the user in the consent dialog — the declaration alone grants nothing until they approve it.
To keep individual tables back, mark them private: true. Everything you define is shareable by default; a private table or transform never appears in the catalog other apps see.
data:
tables:
- tablename: measurements # shared (default)
columns: [ ... ]
- tablename: calibration_state # internal — never visible to other apps
private: true
columns: [ ... ]Access is read-only, granted per project by the user, and revocable at any time. See Consuming Data from Other Apps for the full model and the SDK calls that read a provider’s history and live streams.