Build with RTLayer
RTLayer delivers realtime messages from your servers to your users. Publish with one HTTPS request; every connected web or mobile client receives it over a websocket within milliseconds. No socket servers to build, scale or babysit.
How RTLayer works
Your server publishes over HTTPS; RTLayer pushes to every subscribed client over websockets.
RTLayer sits between your backend and your users’ apps. Your server sends a message to the HTTP API; RTLayer fans it out to every client connected to the same service over a websocket. Clients never publish: anything they need to send goes through your own backend, which keeps you in control of what is broadcast.
flow
- HTTP API:
https://api.rtlayer.com. Publish messages and create services. Authenticated with an API key. - Websocket:
wss://ws.rtlayer.com/<orgId>/<serviceId>/. Clients connect, join channels and receive messages. - Dashboard: rtlayer.com/dashboard. Create services, manage API keys and teammates, try messages in the Playground and watch live analytics.
- Sign in. The first visit to Home creates an organization, a service and an API key for you.
- Open your service on the Dashboard and go to API Keys. Copy the Organization ID, the Service ID and an API key.
- Connect a client and listen on a channel (step 3 below).
- Publish to that channel from your server (step 4 below).
- Watch it arrive. The service’s Playground tab can also send and receive test messages, with timings.
| Concept | What it is |
|---|---|
| Organization | Your team’s workspace. It holds services, teammates (owner, admins, members) and org-level API keys. |
| Service | One realtime app or environment, such as "Production chat" or "Staging". Each has its own ID, API keys, settings and analytics. Messages never cross services. |
| Org ID and service ID | Public identifiers. Clients need both to connect, so they are safe in frontend code. |
| API key | Secret credential for the HTTP API. A service key publishes to one service; an org key can publish to any service in the org and create services. See API keys. |
| Channel | A named stream inside a service, such as orders or room-42. Clients join the channels they care about; a message published to a channel reaches only those clients. |
| Broadcast | A message published without a channel. It reaches every client connected to the service. |
| Personal channel | Every connection is also subscribed to a channel named after its user ID (uid). Publishing to that name reaches one user. Useful with authentication, which sets the uid. |
| Message | Any string or JSON value. Strings are delivered as-is; other values are sent as their JSON text, so clients JSON.parse them. |
| TTL | Optional seconds to keep a message for a channel nobody is listening to yet. See Delivery and TTL. |
endpoint
| Header | Value |
|---|---|
x-api-key | Your service API key (or an org key; see below). The apiKey query parameter also works, but the header wins and keeps the key out of logs. |
Content-Type | application/json (form-encoded bodies work too) |
| Body field | Type | Description |
|---|---|---|
message | any, required | The payload. A string is delivered as-is; objects, arrays, numbers and booleans are delivered as their JSON text. Up to 8 MB, though small messages deliver fastest. |
channel | string, optional | Channel to publish to. Omit it (or send "") to broadcast to every client of the service. Any characters are allowed; colons are fine. |
ttl | integer seconds, optional | Keep the message for up to this long if no client is subscribed to the channel right now. Default 0 (deliver now or drop). See Delivery and TTL. |
serviceId | string | Only with an org API key: the service to publish to. Ignored for service keys. |
A successful call returns 200 with {"status":"ok"}. The response is the same whether the message reached clients, was stored for later, or found nobody listening; it is not a delivery receipt.
terminal
RTLayer(orgId, serviceId, token?, retryInterval?) returns a client that connects at once and reconnects every retryInterval ms (default 5000) when the connection drops, re-joining your channels. It needs a global WebSocket: browsers, React Native and Node 22+ have one.
app.js
Client API
| Call | What it does |
|---|---|
RTLayer(orgId, serviceId, token?, retryInterval = 5000) | Returns the client for that service. Calling it again with the same IDs returns the same client. |
on(channel, handler) | Joins channel and calls handler(message) for each message on it. Returns { remove() }. |
on("*", handler) | Every frame the connection receives, without joining anything. See the table below for what it gets. |
removeListener(channel, handler) | Removes a handler; leaves the channel once no handlers remain. |
subscribe(channel) / unsubscribe(channel) | Join or leave a channel without a handler (re-joined after reconnects). |
close() | Closes the socket. Note: the client then reconnects after retryInterval; reload or navigate away to stop it for good. |
| Handler | Channel message | Broadcast | System frames |
|---|---|---|---|
on("orders") | the payload string | — | — |
on("*") | the payload string (no channel name) | the raw frame: {"channel":"","message":"…"} | raw: {"status":"connected"} etc. |
on("message"), on("open") and on("close"): besides the event, they also join channels literally named "message", "open" and "close". Use on("*") for everything, and named channels for your data.The SDK is a thin layer over a small JSON protocol, so any language with a websocket client works.
| Step | Details |
|---|---|
| Connect | wss://ws.rtlayer.com/<orgId>/<serviceId>/, plus ?token=<jwt> when the service requires authentication. Servers may send Authorization: Bearer <jwt> instead. |
| On connect | The server sends {"status":"connected"}. You are already subscribed to broadcasts and to your personal channel (your uid). |
| Join a channel | Send {"action":"join","id":"orders"}. |
| Leave a channel | Send {"action":"leave","id":"orders"}. |
| Receive | Each message arrives as {"channel":"orders","message":"<payload>"}. channel is "" for broadcasts; message is always a string (JSON text for published objects). |
| Errors | {"status":"error","message":"Invalid JSON"} for frames the server can’t read; {"status":"error","message":"You are not allowed to join <id>"} when your token doesn’t allow the channel. The connection stays open. |
| Keep-alive | The server pings every connection and closes it after 30 s without a pong. Browsers and standard libraries answer pings automatically. |
| Reconnect | If the socket closes, reconnect and send your joins again; channel membership doesn’t survive a reconnect. |
Authenticate clients
Require signed tokens so only your users connect, and control which channels they join.
By default anyone who knows your org and service IDs can connect and join any channel. That’s fine for public data. For anything private, turn on authentication: clients must then present a JWT that your server signs with the service’s secret.
- In the dashboard, open the service’s Settings tab, switch on Require authentication and set a long random secret (owners and admins only).
- On your server, after you have authenticated the user, sign a token with that secret (HS256) carrying the claims below.
- Give the token to the client, which connects with it.
| Claim | Required | Meaning |
|---|---|---|
orgId | yes | Must equal the org ID in the connection URL. |
serviceId | yes | Must equal the service ID in the connection URL. |
uid | yes | Your user’s ID. It becomes the connection’s personal channel. |
room.allowed | yes, in practice | Channel patterns this user may join, e.g. ["orders", "room-*"]. * matches anything. If it’s missing, every join is refused. |
room.denied | no | Patterns to refuse even if allowed; deny wins. |
exp | recommended | Expiry (standard JWT claim). Expired tokens are rejected. |
token.js
- A missing, invalid or expired token, or IDs that don’t match the URL, fail the websocket handshake (HTTP 401). Browsers report this only as a failed connection.
- A join the token doesn’t allow gets an error frame; the connection stays open.
- Patterns are matched against the whole channel name. Stick to letters, digits,
-,_and:in channel names: other symbols such as.have special meaning in patterns. - Turning authentication on or changing the secret can take up to 10 minutes to apply to new connections.
- The JavaScript client is reused per org and service, so a fresh token takes effect after a page reload.
- Live delivery: a message goes to every client subscribed to its channel at that moment (all clients of the service for a broadcast). Messages you publish one after another arrive in that order; requests sent in parallel can arrive in any order.
- At most once: there are no acknowledgements or retries. Clients that are offline, reconnecting or too slow to keep up miss messages. Treat RTLayer as the fast path, and let clients re-sync from your API after a reconnect when every message matters.
- No history: messages are not stored, except as described for TTL below.
With ttl, a message published to a channel nobody is currently subscribed to is kept for up to ttl seconds. The first client to join that channel within that time receives it. Details worth knowing:
- If anyone is subscribed when you publish, the message is delivered live and
ttlhas no effect. - A stored message is handed out once: to the client(s) that join first, not to everyone who joins later.
- Identical messages stored for the same channel are kept once. Add an ID or timestamp if repeats matter.
- Stored messages are delivered in expiry order and are kept for at most about 24 hours, whatever the
ttl.
Org API keys and creating services
One key for every service in an organization, including creating services from code.
An org key belongs to an organization rather than a single service. Use it on servers that manage many services. Owners and admins create and disable org keys on the organization page; every member can see them.
Publish to any service
Send the org key in x-api-key and name the target with serviceId. The service must belong to the key’s organization.
terminal
Create a service
terminal
Both name and description are required. The service is created in the key’s organization and comes with a new service API key:
response
key.key from this response: API keys can’t list a service’s keys later (the dashboard can). Service keys get 403 This operation needs an org API key on this endpoint.| Service key | Org key | |
|---|---|---|
| Publishes to | its own service | any service in the org (with serviceId) |
| Creates services | no | yes (POST /service) |
| Where to manage | service → API Keys tab | organization page → API access |
| Who can create | any member | owners and admins |
| Who can disable | owners and admins | owners and admins |
- Every new service comes with one service key. Create more to rotate keys without downtime: add the new key to your servers, then disable the old one.
- Disabling is permanent; a disabled key can’t be re-enabled. It stops working within about a minute.
- A disabled key and a key that never existed both get
401 Invalid API Key.
| Limit | Value |
|---|---|
| Publish rate | 3,000 requests per 3 minutes per service, shared by all its keys (including org keys targeting it). Over the limit: 429 Too many requests, no Retry-After header. Requests that are rejected still count, so back off before retrying. |
| Request body | 8 MB for JSON, 100 KB for form-encoded bodies. Keep messages small for the fastest delivery. |
| Concurrent connections | 10,000 per service. Further connection attempts get HTTP 429 during the handshake. |
| Idle connections | Closed after 30 s without answering the server’s ping (handled automatically by clients). |
| Client frames | Up to 16 MB; clients only ever send small join/leave frames. |
| TTL storage | About 24 hours at most. |
Errors come back as JSON: {"status":"error","message":"<text>","code":<status>}. Validation errors are 400 with a list instead: {"status":"error","message":[{"field":"message","message":"Required"}]}.
| Status | Message | Meaning |
|---|---|---|
| 400 | [{"field":"message","message":"Required"}] | The body has no message. |
| 400 | [{"field":"ttl","message":"Expected number, received nan"}] | ttl isn’t a number. |
| 400 | serviceId is required when using an org API key | Org keys must say which service to publish to. |
| 401 | API Key not found | No x-api-key header or apiKey parameter. |
| 401 | Invalid API Key | The key doesn’t exist or was disabled. |
| 401 | Token not found | POST /service without credentials. |
| 403 | This operation needs an org API key | POST /service with a service key. |
| 404 | Service not found | An org key’s serviceId doesn’t exist or belongs to another org. |
| 429 | Too many requests | Publish rate limit; wait and retry with backoff. |
| 500 | Internal Server Error | Also returned for malformed JSON and bodies over the size limit. Check your JSON first. |
| Websocket | Meaning |
|---|---|
| Handshake fails with HTTP 401 | Authentication is on and the token is missing, invalid, expired or for other IDs. |
| Handshake fails with HTTP 429 | The service already has 10,000 connections. |
{"status":"error","message":"You are not allowed to join <id>"} | The token’s room.allowed / room.denied don’t permit the channel. |
{"status":"error","message":"Invalid JSON"} | The client sent a frame the server couldn’t parse. |
| Where | What you can do |
|---|---|
| Dashboard | Every service with its live connection count. Press Ctrl K (⌘K on Mac) to jump to any service or org. |
| Service → Overview | Connections right now, messages in the last 24 hours, message chart, active sessions. |
| Service → Playground | Send test messages and watch them arrive live, with the round-trip time of each one. |
| Service → API Keys | Org and service IDs, create and disable service keys. |
| Service → Settings | Require authentication and set the token secret. |
| Organization page | Org API keys, team summary, org-wide message analytics, rename. |
| Analytics | Sessions by device, platform, browser and origin; message volume over time. |
| Account menu → Team | Invite teammates by email (they join on their first sign-in) and change roles. |
| Role | Can |
|---|---|
| Owner | Everything an admin can do. The creator of the organization; can’t be removed from it. |
| Admin | Invite and remove members, change roles, create and disable org keys, disable service keys, change service authentication, rename the org. |
| Member | Create services and service keys, rename services, use the Playground and analytics, view keys. |
- /llms.txt: a short index of these docs in the llms.txt format.
- /llms-full.txt: this whole page as one Markdown file. The Copy as Markdown button at the top copies the same text.
- Ask the user for their org ID, service ID and a service API key (dashboard → service → API Keys). Never put the API key in client code; read it from an environment variable such as
RTLAYER_API_KEY. - Server side, publish with
POST https://api.rtlayer.com/message, headerx-api-key, JSON body{"channel": "...", "message": ...}. Check for{"status":"ok"}. - Client side, use
rtlayer-client:RTLayer(orgId, serviceId).on(channel, handler), andJSON.parsethe message when you published an object. - If the data is private, enable authentication and sign tokens on the server with
orgId,serviceId,uidandroom.allowed. - Verify end to end: subscribe, publish, and confirm the handler fires (or use the service’s Playground tab).
| Symptom | Check |
|---|---|
Publish returns ok but nothing arrives | The client uses the same org and service IDs as the key’s service; it joined the channel before you published (or use ttl); the channel names match exactly. |
Broadcasts show up as {"channel":"","message":…} | That’s the raw frame on("*") receives for broadcasts; unwrap it as shown in Broadcasts, or publish to a channel. |
| Objects arrive as strings | Expected: messages are delivered as text. JSON.parse them. |
| Client can’t connect after enabling authentication | Pass a token; check orgId/serviceId match the URL, the secret is current and the token hasn’t expired. Changes can take up to 10 minutes to apply. |
| Connected but joins are refused | The token needs room.allowed patterns that match the channel. |
401 Invalid API Key | The key was disabled or mistyped; copy a current one from the API Keys tab. |
429 Too many requests | Over 3,000 publishes in 3 minutes for this service: batch updates and back off. |
500 on publish | Usually malformed JSON or an oversized body. |