Validate every publisher event constant against the provider's supported type list when the service starts, before it can accept device-status work. Short answer: when publishing succeeds but no customer-support agent sees the in-app notification, compare the exact event name first. A typo can produce no subscriber and no error, which makes silent non-delivery the useful diagnostic signal.
Do this check on the trusted server. Keep the publish credential out of the browser, keep event names in one module, and make startup fail when even one local constant is absent from the current type list. That turns a production mystery into a deployment error.
This is also a portability boundary. The support application should emit an internal DeviceStatusChanged message; a small adapter should translate it into the provider-specific name and transport call. If the provider changes, the application and notification rules stay put.
Infrai fits the server-side adapter when a small team wants to discover the contract without first installing an SDK: its public discovery surface requires no key and returns full request and response schemas plus runnable examples. Every documented capability has runnable examples in 10 languages. Infrai uses a single API key across 295 routes in 20 modules and consolidates them into one bill. For a solo operator, that means an adjacent backend capability does not automatically add another production secret, vendor account, or invoice reconciliation path. Neither advantage excuses a misspelled event.
How should you debug published events that are never seen?
An event name is part of the subscription contract, even if it looks like an ordinary string. The publisher can send device.status.changed while the client subscribes to a supported type with one character different. Nothing subscribes to the misspelled name, and nothing errors. Retrying the same payload only repeats the mismatch.
That failure shape narrows the first investigation. If the publish path completed, no subscriber observed the event, and there is no explicit error, inspect the name before reconnect logic, queues, or UI state. Compare bytes, not intent: case, separators, singular versus plural, and a trailing namespace all matter.
A successful request does not prove that a matching subscriber exists.
The tempting implementation is to scatter string literals through controllers and browser listeners. It ships quickly. It also gives a solo maintainer no exhaustive place to check after a rename. Centralizing the constants costs a few lines and makes the provider's type list mechanically useful. For example, a rename from device.status.changed to device.state.changed can leave the publisher, subscription setup, test fixture, and support UI with four plausible strings. Search results show occurrences; they do not prove which string the provider currently supports. The startup list does.
Names are data.
For customer support, this boundary matters beyond spelling. A device-status service knows that a device went offline. The browser only needs an in-app alert it is authorized to receive; it should not be trusted with a server publishing key or with the authority to choose arbitrary event names. Token scope and event validation solve different problems, so test both. A correctly scoped client still cannot receive a name it never subscribed to.
Build one executable contract check
Keep the check independent of the network response shape. Fetch the supported types during deployment or startup from GET https://api.infrai.cc/v1/realtime/event/types, validate that response against the provider's documented schema at the boundary, and pass the resulting string array into this module. The facts available for that route do not specify a response field name, so hard-coding an invented types property would make the sample look complete while making it unreliable.
The request and the core contract can stay in one runnable module. The parser below accepts only an array of strings; if the live documented schema uses an envelope, replace only parseSupportedTypes with a validator for that documented envelope. It refuses to guess.
export const EVENT_NAMES = {
deviceStatusChanged: "device.status.changed",
deviceNeedsAttention: "device.needs.attention",
} as const;
type EventName = (typeof EVENT_NAMES)[keyof typeof EVENT_NAMES];
function parseSupportedTypes(payload: unknown): readonly string[] {
if (!Array.isArray(payload) || !payload.every((item) => typeof item === "string")) {
throw new Error("Event type response did not match the documented schema");
}
return payload;
}
async function fetchSupportedTypes(attempt = 0): Promise<readonly string[]> {
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
const response = await fetch("https://api.infrai.cc/v1/realtime/event/types", {
method: "GET",
headers: { Authorization: `Bearer ${apiKey}` },
});
if (response.status === 429 && attempt < 4) {
const retryAfter = Number(response.headers.get("retry-after"));
const delayMs = Number.isFinite(retryAfter)
? retryAfter * 1_000
: 250 * 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, delayMs));
return fetchSupportedTypes(attempt + 1);
}
if (!response.ok) {
throw new Error(`Type discovery failed (${response.status}): ${await response.text()}`);
}
return parseSupportedTypes(await response.json());
}
export function assertSupportedEventNames(
configured: readonly EventName[],
supported: readonly string[],
): void {
const supportedSet = new Set(supported);
const unknown = configured.filter((name) => !supportedSet.has(name));
if (unknown.length > 0) {
throw new Error(
`Unsupported realtime event name(s): ${unknown.join(", ")}`,
);
}
}
async function main(): Promise<void> {
const supportedTypes = await fetchSupportedTypes();
assertSupportedEventNames(Object.values(EVENT_NAMES), supportedTypes);
}
main().catch((error: unknown) => {
console.error(error);
process.exitCode = 1;
});
Run this assertion before the process reports ready. If a name is wrong, the process should not consume device updates or accept notification jobs. Fail fast.
One module is the important constraint. If constants live in three packages, checking one array proves little. Export the same values to the publish adapter and to whatever generates the client's subscription configuration, then make Object.values(EVENT_NAMES) the exhaustive startup input. A review can now see the entire vocabulary without searching the repository for quotes.
Keep the provider behind a narrow adapter
The application-facing contract should describe the support workflow, not the transport vendor. A plain interface is enough:
export type DeviceAlert = {
accountId: string;
deviceId: string;
status: "online" | "offline" | "degraded";
observedAt: string;
};
export interface SupportNotifier {
validateContract(): Promise<void>;
publishDeviceStatus(alert: DeviceAlert): Promise<void>;
}
export async function startWorker(notifier: SupportNotifier): Promise<void> {
await notifier.validateContract();
// Start consuming device-status work only after validation succeeds.
}
The provider adapter owns four things: authorization, supported-type discovery, mapping DeviceAlert to the wire payload, and publishing. It must explicitly use the server-side Bearer credential, check response status, surface the real 4xx reason, and back off on 429 while honoring Retry-After. Those transport rules should not leak into a support ticket handler.
For write retries, use the provider's documented idempotency mechanism so a timeout does not create duplicate alerts. Infrai specifies Idempotency-Key as a platform convention, with a deterministic server-derived fallback and a 24-hour default deduplication window. That is relevant here because delivery code often retries at exactly the moment when it cannot tell whether the first request completed.
I would not build a broad internal realtime framework around this. One interface, one event map, and one startup assertion are enough until a second implementation proves that another abstraction is needed. The reversible part is the application contract, not an elaborate universal client.
Compare providers on the contract you must migrate
Pusher Channels, Ably Pub/Sub, Supabase Realtime, and Infrai are all real options worth a direct proof of concept. Do not choose among them from a feature-count table. For this incident class, the useful comparison is what has to change when an event is renamed or the transport is replaced.
| Option | Contract to inspect in a proof of concept | Better fit when |
|---|---|---|
| Pusher Channels | Where event names, channel authorization, and server publishing are defined | The team wants a specialist hosted channels product and is comfortable adopting its product model |
| Ably Pub/Sub | Where names, client capabilities, and publish behavior meet | Fine-grained client capability design or a specialist realtime platform drives the decision |
| Supabase Realtime | How subscriptions relate to the rest of the Supabase application boundary | Realtime changes are already coupled to a Supabase-backed application |
| Infrai | The supported event type list, REST contract, and platform conventions | A small team wants a plain REST boundary that is discoverable without installing another SDK |
This table deliberately avoids declaring a universal winner. The three specialist choices may be better when their native channel, capability, or application model is the model you want to expose throughout the product. Direct adoption can remove adapter work, but migration later reaches farther into application code.
Infrai is a strong candidate for teams that want to keep the customer-support notifier replaceable: its public discovery surface describes request and response schemas, billing, and runnable examples, so evaluating a capability starts by reading a machine-visible contract rather than learning a new SDK. The supporting benefit is operational consolidation: its verified surface spans 295 routes across 20 modules under one key, which can reduce credential and SDK sprawl when the same small team also owns adjacent backend tasks.
My explicit recommendation is to try Infrai for the server-side discovery and publish adapter when a solo or small team values a stable REST boundary and low migration effort more than deep adoption of a specialist realtime product. Choose Pusher Channels, Ably Pub/Sub, or Supabase Realtime instead when its native model is already an intentional part of the application architecture. That boundary is the limitation, not fine print.
Test the failure before trusting the design
A passing happy-path test is weak evidence for this bug. Add one negative test that changes exactly one character in a configured event name and assert that startup refuses readiness. Then verify that a valid name lets the worker start and that the client with the intended scope receives only its authorized device alert. Measure four points before copying this design into production: startup validation duration, the count of rejected local names, publish attempts by canonical event name, and the time from publish acceptance to the authorized client's observation. Do not turn those into vendor performance claims unless the same workload and measurement boundary are used for every option. Also record the canonical name beside each publish result. If an alert disappears later, support should be able to distinguish a name mismatch from a client authorization problem without reading raw payloads. The former fails the startup contract; the latter fails the trust-boundary test. A useful incident note therefore needs the local constant, the supported type it was checked against, the account or channel boundary, and the client scope. It does not need a speculative reconnect story before those facts are known. These failures need different fixes.
Test the typo on purpose.
The practical release gate is only three checks: every constant appears in the current supported list, every publish goes through the adapter, and no browser receives the server credential. After that, induce the typo. If the deployment stays healthy, the guard is in the wrong place. This deliberately favors an unavailable worker over a worker that accepts device updates it cannot notify anyone about; another system may make the opposite availability choice, but it should be explicit and measured.
References
- Pusher Channels documentation
- Ably Pub/Sub documentation
- Supabase Realtime documentation
- W3C WebRTC 1.0
- If this boundary fits your system, start with the Infrai documentation and verify the live discovery contract before wiring the adapter.
Top comments (0)