Guides

Realtime Pub/Sub

Intermediate~10 min

Subscribe to project-scoped realtime channel aliases and publish developer-facing events without using internal Phoenix topics.

Prerequisites

  • A Meteorack project id.
  • An API key with `cloud.realtime:subscribe` and, for publishing, `cloud.realtime:publish`.
  • A JavaScript or TypeScript app that can open a WebSocket connection.

What You Will Finish With

  • Mint an alias-safe realtime token.
  • Join the public `project-events` stream without seeing raw `project:*` topics.
  • Publish developer-facing events through the public control-plane API.

Meteorack Realtime exposes public channel aliases, not raw Phoenix topics. Your client joins names like project-events; the platform maps them to the internal transport topics server-side.

Public channel aliases

Current project-scoped aliases:

  • project-events developer-facing event stream; publishable; ephemeral
  • storage-audit subscribe-only storage audit stream; resumable
  • storage-usage subscribe-only storage usage stream; ephemeral
  • storage-objects subscribe-only storage object change stream; ephemeral

Only project-events is currently publishable from the public API.

1. Mint a subscription token

Call the public token route with an API key that has cloud.realtime:subscribe.

If your key is channel-restricted, request the exact aliases you want with the channels query parameter. That keeps the scope check unambiguous.

const response = await fetch(
  `${apiBase}/api/v1/cloud/projects/${projectId}/realtime/token?channels=project-events,storage-audit`,
  {
    headers: {
      Authorization: `Bearer ${apiKey}`,
    },
  },
);

if (!response.ok) {
  throw new Error(`Token request failed with ${response.status}`);
}

const { data } = await response.json();

Successful responses look like this:

{
  "data": {
    "token": "eyJhdWQiOiJtZXRlb3JhY2stcHJvamVjdC1yZWFsdGltZSIsLi4u",
    "ws_url": "wss://realtime.meteorack.com/socket",
    "engine": "phoenix",
    "expires_at": "2026-04-22T21:10:00.000Z",
    "channels": [
      {
        "name": "project-events",
        "deliveryClass": "ephemeral",
        "publishable": true
      },
      {
        "name": "storage-audit",
        "deliveryClass": "resumable",
        "publishable": false
      }
    ]
  }
}

The token payload does not expose internal project:<id> topics.

2. Connect and join a public alias

Use the Phoenix JavaScript client and join the alias directly.

import { Socket } from "phoenix";

const socket = new Socket(data.ws_url, {
  params: { token: data.token },
});

socket.connect();

const projectEvents = socket.channel("project-events");

projectEvents.on("inventory.updated", (payload) => {
  console.log("inventory update", payload);
});

await projectEvents.join();

storage-audit, storage-usage, and storage-objects join the same way. The alias stays stable even if the internal transport changes later.

3. Publish a developer-facing event

Publishing uses the public control-plane route and requires a key with cloud.realtime:publish. Public browser keys are subscribe-only; use a secret key for publish calls.

const eventId = crypto.randomUUID();
const response = await fetch(
  `${apiBase}/api/v1/cloud/projects/${projectId}/realtime/publish`,
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${secretApiKey}`,
      "Content-Type": "application/json",
      "Idempotency-Key": eventId,
    },
    body: JSON.stringify({
      channel: "project-events",
      event: "inventory.updated",
      data: {
        sku: "sku_1",
        available: 12,
      },
    }),
  },
);

const result = await response.json();
if (
  response.headers.get("Idempotency-Key") !== eventId ||
  result.data?.event_id !== eventId
) {
  throw new Error("Realtime publish identity mismatch");
}

Reuse the same eventId whenever you retry this logical publish after a timeout or other ambiguous result. Only project-events is currently accepted by the public publish route. Subscribe-only aliases return a validation error if you try to publish to them.

Failure semantics

Two failure classes matter most in practice:

  • Token issuance can return:
    • 402 when the next connection would exceed quota or a spending cap
    • 429 when the live connection gate is already at its configured ceiling
  • Publish can return:
    • 429 with publish_rate_limited when the runtime throttle is active on that channel

In practice this means:

  • fetch a new token shortly before connecting
  • handle token 402 and 429 separately from general auth failures
  • back off on publish 429 instead of retrying immediately