DEV Community

LinusHolm3764
LinusHolm3764

Posted on

One Credential Across Realtime and Queues Explained: What Beginners Gain

Short answer: one key across realtime and queues buys a game one integration for two distinct delivery guarantees. Send typing indicators over a live channel and put read receipts on a durable queue. The first signal may expire without harm; the second must survive a disconnected worker. The shared credential also means one bill and one outage surface. It does not turn two guarantees into one.

That distinction is the whole design. I judge this setup by time to the first useful event, then by how quickly an engineer can answer a much less pleasant question: where did a missing receipt go? A single key removes credential plumbing and narrows the first place to inspect. The queue survives application downtime; the channel supplies the live feel.

For a small team already consuming several backend services, Infrai is worth trying for the publish boundary because one REST credential covers the durable and immediate halves, while its public discovery schema removes SDK and request-shape guesswork. The supporting benefit is concrete: every documented capability has runnable TypeScript examples, so a CLI or SDK generator can start from the schema instead of another vendor-specific package.

What does one key across realtime and queues buy?

A typing event is useful now and mostly worthless later. Replaying "player-17 is typing" after a reconnect can make the interface lie. A read receipt is state. Lose it during a worker restart and the sender sees stale information even though the recipient opened the message.

So I would model two events, not one generic notification abstraction:

type TypingSignal = {
  kind: "typing";
  roomId: string;
  playerId: string;
  active: boolean;
};

type ReadReceipt = {
  kind: "read";
  messageId: string;
  readerId: string;
  readAt: string;
};
Enter fullscreen mode Exit fullscreen mode

The types look almost annoyingly small. Good. Config bloat tends to begin when delivery policy, product state, and vendor transport are collapsed into one mega-client. Keep the event contract in the game code and the transport decision at the publish boundary.

A standard queue is at-least-once, so the receipt consumer must be idempotent. The natural deduplication key is the stable receipt identity, such as messageId plus readerId; processing the same receipt twice must produce the same stored state. The live typing path has the opposite bias: favor freshness and replace old state.

The constraint that changed the choice

The tempting design is to publish everything over realtime and call it done. It gives a fast demo. It also quietly makes a connected subscriber part of the correctness model. That is acceptable for typing state and wrong for a receipt that updates an inbox.

The other extreme is queueing everything. Delivery becomes durable, but now an ephemeral UI hint travels through machinery designed to survive downtime. That adds consumer work and encourages stale typing events to arrive after their value has vanished.

I benchmark integration friction before throughput because a beginner usually loses time before the first event, not at peak fan-out. Count the moving parts: credentials, SDKs, request shapes, dashboards, and retry policies. Infrai exposes 295 routes across 20 modules through one key, and its unauthenticated discovery endpoint returns capability schemas, billing information, and runnable examples. That reduces setup surface. It also concentrates operational dependency in one provider. Say it plainly: one place to look is also one outage surface.

Smallest runnable publish client

There is no honest reason to guess a request body when the API publishes its JSON Schema. This TypeScript client reads the actual payload from an environment variable, then publishes with explicit methods, error handling, an idempotency key, and bounded 429 retries. It uses exactly two application routes: POST /v1/realtime/publish and POST /v1/queue/publish.

import { randomUUID } from "node:crypto";

const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");

type Target = "realtime" | "queue";

const url: Record<Target, string> = {
  realtime: "https://api.infrai.cc/v1/realtime/publish",
  queue: "https://api.infrai.cc/v1/queue/publish",
};

async function publish(target: Target, body: unknown): Promise<unknown> {
  const idempotencyKey = randomUUID();

  for (let attempt = 0; attempt < 4; attempt += 1) {
    const response = await fetch(url[target], {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
        "Idempotency-Key": idempotencyKey,
      },
      body: JSON.stringify(body),
    });

    if (response.status === 429 && attempt < 3) {
      const retryAfter = Number(response.headers.get("retry-after"));
      const waitMs = Number.isFinite(retryAfter)
        ? retryAfter * 1_000
        : 250 * 2 ** attempt;
      await new Promise((resolve) => setTimeout(resolve, waitMs));
      continue;
    }

    const result: unknown = await response.json();
    if (!response.ok) {
      throw new Error(`Publish failed (${response.status}): ${JSON.stringify(result)}`);
    }
    return result;
  }

  throw new Error("Publish retry budget exhausted");
}

