npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@relay-x/app-sdk

v0.1.8

Published

RelayX App SDK for IoT device management

Readme

RelayX App SDK for JavaScript

Official JavaScript SDK for building applications on the RelayX platform.

View Full Documentation →

Installation

npm install @relay-x/app-sdk

Quick Start

import { 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();

Configuration

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.

Connection

await app.connect();
await app.disconnect();

// Connection lifecycle events
app.connection.listeners((event) => console.log(event));
// Events: 'connected' | 'disconnected' | 'reconnecting' | 'reconnected' | 'auth_failed'

Presence

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();

Devices

// 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");

Telemetry

Live Streaming

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" });

History

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",
});

Aggregation

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.

Latest

Fetches the most recent telemetry values (last 24 hours).

const latest = await app.telemetry.latest({
  device_ident: "sensor-1",
  fields: ["temperature", "humidity"],
});

Commands

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",
});

RPC

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)
});

Events

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" });

History

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",
});

Alerts

CRUD

// 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();

Listening

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

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,
});

Acknowledge

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",
});

Mute / Unmute

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

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();

Logs

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.

Live Streaming

// 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" });

History

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",
});

Logical Groups

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);

Group Streaming

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();

Hierarchy Groups

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);

Hierarchy Group Streaming

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();

Notifications

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: ["[email protected]"],
    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>");

Offline Behavior

  • 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.

Error Handling

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)

License

Apache-2.0