wss://api.socketly.co
Sockets you don’t have to run.
Publish from your server, subscribe from the browser. Presence rosters, signed private channels, per-app quotas and keys you can roll — on an endpoint you never deploy.
two connections opened when this page loaded
public-lobby-…clientAda
Connecting…
clientGrace
Connecting…
the wirebytes on arrival
Nothing has crossed yet. Send a message and watch the frame arrive.
channels
Three prefixes, and the prefix is the rule.
A channel’s name decides who may join it. There is no second permission model to keep in step with the first — the gateway parses the name.
public-Open to anyone holding the key
A public key is meant to sit in a browser. Anyone with it can subscribe and read; only your server can write. Right for tickers, build status, anything already on the page.
private-Your server decides who joins
The client asks your auth endpoint, which signs the subscription with your secret key. The gateway checks that signature before the socket joins the room. You already know who your user is — this is where you say so.
presence-Private, and it keeps the roster
Everything a private channel does, plus who is here. Members arrive with the subscription, and joins and departures are pushed as they happen. You do not maintain the list.
quickstart
Three steps, and the middle one is conditional.
One install, a public key and a secret key. Both keys are minted the moment you create an app, and the secret is shown exactly once.
- 01
Subscribe in the browser
The public key belongs here. That is what it is for.
import { Socketly } from '@socketly/client'; const socketly = new Socketly({ key: process.env.NEXT_PUBLIC_SOCKETLY_KEY!, authEndpoint: '/api/socketly/auth', }); socketly .subscribe('presence-room-42') .bind('new-message', ({ data }) => render(data)); - 02
Authorize, for private and presence channels
One endpoint, and the only step that asks anything of you. Public channels skip it.
import { authorizeChannel } from '@socketly/server'; export async function POST(request: Request) { const { socket_id, channel_name } = await request.json(); const user = await getCurrentUser(); if (!user) return new Response('Unauthorized', { status: 401 }); return Response.json( await authorizeChannel({ secret: process.env.SOCKETLY_SECRET!, socketId: socket_id, channel: channel_name, userData: { user_id: user.id, user_info: { name: user.name } }, }), ); } - 03
Publish from your server
The secret key never leaves it.
import { SocketlyServer } from '@socketly/server'; const socketly = new SocketlyServer({ secret: process.env.SOCKETLY_SECRET!, }); await socketly.trigger('presence-room-42', 'new-message', { text: 'Shipped.', });
The rest — channels, keys and the React hooks — is in the docs.
agents
Or let your agent do it — and check its own work.
@socketly/mcp gives a coding agent five tools and these docs as a resource. It can publish an event, watch the channel, and see the message arrive — so it finds out whether the code it just wrote works, instead of telling you it does.
claude mcp add socketly \
--env SOCKETLY_SECRET_KEY=sk_app_… \
--env SOCKETLY_PUBLIC_KEY=pk_app_… \
-- npx -y @socketly/mcp- socketly_publish
- send an event, as your server would
- socketly_subscribe
- start watching a channel
- socketly_receive
- read what has arrived
- socketly_channel_info
- who is in a presence channel
- socketly://docs
- these docs, as a resource it reads
Works with Claude Code, Claude Desktop, Cursor, Windsurf and anything else that speaks MCP. How it works.
operations
The part that isn’t the fun part.
Anyone can stand up a Socket.IO server in an afternoon. What takes the time is everything around it, afterwards, forever.
Quotas, by the minute and by the day
Every message passes one chokepoint, so nothing is counted twice and nothing slips past uncounted. When Redis is unreachable we refuse rather than give the product away.
Keys you can roll without an outage
A rolled secret stays valid for a grace window you choose, so servers already deployed keep verifying while you ship the new one. Set the window to zero for a leaked key and the old one dies on the spot.
Origins matched on the host
A public key is public; the origin list on your app is what makes it yours. Matched on the host, port included, with *.example.com when you want subdomains.
Usage you can actually read
Per-minute buckets, zero-filled across the range you asked for. Two busy minutes render as two spikes rather than as a full chart implying sustained load.
plans
Plans
Every number below is read from the constants the gateway enforces, so this page cannot drift from the product.
| Limit | Free$0/mo | Starter$19/mo | Pro$49/mo | Scale$199/mo |
|---|---|---|---|---|
| Messages a day | 200K | 1M | 5M | 25M |
| Messages a minute | 500 | 2K | 10K | 50K |
| Concurrent connections | 100 | 500 | 2,000 | 10,000 |
| Channels per connection | 20 | 100 | 500 | 2,000 |
| Message size | 32 KB | 64 KB | 128 KB | 256 KB |
| Apps | 2 | 10 | 50 | 1,000 |
| Usage history | 7 days | 30 days | 90 days | 365 days |
A message counts once when you publish it and once for each connected client we deliver it to, the same way the rest of this category counts. The free plan also includes one workspace.
Change plan or cancel from the dashboard.