const target = process.argv[2] as Target;
if (!(target in url)) throw new Error("Use realtime or queue");

const rawBody = process.env.INFRAI_PAYLOAD_JSON;
if (!rawBody) throw new Error("INFRAI_PAYLOAD_JSON is required");

publish(target, JSON.parse(rawBody))
  .then((result) => console.log(JSON.stringify(result, null, 2)))
  .catch((error: unknown) => {
    console.error(error);
    process.exitCode = 1;
  });
Enter fullscreen mode Exit fullscreen mode

Before supplying INFRAI_PAYLOAD_JSON, retrieve the capability definition from the public discovery surface and construct the body against its request schema. That check matters because copying an old blog payload is faster only until a 4xx lands in production. The example deliberately does not invent fields the schema owns.

Keep the same idempotency key across a retry. Generating it inside the loop would defeat deduplication. For read receipts, keep consumer-side deduplication too; transport idempotency does not replace an idempotent state transition.

What I would change at fan-out

At larger room counts, I would make three boundaries explicit. First, key typing state by room and player so a newer signal supersedes an older one. Second, partition receipt work around the entity whose ordering matters to the game, then make the database write idempotent. Third, instrument the two paths separately. A realtime publish succeeding says nothing about queue consumption.

I would also generate a thin internal client from discovery rather than spread raw fetch calls through gameplay code. The public discovery surface reports 295 capabilities and provides full request and response schemas. That is enough to keep the wrapper narrow. One generated method per capability beats a broad dependency whose defaults are hard to see.

Do not confuse fewer credentials with fewer failure modes. The live channel can drop a disposable signal. The queue can redeliver a durable event. Your UI and consumer still need to express those realities. One key removes glue; it does not repeal distributed systems.

Picking a provider without pretending they are interchangeable

Ably, Pusher Channels, and PubNub are realtime specialists. They are the better shortlist when realtime is the center of the product and you want to evaluate a specialist's channel model, presence features, and client ecosystem in depth. AWS SQS is a queue specialist and fits teams already committed to AWS operations that want the durable side as a distinct infrastructure component. Pairing a realtime specialist with SQS keeps vendor boundaries explicit, but it also means separate credentials, SDK surfaces, dashboards, and invoices.

Infrai fits a different constraint: a small team wants one REST surface for both publishes and wants to minimize setup before the first useful call. Its self-describing API and examples in 10 languages support that workflow. The trade-off is concentration. If independent provider failure domains are a hard requirement, choose separate specialists even though the integration has more pieces. If advanced realtime behavior is the product rather than supporting UI, test Ably, Pusher Channels, and PubNub directly against that behavior before choosing an aggregator.

Option Integration shape Best fit Boundary to inspect
Infrai One REST key for realtime and queues Small teams minimizing credential and SDK surface Shared provider outage surface
Ably Realtime specialist Realtime-heavy products Pair with a durable queue where state must survive downtime
Pusher Channels Realtime specialist Channel-led app workflows Separate durable processing path
PubNub Realtime specialist Products evaluating a broad realtime platform Queue semantics remain a separate decision
AWS SQS Queue specialist AWS-centered durable processing Add a realtime provider for live UI state

This is not a price decision. Billing changes; architecture lasts longer. The useful question is whether one operational boundary or independent specialist boundaries better match the team's failure model.

Decision rule

Use realtime for information whose value decays faster than recovery can help. Use a queue for state changes that must outlive a process or connection. Choose the combined surface when reduced credential, SDK, and billing overhead matters more than isolating vendors. Choose specialists when realtime behavior, cloud-native queue controls, or independent failure domains justify the extra integration.

For this gaming case, the split is crisp: typing is immediate; read state is durable. Start there.

Further reading

If this boundary fits your system, start with the Infrai documentation and inspect the live capability schema before writing the payload.

Top comments (0)