Official JavaScript SDK for building applications on the RelayX platform.
npm install @relay-x/app-sdkimport { RelayApp } from "@relay-x/app-sdk";
const app = new RelayApp({
api_key: "<YOUR_API_KEY>",
secret: "<YOUR_SECRET>",
mode: "production",
});
app.connection.listeners((event) => console.log(`[connection] ${event}`));
await app.connect();
await app.telemetry.stream({
device_ident: "sensor-1",
metric: "temperature",
callback: (data) => console.log(`temp: ${JSON.stringify(data)}`),
});
await app.log.stream({
device_ident: "sensor-1",
callback: (entry) => console.log(`[${entry.level}] ${entry.data}`),
});
// ... your application logic ...
await app.disconnect();const app = new RelayApp({
api_key: "<YOUR_API_KEY>", // JWT credential from RelayX console
secret: "<YOUR_SECRET>", // Secret key
mode: "production", // 'production' | 'test'
debug: false, // Enable debug logging (default: false)
});Get your credentials at console.relay-x.io.
await app.connect();
await app.disconnect();
// Connection lifecycle events
app.connection.listeners((event) => console.log(event));
// Events: 'connected' | 'disconnected' | 'reconnecting' | 'reconnected' | 'auth_failed'Subscribe to device connect/disconnect events.
await app.connection.presence((data) => {
console.log(`${data.device_ident} ${data.event}`);
// data.event: 'connected' | 'disconnected'
});
// Unsubscribe
await app.connection.presenceOff();// List all devices
const devices = await app.device.list();
// Get a single device
const device = await app.device.get({ ident: "sensor-1" });
// Create a device
const newDevice = await app.device.create({
ident: "sensor-1",
schema: {
temperature: { type: "number", unit: "Celsius", unit_symbol: "°C" },
humidity: { type: "number", unit: "Percentage", unit_symbol: "%" },
},
config: {},
});
// Update a device
const updated = await app.device.update({
id: device.id,
config: { interval: 5000 },
});
// Delete a device
await app.device.delete("sensor-1");Stream real-time telemetry from a device. The metric is validated against the device schema.
// Stream a specific metric
await app.telemetry.stream({
device_ident: "sensor-1",
metric: "temperature",
callback: (data) =>
console.log(`[${data.metric}] ${JSON.stringify(data.data)}`),
});
// Stream all metrics
await app.telemetry.stream({
device_ident: "sensor-1",
metric: "*",
callback: (data) => console.log(data),
});
// Unsubscribe from specific metrics
await app.telemetry.off({ device_ident: "sensor-1", metric: ["temperature"] });
// Unsubscribe from all metrics for a device
await app.telemetry.off({ device_ident: "sensor-1" });Returns each requested field as an array of {value, timestamp} points.
const history = await app.telemetry.history({
device_ident: "sensor-1",
fields: ["temperature", "humidity"],
start: "2026-03-01T00:00:00.000Z",
end: "2026-03-25T00:00:00.000Z",
});Bucket by time with interval + aggregate_fn. Both must be supplied
together. interval is a Flux duration ("30s", "5m", "1h", "1d").
aggregate_fn is one of:
| function | meaning |
|---|---|
mean |
arithmetic mean per bucket |
min / max |
extrema per bucket |
sum |
total per bucket |
count |
number of points per bucket |
first / last |
first or last point per bucket |
median |
median per bucket |
stddev |
standard deviation per bucket |
// Hourly average temperature for the past day
const hourlyAvg = await app.telemetry.history({
device_ident: "sensor-1",
fields: ["temperature"],
start: "2026-04-28T00:00:00.000Z",
end: "2026-04-29T00:00:00.000Z",
interval: "1h",
aggregate_fn: "mean",
});
// Daily peak humidity for the past month
const dailyMax = await app.telemetry.history({
device_ident: "sensor-1",
fields: ["humidity"],
start, end,
interval: "1d",
aggregate_fn: "max",
});Numeric aggregates (mean, min, max, sum, median, stddev)
require numeric metric values; non-numeric points are ignored.
count, first, and last work on any value type.
Fetches the most recent telemetry values (last 24 hours).
const latest = await app.telemetry.latest({
device_ident: "sensor-1",
fields: ["temperature", "humidity"],
});Send one-way commands to devices.
// Send to one or more devices
const result = await app.command.send({
name: "set_interval",
device_ident: ["sensor-1", "sensor-2"],
data: { interval: 5000 },
});
// result: { 'sensor-1': { sent: true }, 'sensor-2': { sent: true } }
// Command history
const history = await app.command.history({
name: "set_interval",
device_idents: ["sensor-1"],
start: "2026-03-01T00:00:00.000Z",
end: "2026-03-25T00:00:00.000Z",
});Make request/reply calls to devices.
const response = await app.rpc.call({
device_ident: "sensor-1",
name: "get_status",
data: { verbose: true },
timeout: 10, // seconds (default: 10)
});Subscribe to device-published events. device_ident accepts:
"*"— all devices in your org[ident]— a single device[a, b, …]— a specific list of devices
The callback receives { <device_ident>: <event_data> } so you always
know which device fired the event.
// One device
await app.events.stream({
name: "door_opened",
device_ident: ["entry-sensor"],
callback: (payload) => {
for (const [ident, data] of Object.entries(payload)) {
console.log(`${ident} fired:`, data);
}
},
});
// All devices
await app.events.stream({
name: "boot",
device_ident: "*",
callback: (payload) => console.log(payload),
});
await app.events.off({ name: "door_opened" });const events = await app.events.history({
device_ident: "sensor-1",
event_names: ["door_opened", "boot"],
start: "2026-03-01T00:00:00.000Z",
end: "2026-03-25T00:00:00.000Z",
});// Create a threshold alert
const alert = await app.alert.create({
name: "high-temp",
type: "THRESHOLD", // 'THRESHOLD' | 'RATE_CHANGE'
metric: "temperature",
config: { threshold: 85, duration: 5 },
notification_channel: ["ops-webhook"],
});
// Get, update, delete
const fetched = await app.alert.get("high-temp");
const updated = await app.alert.update({
id: fetched.id,
config: { threshold: 90 },
});
await app.alert.delete(fetched.id);
// List all alerts
const alerts = await app.alert.list();const alert = await app.alert.get("high-temp");
await alert.listen({
on_fire: (data) => console.log("FIRED:", data),
on_resolved: (data) => console.log("RESOLVED:", data),
on_ack: (data) => console.log("ACK:", data),
});Each fire / resolved / ack event carries an incident_id that's stable
across the lifetime of an alerting episode — minted when the alert
goes from normal → alerting, persisted across cooldown re-fires and
acks, and cleared only on resolution. Use it to group related events.
History fires through the same streaming protocol as telemetry/events
and supports filtering by alert state (fire, resolved, ack) and
optionally by incident_id.
const history = await app.alert.history({
rule_type: "RULE", // 'RULE' | 'DEVICE'
rule_id: alert.id,
rule_states: ["fire", "resolved", "ack"],
start: "2026-03-01T00:00:00.000Z",
end: "2026-03-25T00:00:00.000Z",
});
// Walk a single incident end-to-end
const incident = await app.alert.history({
rule_type: "DEVICE",
device_ident: "sensor-1",
incident_id: "<incident_uuid>",
start,
end,
});device_id is required — it identifies which device's incident gets
acknowledged. After ack, cooldown re-fires for the same incident are
recorded for audit but do not dispatch notifications until the alert
resolves and a new incident begins.
await app.alert.ack({
device_id: "<device_id>",
alert_id: alert.id,
acked_by: "operator-1",
ack_notes: "Investigating",
});await app.alert.mute({
id: alert.id,
mute_config: { type: "FOREVER" },
// or { type: 'TIME_BASED', mute_till: '2026-04-01T00:00:00.000Z' }
});
await app.alert.unmute(alert.id);Ephemeral alerts let you define custom alert rules that are evaluated client-side with your own logic. See the full guide: Ephemeral Alerting Guide.
// Create an ephemeral alert
const alert = await app.alert.createEphemeral({
name: "custom-temp-alert",
config: {
topic: {
source: "TELEMETRY",
device_ident: "sensor-1",
last_token: "temperature",
},
duration: 5,
recovery_duration: 10,
recovery_eval_type: "VALUE",
},
});
// Set your evaluator
alert.setEvaluator(
(state) => (state["sensor-1"]?.temperature?.value ?? 0) > 85,
);
// Start monitoring
await alert.listen({
on_fire: (data) => console.log("ALERT:", data),
on_resolved: (data) => console.log("RESOLVED:", data),
});
// Stop
await alert.stop();Subscribe to live device logs and query history. Each log entry carries
a level (info | warn | error), a data payload, and a
device-side timestamp.
// All levels
await app.log.stream({
device_ident: "sensor-1",
callback: (entry) => console.log(`[${entry.level}] ${entry.data}`),
});
// Errors only
await app.log.stream({
device_ident: "sensor-1",
levels: ["error"],
callback: (entry) => console.error(entry.data),
});
await app.log.off({ device_ident: "sensor-1" });Returns logs grouped by level. Optionally bucket with interval +
aggregate_fn: "count" for per-level counts over time.
const logs = await app.log.history({
device_ident: "sensor-1",
start: "2026-04-28T00:00:00.000Z",
end: "2026-04-29T00:00:00.000Z",
});
// { info: [...], warn: [...], error: [...] }
// Hourly error counts
const hourly = await app.log.history({
device_ident: "sensor-1",
levels: ["error"],
start,
end,
interval: "1h",
aggregate_fn: "count",
});Group devices by tags for batch operations and streaming.
// Create
const group = await app.logicalGroup.create({
name: "floor-1-sensors",
tags: ["floor_1", "temperature"],
device_idents: ["sensor-1", "sensor-2"],
});
// Update membership
const updated = await app.logicalGroup.update({
id: group.id,
devices: { add: ["sensor-3"], remove: ["sensor-1"] },
tags: { add: ["humidity"], remove: ["floor_1"] },
});
// List, get, delete
const groups = await app.logicalGroup.list();
const fetched = await app.logicalGroup.get(group.id);
const devices = await app.logicalGroup.listDevices(group.id);
await app.logicalGroup.delete(group.id);Each group instance has stream() and off() methods.
const group = await app.logicalGroup.get("<group_id>");
await group.stream({
callback: (data) => console.log(data),
});
await group.off();Organize devices in a hierarchy path (e.g., building_1.floor_2.zone_a).
// Create
const group = await app.heirarchyGroup.create({
name: "zone-a-sensors",
heirarchy: "building_1.floor_2.zone_a",
device_idents: ["sensor-1", "sensor-2"],
});
// Update
const updated = await app.heirarchyGroup.update({
id: group.id,
devices: { add: ["sensor-3"], remove: [] },
heirarchy: "building_1.floor_3.zone_a",
});
// List, get, delete
const groups = await app.heirarchyGroup.list();
const fetched = await app.heirarchyGroup.get(group.id);
const devices = await app.heirarchyGroup.listDevices(group.id);
await app.heirarchyGroup.delete(group.id);Supports metric and hierarchy path filtering with wildcards.
const group = await app.heirarchyGroup.get("<group_id>");
// Stream all data
await group.stream({ callback: (data) => console.log(data) });
// Filter by metric
await group.stream({
metric: "temperature",
callback: (data) => console.log(data),
});
// Filter by hierarchy path (supports * and > wildcards)
await group.stream({
heirarchy: "building_1.*.zone_a",
callback: (data) => console.log(data),
});
await group.off();Create webhook or email notification channels for alerts.
// Webhook
const notif = await app.notification.create({
name: "ops-webhook",
type: "WEBHOOK",
config: { endpoint: "https://hooks.example.com/alerts" },
});
// Email
const emailNotif = await app.notification.create({
name: "ops-email",
type: "EMAIL",
config: {
recipients: ["ops@example.com"],
subject: "Alert Notification",
template: "Alert {{alert_name}} fired on {{device_ident}}",
},
});
// Update, delete, list, get
const updated = await app.notification.update({
name: "ops-webhook",
type: "WEBHOOK",
config: { endpoint: "https://new-url.com" },
});
await app.notification.delete(notif.id);
const all = await app.notification.list();
const fetched = await app.notification.get("<notif_id>");- Commands: Buffered in memory while disconnected and flushed automatically on reconnect.
- Subscriptions: All active telemetry, event, presence, alert, and group stream subscriptions are automatically restored on reconnect.
- Ephemeral Alerts: Alert state events (fire, resolved, ack) are buffered and published on reconnect.
The SDK throws standard Error objects with descriptive messages.
try {
await app.telemetry.stream({
device_ident: "sensor-1",
metric: "nonexistent",
callback: () => {},
});
} catch (err) {
console.log(`Validation error: ${err.message}`);
}Common error scenarios:
- Invalid arguments or missing required fields
- Operations attempted while disconnected
- Schema validation failures (metric not in device schema)
Apache-2.0