Developer guide

Build your first
realtime path.

Your backend publishes an update. Orastack delivers it to every authorized client and recovers anything missed during a disconnect.

Backend to browser in three steps

Keep your database and HTTP API. Add Orastack after the write when connected clients need to know that something changed.

Your backendOrastackAuthorized clients
Before you start

Create a project, a development environment, and a server API key in the console. Keep the server key on your backend. During private beta, the SDKs in this repository still use the @reelay package namespace.

Step 1

Create a scoped client token

Your backend authenticates the user with its existing session, then issues a short-lived token containing only the scopes and permissions that user needs.

backend · auth route
import { Reelay } from "@reelay/server"

const ora = new Reelay({
  apiKey: process.env.ORASTACK_SECRET!,
  baseUrl: process.env.ORASTACK_REALTIME_URL!
})

const { token } = await ora.tokens.create({
  userId,
  grants: [{
    scope: ora.resource("project", projectId),
    permissions: ["subscribe"]
  }]
})
Identity-boundUser, session, and device stay distinct.Default denyKnowing a resource ID never grants access.Environment-safeA development token cannot reach production.

Step 2

Subscribe in the browser

Create one client and subscribe it to any number of scopes. The SDK owns the WebSocket, reconnects it, restores subscriptions, and resumes each reliable ordering lane.

browser · client.ts
import { Reelay } from "@reelay/client"

const ora = new Reelay({
  token,
  url: process.env.NEXT_PUBLIC_ORASTACK_REALTIME_URL!
})
const project = ora.subscribe(
  ora.resource("project", projectId)
)

project.on("task.updated", event => {
  refreshTask(event.data.taskId)
})

One connection can subscribe to many users, groups, channels, or application resources. Never open one socket per scope.

Step 3

Publish after your database write

Normal .emit() calls are reliable. A successful response means the event has an ID, a server-assigned sequence, and a durable replay record.

backend · update-task.ts
await database.tasks.update(taskId, input)

const accepted = await ora
  .resource("project", projectId)
  .emit("task.updated", { taskId }, {
    idempotencyKey: `task_${taskId}_${version}`
  })

// { id, sequence, acceptedAt }
If the response times out

Retry with the same idempotency key. Orastack returns the original logical event instead of publishing a duplicate.

Events, state, and signals

They share the same scope model, but they make different promises.

Event

Something happened

.emit("invoice.paid", data)

Durable, ordered within its lane, replayable, and delivered at least once.

State

This is true now

.state.set({ progress: 73 })

A versioned latest value. Reconnecting clients receive the current document.

Signal

This is happening now

.signal("cursor.moved", point)

Low latency and non-durable. Stale values may be coalesced or dropped.

Disconnects are an ordinary state

The SDK persists bounded cursors, sends cumulative ACKs, and suppresses ordinary duplicate dispatch by event ID, lane, and sequence. Live delivery waits until replay finishes for that lane.

  1. 1003Client ACKs through sequence 1003
  2. —The connection disappears
  3. 1004–06Events are durably accepted while offline
  4. 1004–06The SDK reconnects and replays the gap
  5. 1007Live delivery resumes in order
At least once, not exactly once.

An ACK means the SDK accepted an event into its local dispatch path. It does not mean your application handler completed. If a cursor has expired, the SDK surfaces resync_required so you can refetch authoritative state.

Add hooks where they help

Use Orastack for live invalidation and synchronized UI. Keep the customer's database and HTTP API as the source of truth.

component · task-list.tsx
useEvent({
  scope: resource("project", projectId),
  event: "task.updated",
  onEvent: () => queryClient.invalidateQueries({
    queryKey: ["tasks", projectId]
  })
})

const job = useRealtimeState(resource("job", jobId))
const people = usePresence(resource("document", documentId))

Branch on stable codes

Every API and SDK error includes a machine-readable code. Do not parse the message text.

unauthenticatedunauthorizedinvalid_scopepayload_too_largerate_limitedtoken_expiredstate_version_conflictidempotency_conflictresync_required

Ship the first live path.

Start in a development environment, inspect the event in live tail, then test recovery by taking the browser offline.

Open the console