ironflock
v1.5.3
Published
Collect data in the IronFlock Storage architecture
Readme
ironflock
About
With this library you can publish data from your apps on your IoT edge hardware to the fleet data storage of the IronFlock devops platform. When this library is used on a certain device the library automatically uses the private messaging realm (Unified Name Space) of the device's fleet and the data is collected in the respective fleet database.
So if you use the library in your app, the data collection will always be private to the app user's fleet.
For more information on the IronFlock IoT Devops Platform for engineers and developers visit our IronFlock home page.
Requirements
- Node.js: 18 or higher
- Browser: Any modern browser with WebSocket support (Chrome, Firefox, Safari, Edge)
Installation
npm install ironflockUsage
import { IronFlock } from "ironflock";
// Create an IronFlock instance to connect to the IronFlock platform data infrastructure.
// The IronFlock instance handles authentication when run on a device registered in IronFlock.
const ironflock = new IronFlock();
// Start the connection to the platform
await ironflock.start();
// Publish an event
await ironflock.publish("test.publish.example", [{ temperature: 20 }]);
// Stop the connection when done
await ironflock.stop();Options
The IronFlock constructor accepts an options object. In Node.js, values fall back to environment variables. In the browser, all config must be passed explicitly.
{
serialNumber?: string; // Device serial number (env: DEVICE_SERIAL_NUMBER)
deviceName?: string; // Device display name (env: DEVICE_NAME)
deviceKey?: string; // Device key for auth (env: DEVICE_KEY)
appName?: string; // Application name (env: APP_NAME)
swarmKey?: number; // Fleet/swarm key (env: SWARM_KEY)
appKey?: number; // Application key (env: APP_KEY)
env?: string; // Environment: "DEV" or "PROD" (env: ENV)
reswarmUrl?: string; // Studio URL to resolve WebSocket URI (env: RESWARM_URL)
cburl?: string; // Direct WebSocket URI override
}All fields are optional in the type, but serialNumber is required at runtime (either via the option or the DEVICE_SERIAL_NUMBER environment variable).
Browser Usage
The library works in modern browsers. Since browsers don't have environment variables, pass all config via the constructor:
import { IronFlock } from "ironflock";
const ironflock = new IronFlock({
serialNumber: "device-serial-from-server",
deviceKey: "my-device-key",
appName: "MyWebApp",
swarmKey: 10,
appKey: 20,
env: "PROD",
});
await ironflock.start();
await ironflock.publish("sensor.readings", [{ temperature: 22 }]);Loading config from a server
Use IronFlock.fromServer() to fetch configuration from your backend:
import { IronFlock } from "ironflock";
// Your backend endpoint returns a JSON object matching IronFlockOptions
const ironflock = await IronFlock.fromServer("/api/ironflock-config");
await ironflock.start();Your backend endpoint should return JSON like:
{
"serialNumber": "device-serial-123",
"deviceKey": "dev-key-1",
"appName": "MyApp",
"swarmKey": 10,
"appKey": 20,
"env": "PROD"
}Normally the server should extracts these values from the environment variables of the server process, since they depend on the execution environment. i.e. the edge device the server is running on.
Security note: The endpoint providing device credentials should be protected by authentication. This is normally already provided by the IronFlock remote access proxy when executed in an IronFlock project.
API Reference
publish(topic, args?, kwargs?)
Publishes an event to a topic on the IronFlock message router.
const publication = await ironflock.publish("com.myapp.mytopic", [{ temperature: 20 }]);| Parameter | Type | Description |
|-----------|------|-------------|
| topic | string | The URI of the topic to publish to |
| args | unknown[], optional | Payload arguments |
| kwargs | Record<string, unknown>, optional | Payload keyword arguments |
Returns: Promise<unknown> — The publication object (an acknowledged publish receipt).
publishToTable(tablename, args?, kwargs?)
Convenience function to publish data to a fleet table in the IronFlock platform. Automatically constructs the correct topic using the SWARM_KEY and APP_KEY environment variables.
await ironflock.publishToTable("sensordata", [{ temperature: 22.5, humidity: 60 }]);| Parameter | Type | Description |
|-----------|------|-------------|
| tablename | string | The name of the table, e.g. "sensordata" |
| args | unknown[], optional | Row data to publish |
| kwargs | Record<string, unknown>, optional | Row data as keyword arguments |
Returns: Promise<unknown> — The publication object (an acknowledged publish receipt).
appendToTable(tablename, args?, kwargs?)
Appends data to a fleet table by calling the registered append procedure at append.<SWARM_KEY>.<APP_KEY>.<tablename>. Unlike publishToTable, this uses a remote procedure call rather than a pub/sub event.
await ironflock.appendToTable("sensordata", [{ temperature: 22.5, humidity: 60 }]);| Parameter | Type | Description |
|-----------|------|-------------|
| tablename | string | The name of the table, e.g. "sensordata" |
| args | unknown[], optional | Row data to append |
| kwargs | Record<string, unknown>, optional | Row data as keyword arguments |
Returns: Promise<unknown> — The result of the remote procedure call.
publishRowsToTable(tablename, rows, kwargs?)
Publishes many rows in a single message (bulk insert) to the dedicated topic bulk.<SWARM_KEY>.<APP_KEY>.<tablename>. The platform inserts the whole batch atomically (all-or-nothing) in one operation. Use this for high-frequency data where one round-trip per row is too costly. Like publishToTable, this is fire-and-forget — the ack confirms delivery to the router, not the DB insert.
await ironflock.publishRowsToTable("sensordata", [
{ tsp: "2024-01-15T10:30:00.000Z", temperature: 22.5 },
{ tsp: "2024-01-15T10:30:01.000Z", temperature: 22.7 },
]);| Parameter | Type | Description |
|-----------|------|-------------|
| tablename | string | The name of the table, e.g. "sensordata" |
| rows | Record<string, unknown>[] | Non-empty array of row objects to insert |
| kwargs | Record<string, unknown>, optional | Extra arguments shared by the whole batch |
Returns: Promise<unknown> — The publication object (an acknowledged publish receipt).
appendRowsToTable(tablename, rows, kwargs?)
Appends many rows in a single RPC (bulk insert) by calling the dedicated procedure appendBulk.<SWARM_KEY>.<APP_KEY>.<tablename>. The platform inserts the whole batch atomically (all-or-nothing): if any row is invalid the entire batch is rejected and nothing is persisted. Prefer this over publishRowsToTable when you need the insert outcome.
const result = await ironflock.appendRowsToTable("sensordata", [
{ tsp: "2024-01-15T10:30:00.000Z", temperature: 22.5 },
{ tsp: "2024-01-15T10:30:01.000Z", temperature: 22.7 },
]);
// result -> { success: true, count: 2 }| Parameter | Type | Description |
|-----------|------|-------------|
| tablename | string | The name of the table, e.g. "sensordata" |
| rows | Record<string, unknown>[] | Non-empty array of row objects to insert |
| kwargs | Record<string, unknown>, optional | Extra arguments shared by the whole batch |
Returns: Promise<unknown> — The result of the remote procedure call (e.g. { success: true, count: N }).
reportError(error, opts?)
Reports an application error into the fleet's error-logs table. This is a convenience wrapper over publishToTable / appendToTable: it stamps the row with source: "app", a severity level, and a timestamp, then writes it like any normal table row. The error lands in the same per-databackend error-logs table that fleetdb system errors use (tagged source: "system"), so it is queryable with getHistory, streamable with subscribeToTable, usable in board-templates, and delivered in realtime on transformed.error-logs — without firing the platform's system-error toast.
// Fire-and-forget (default): publishes to the error-logs table
await ironflock.reportError("Sensor read timed out", { level: "warn" });
// Pass an Error to capture its stack (falls back to the message)
try {
riskyOperation();
} catch (err) {
await ironflock.reportError(err as Error);
}
// Use the append RPC when you want to await the insert outcome
await ironflock.reportError("Calibration failed", { level: "error", append: true });| Parameter | Type | Description |
|-----------|------|-------------|
| error | string \| Error | The error message, or an Error whose stack (or message) is recorded |
| opts | ReportErrorOptions, optional | Options (see below) |
opts fields:
| Field | Type | Description |
|-------|------|-------------|
| level | ErrorLevel, optional | Severity: "error", "warn", "info" or "debug". Defaults to "error" |
| append | boolean, optional | When true, use the append RPC (resolves with the insert outcome). Defaults to false (fire-and-forget publish) |
| tsp | string, optional | ISO-8601 timestamp override. Defaults to the current time |
Returns: Promise<unknown> — The publication object, or — with append: true — the result of the remote procedure call.
subscribe(topic, handler)
Subscribes to a topic on the IronFlock message router.
function onMessage(...args: any[]) {
console.log("Received:", args);
}
const subscription = await ironflock.subscribe("com.myapp.mytopic", onMessage);| Parameter | Type | Description |
|-----------|------|-------------|
| topic | string | The URI of the topic to subscribe to |
| handler | (...args: any[]) => void | Function called when a message is received |
Returns: Promise<Subscription | undefined> — The subscription object.
subscribeToTable(tablename, handler)
Convenience function to subscribe to a fleet table. Automatically constructs the correct topic using the SWARM_KEY and APP_KEY environment variables. Receives rows written via both the single-row and the bulk insert paths — rows from a bulk insert are delivered to your handler one at a time, so handler code stays the same.
function onTableData(...args: any[]) {
console.log("New row:", args);
}
await ironflock.subscribeToTable("sensordata", onTableData);| Parameter | Type | Description |
|-----------|------|-------------|
| tablename | string | The name of the table to subscribe to |
| handler | (...args: any[]) => void | Function called when new data arrives |
Returns: Promise<Subscription | undefined> — The subscription object.
getHistory(tablename, queryParams?)
Retrieves historical data from a fleet table.
// Simple query with limit
const data = await ironflock.getHistory("sensordata", { limit: 100 });
// Query with time range and filters
const data = await ironflock.getHistory("sensordata", {
limit: 500,
offset: 0,
timeRange: {
start: "2026-01-01T00:00:00Z",
end: "2026-03-01T00:00:00Z",
},
filterAnd: [
{ column: "temperature", operator: ">", value: 20 },
{ column: "humidity", operator: "<=", value: 80 },
],
});| Parameter | Type | Description |
|-----------|------|-------------|
| tablename | string | The name of the table to query |
| queryParams | TableQueryParams | Query parameters (see below) |
queryParams fields:
| Field | Type | Description |
|-------|------|-------------|
| limit | number | Maximum number of rows to return (1–10000, required) |
| offset | number, optional | Offset for pagination |
| timeRange | ISOTimeRange, optional | { start: "<ISO datetime>", end: "<ISO datetime>" } |
| filterAnd | SQLFilterAnd[], optional | List of AND filter conditions: { column: string, operator: string, value: ... } |
Supported filter operators: =, !=, >, <, >=, <=, LIKE, ILIKE, IN, NOT IN, IS, IS NOT.
Returns: Promise<unknown | null> — The query result data (typically an array of row objects), or null if the call failed.
getSeriesHistory(tablename, params)
Retrieves down-sampled time-series data from a fleet table — numeric columns aggregated into time buckets (e.g. hourly averages). Ideal for charts over long time ranges. Available for tables (not transforms).
const series = await ironflock.getSeriesHistory("sensordata", {
metrics: ["temperature", "humidity"],
method: "AVG",
limit: 500,
timeRange: ["2026-01-01T00:00:00Z", "2026-03-01T00:00:00Z"],
groupBy: ["device_id"],
});| Parameter | Type | Description |
|-----------|------|-------------|
| tablename | string | The name of the table to query |
| params | SeriesQueryParams | Series query parameters (see below) |
params fields:
| Field | Type | Description |
|-------|------|-------------|
| metrics | string[] | Numeric columns to down-sample |
| method | DownSampleMethod | Aggregation per bucket: "AVG", "SUM", "COUNT", "MIN", "MAX", "FIRST" or "LAST" |
| limit | number | Maximum number of buckets (1–10000) |
| timeRange | SeriesTimeRange | [start, end] — ISO strings or epoch-ms numbers; null = open end (required) |
| groupBy | string[], optional | Columns to group the series by |
| filterAnd | SQLFilterAnd[], optional | AND filter conditions |
Returns: Promise<unknown | null> — The down-sampled series rows, or null if the call failed.
call(topic, args?, kwargs?)
Calls a remote procedure on the IronFlock message router using a full WAMP topic URI.
const result = await ironflock.call("some.full.wamp.topic", [42]);| Parameter | Type | Description |
|-----------|------|-------------|
| topic | string | The full WAMP URI of the procedure to call |
| args | unknown[], optional | Positional arguments |
| kwargs | Record<string, unknown>, optional | Keyword arguments |
Returns: Promise<unknown> — The result of the remote procedure call.
callDeviceFunction(deviceKey, topic, args?, kwargs?)
Calls a remote procedure registered by another IronFlock device. Automatically assembles the full WAMP topic as {swarmKey}.{deviceKey}.{appKey}.{env}.{topic}.
const result = await ironflock.callDeviceFunction(42, "com.myapp.myprocedure", [42]);| Parameter | Type | Description |
|-----------|------|-------------|
| deviceKey | number | The device key of the target device |
| topic | string | The URI of the procedure to call |
| args | unknown[], optional | Positional arguments |
| kwargs | Record<string, unknown>, optional | Keyword arguments |
Returns: Promise<unknown> — The result of the remote procedure call.
callFunction()is a deprecated alias forcallDeviceFunction().
IronFlock.fromServer(url) (static)
Fetches configuration from a server endpoint and creates an IronFlock instance. Useful in browser environments where the backend provides device credentials.
const ironflock = await IronFlock.fromServer("/api/ironflock-config");
await ironflock.start();| Parameter | Type | Description |
|-----------|------|-------------|
| url | string | URL of the endpoint returning IronFlockOptions JSON |
Returns: Promise<IronFlock> — A configured IronFlock instance.
registerDeviceFunction(topic, endpoint)
Registers a procedure that can be called by other devices in the fleet. Automatically constructs the full WAMP topic as {swarmKey}.{deviceKey}.{appKey}.{env}.{topic}.
function add(args: any[]) {
return args[0] + args[1];
}
await ironflock.registerDeviceFunction("com.myapp.add", add);| Parameter | Type | Description |
|-----------|------|-------------|
| topic | string | The URI of the procedure to register |
| endpoint | (...args: any[]) => any | The function to register |
Returns: Promise<Registration | undefined> — The registration object.
register()is an alias forregisterDeviceFunction().registerFunction()is a deprecated alias.
setDeviceLocation(long, lat)
Updates the device's location in the platform master data. The maps in device or group overviews will reflect the new location in realtime.
await ironflock.setDeviceLocation(8.6821, 50.1109);| Parameter | Type | Description |
|-----------|------|-------------|
| long | number | Longitude (-180 to 180) |
| lat | number | Latitude (-90 to 90) |
Returns: Promise<unknown | null> — The result of the location update call, or null if the call failed.
Note: Location history is not stored. If you need location history, create a dedicated table and use
publishToTable.
getRemoteAccessUrlForPort(port)
Returns the remote access URL for a given port on the device.
const url = ironflock.getRemoteAccessUrlForPort(8080);
// e.g. "https://<device_key>-<app_name>-8080.app.ironflock.com"| Parameter | Type | Description |
|-----------|------|-------------|
| port | number | The port number |
Returns: string | null — The remote access URL string, or null if the device key or app name is not available.
Properties
| Property | Type | Description |
|----------|------|-------------|
| isConnected | boolean | true if the connection to the platform is established |
| connection | CrossbarConnection | The underlying connection instance (for advanced use) |
Lifecycle Methods
| Method | Description |
|--------|-------------|
| await start(cburl?) | Configures and starts the connection to the platform |
| await stop() | Stops the connection |
Cross-App Data Access
Read another app's fleet data from within your app, in the same project and fleet. The provider app must list your app in its data-template consumes: section, and the project user must grant access. Access is read-only: you can query history and subscribe to realtime rows of the tables and transforms (views) the provider shares — you cannot write to them.
import { IronFlock, CrossAppAccessError } from "ironflock";
const ironflock = new IronFlock();
await ironflock.start();
// Open a read-only handle on another app's data backend
const weather = await ironflock.connectToApp("weather-app");
// Inspect what the provider shares (non-private tables / transforms)
console.log(weather.tables.map((t) => t.tablename));
// Query history, just like your own tables
const rows = await weather.getHistory("forecasts", { limit: 100 });
// Subscribe to realtime rows
await weather.subscribeToTable("forecasts", (...args) => {
console.log("New forecast:", args);
});
// Access errors carry a machine-readable code
try {
await ironflock.connectToApp("unshared-app");
} catch (err) {
if (err instanceof CrossAppAccessError) {
console.error(err.code); // e.g. "NO_GRANT"
}
}Consumed-app connections are cached per app + stage and are closed automatically by ironflock.stop().
If your app holds the wildcard grant (consumes: [{ app: "*" }]), use listConsumableApps to discover every provider in the project and connectToAllApps to open them all at once.
connectToApp(appName, opts?)
Opens a read-only connection to another app's data backend in the same project and returns a ConsumedApp handle. Resolves the provider and connects to its realm using this device's credentials.
const weather = await ironflock.connectToApp("weather-app", { stage: "prod" });| Parameter | Type | Description |
|-----------|------|-------------|
| appName | string | Provider app name, as declared in your consumes: section |
| opts | ConnectToAppOptions, optional | Options (see below) |
opts fields:
| Field | Type | Description |
|-------|------|-------------|
| stage | "dev" \| "prod", optional | Provider stage to connect to. Defaults to this app's own stage (ENV) |
| onError | (error: CrossAppAccessError) => void, optional | Called when the connection is fatally denied after connectToApp resolved (e.g. the grant is later revoked) |
Returns: Promise<ConsumedApp> — A read-only handle on the provider's data backend.
Throws: CrossAppAccessError (code: NO_GRANT, PROVIDER_NOT_INSTALLED, UNKNOWN_APP, or NOT_AUTHORIZED).
listConsumableApps()
Lists every non-private provider in the project — the discovery primitive for apps that hold the wildcard consume grant (consumes: [{ app: "*" }] in your data-template, granted by the project user). Performs a single sys.appaccess.list call and opens no connections: render the returned catalogs in a picker, then call connectToApp for the ones you want — or connectToAllApps to open them all at once.
Note: Declare the grant in your app's
data-template.yml, and quote the*— a bare*is a YAML alias and won't parse:consumes: - app: "*"
const providers = await ironflock.listConsumableApps();
for (const p of providers) {
console.log(p.app, Object.keys(p.stages)); // e.g. "weather-app" ["dev", "prod"]
}Returns: Promise<ConsumedAppInfo[]> — one entry per non-private provider:
| Field | Type | Description |
|-------|------|-------------|
| app | string | Provider app name |
| provider_app_key | number | The provider's app key |
| stages | { dev?, prod? } | Per-stage catalog; a stage is present only if the provider has a data backend for it. Each catalog is the non-private { tables, transforms } it shares |
Throws: CrossAppAccessError (code: NO_GRANT) when your app holds no wildcard grant.
connectToAllApps(opts?)
Opens read-only connections to every non-private provider in the project in one go (wildcard consumers only). Enumerates providers via listConsumableApps and opens each one, skipping any that lack a data backend for the requested stage. Each handle is cached under the same app + stage key as connectToApp, so a later connectToApp(name) returns the already-warmed handle instead of opening a duplicate.
const apps = await ironflock.connectToAllApps({
onError: (err) => console.warn("Provider skipped:", err),
});
for (const app of apps) {
const rows = await app.getHistory(app.tables[0]?.tablename, { limit: 10 });
console.log(app.app, rows);
}| Parameter | Type | Description |
|-----------|------|-------------|
| opts | ConnectToAllAppsOptions, optional | Options (see below) |
opts fields:
| Field | Type | Description |
|-------|------|-------------|
| stage | "dev" \| "prod", optional | Provider stage to connect to. Defaults to this app's own stage (ENV) |
| continueOnError | boolean, optional | When true (the default), a provider that fails to open is reported via onError and omitted from the result. When false, the first failure rejects the whole call |
| onError | (error: unknown) => void, optional | Called with each provider that could not be opened (while continueOnError is true), and with a CrossAppAccessError if an already-opened connection is later fatally denied (e.g. the grant is revoked) |
Returns: Promise<ConsumedApp[]> — the successfully-opened provider handles. Closed together by ironflock.stop().
Throws: CrossAppAccessError (code: NO_GRANT) when your app holds no wildcard grant. With continueOnError: false, also rejects with the first provider's open failure.
ConsumedApp handle
Returned by connectToApp. A read-only view of a provider app's shared tables and transforms.
Properties:
| Property | Type | Description |
|----------|------|-------------|
| app | string | Provider app name |
| stage | "dev" \| "prod" | Provider stage this handle is connected to |
| tables | ProviderTableInfo[] | Non-private tables the provider shares |
| transforms | ProviderTableInfo[] | Non-private transforms (views) the provider shares |
| isConnected | boolean | true while the connection to the provider is open |
| connection | CrossbarConnection | The underlying connection (advanced use) |
consumedApp.getHistory(tablename, queryParams?)
Queries history rows of a shared table or transform. Takes the same query parameters as getHistory (limit, offset, timeRange, filterAnd).
const rows = await weather.getHistory("forecasts", { limit: 100 });Returns: Promise<unknown> — The query result rows.
consumedApp.subscribeToTable(tablename, handler)
Subscribes to realtime rows of a shared table or transform. Bulk-inserted rows are delivered one at a time, exactly like subscribeToTable.
await weather.subscribeToTable("forecasts", (...args) => console.log(args));Returns: Promise<Subscription | undefined> — The subscription object.
consumedApp.getSeriesHistory(tablename, params)
Queries down-sampled time-series history of a shared table (not available for transforms).
const series = await weather.getSeriesHistory("forecasts", {
metrics: ["temperature"],
method: "AVG",
limit: 500,
timeRange: ["2026-01-01T00:00:00Z", "2026-03-01T00:00:00Z"],
});params fields (SeriesQueryParams):
| Field | Type | Description |
|-------|------|-------------|
| metrics | string[] | Numeric columns to down-sample |
| method | DownSampleMethod | Aggregation per bucket: "AVG", "SUM", "COUNT", "MIN", "MAX", "FIRST" or "LAST" |
| limit | number | Maximum number of rows (1–10000) |
| timeRange | SeriesTimeRange | [start, end] — ISO strings or epoch-ms numbers; null = open end (required) |
| groupBy | string[], optional | Columns to group the series by |
| filterAnd | SQLFilterAnd[], optional | AND filter conditions |
Returns: Promise<unknown> — The down-sampled series rows.
consumedApp.close()
Closes this handle's connection to the provider. (All consumed-app connections are also closed by ironflock.stop().)
Returns: Promise<void>
CrossAppAccessError
Thrown by connectToApp and the ConsumedApp methods when cross-app access is denied or misused. Exposes a machine-readable code:
| code | Meaning |
|--------|---------|
| NO_GRANT | The project user has not granted your app access to the provider |
| PROVIDER_NOT_INSTALLED | The provider app has no data backend for that stage in this project |
| UNKNOWN_APP | No app by that name |
| PRIVATE_TABLE | The requested table/transform is not in the provider's shared catalog |
| NOT_AUTHORIZED | The router/provider denied access (e.g. the grant was revoked) |
Development
Install dependencies:
npm installRun tests:
npm test # Node.js tests
npm run test:browser # Browser environment tests
npm run test:all # BothType check:
npm run typecheckBuild:
npm run buildPublish a new release:
npm run release