Quickstart
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.
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.
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"]
}]
})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.
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.
await database.tasks.update(taskId, input)
const accepted = await ora
.resource("project", projectId)
.emit("task.updated", { taskId }, {
idempotencyKey: `task_${taskId}_${version}`
})
// { id, sequence, acceptedAt }Retry with the same idempotency key. Orastack returns the original logical event instead of publishing a duplicate.
Choose the behavior
Events, state, and signals
They share the same scope model, but they make different promises.
Something happened
.emit("invoice.paid", data)Durable, ordered within its lane, replayable, and delivered at least once.
This is true now
.state.set({ progress: 73 })A versioned latest value. Reconnecting clients receive the current document.
This is happening now
.signal("cursor.moved", point)Low latency and non-durable. Stale values may be coalesced or dropped.
Recovery
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.
- 1003Client ACKs through sequence 1003
- —The connection disappears
- 1004–06Events are durably accepted while offline
- 1004–06The SDK reconnects and replays the gap
- 1007Live delivery resumes in order
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.
React
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.
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))Errors
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_requiredNext
Ship the first live path.
Start in a development environment, inspect the event in live tail, then test recovery by taking the browser offline.