Code
Five places code has to read well — a quickstart, release notes with code in the prose, an incident runbook, a config reference and a before/after lesson.
Send your first event
Two commands and nine lines. If you already have a token in your environment, skip straight to the second block.
InstallCopy
npm install @relay/sdk
# or
pnpm add @relay/sdkSend an eventCopy
import { Relay } from "@relay/sdk";
const relay = new Relay({
token: process.env.RELAY_TOKEN
});
await relay.events.send({
type: "order.created",
data: { orderId: "NW-40218" }
});New idempotencyKey option on every write. Retries with the same key return the original response instead of creating a duplicate.
Default backoff is now exponential with jitter. Override it with retries or disable entirely with retries: 0.
relay.events.list() no longer drops the cursor parameter when combined with a date filter.
The deprecated v2 config format. Run npx relay migrate to convert existing files.
Breaking changes are always announced one minor version ahead.
Elevated webhook delivery latency
Confirm the symptom
Check delivery lag before touching anything. If p99 is under 30s this is not the runbook you want.
$relay metrics delivery --window 15m --percentile 99Identify the bad rollout
Compare the running config hash against the last known-good deploy.
$relay config diff --env production --against last-greenRoll back
Rolling back is safe at any point — queued events replay automatically.
$relay deploy rollback --env production --to last-greenrelay.config.ts
Every key is optional. Anything you omit falls back to the default shown below, which is what most projects ship with.
regionstring"eu-central-1"Where builds run and functions deploy.runtimestring"node22"Applies to new deployments only.buildFilterstring[][]Glob patterns; empty means build everything.previewAuthbooleantrueRequire sign-in to open a preview URL.retriesnumber4Maximum delivery attempts per event.// relay.config.ts
import { defineConfig } from "@relay/sdk";
export default defineConfig({
region: "eu-central-1",
buildFilter: ["apps/web/**"],
previewAuth: false
});Lesson 2 — Why await inside a loop is usually wrong
Module 4 · Asynchrony
// Sequential — 10 requests take 10 × latency
const results = [];
for (const id of ids) {
results.push(await fetchUser(id));
}// Concurrent — 10 requests take about 1 × latency
const results = await Promise.all(
ids.map((id) => fetchUser(id))
);- Use
Promise.allwhen the calls do not depend on each other - Keep
awaitin the loop when each call needs the previous result - Reach for
Promise.allSettledwhen one failure should not abort the rest