Realtime for your app,
without the plumbing.
Ship live dashboards, notifications, and collaborative features in an afternoon. Publish from your backend with one signed HTTP call. Subscribe in the browser with a few lines of JavaScript. We run the sockets.
No credit card. Free Sandbox plan — 200,000 messages a day, 100 concurrent connections. Your first channel is live in minutes.
// Publish an event. One signed POST — no socket to manage. await fetch(`/apps/${appId}/events`, { method: "POST", headers: { "X-Pulsely-Timestamp": ts, "X-Pulsely-Signature": signature }, body: JSON.stringify({ channel: "orders", event: "created", data: { id: 42, total: 1999 } }) });
// Subscribe. Your app key is public and safe to ship. const bp = new Pulsely("pl_your_app_key"); await bp.connect(); bp.subscribe("orders"); bp.bind("created", (order) => { renderOrder(order); });
Everything you need to go realtime. Nothing you don't.
A hosted pub/sub broker with tenant isolation, channel authorization, and usage metering built in — so you are not running a socket fleet to send a few thousand events.
Message history & replay
Late subscribers catch up automatically. On subscribe we replay recent messages in their original order, bounded by your plan's retention window, before the live stream starts. No separate backfill endpoint to build.
Hard tenant isolation
Every app is its own namespace. A connection authenticated for one app cannot subscribe outside it — enforced at the broker on every single subscribe, not by convention.
Private & presence channels
Channels prefixed private- call your own auth endpoint before a
subscription is allowed. presence- channels go further and keep a
live roster of who is in the room, with join and leave events — and a
user with three tabs open still counts as one member.
Signed trigger API
Publishing is a single HMAC-signed POST. The signature covers the path, body, and timestamp, so a captured request cannot be replayed against another app or with a swapped payload.
Standard STOMP, no lock-in
The wire protocol is STOMP over WebSocket. Our SDK is a thin convenience layer — any STOMP client can talk to us, so your realtime layer is never a one-way door.
Usage you can watch live
Messages, live connections, and peak concurrency stream into your dashboard as they happen — over Pulsely itself. We use our own product to build it.
From zero to a live channel
No infrastructure to provision, no connection pool to size, no sticky sessions to configure. The first three steps are all you need to ship; the last two are there when you want them.
Name your app and take your keys
An app is an isolated pub/sub namespace. Create one in the dashboard and three credentials appear immediately — no provisioning step, no waiting.
- App ID — identifies the app in the trigger API URL.
- App key — public. Ship it in your frontend bundle.
- App secret — server-side only. It signs your publishes and must never reach a browser.
Channels are created implicitly. There is nothing to register — publish to
orders and the channel exists.
# From the dashboard, per app APP_ID = "a3f19c42e8b7415d9c02e77a1b5d6e30" APP_KEY = "pl_9f2c41ad77be0c3518ea6b94" APP_SECRET = "3b8e...ac71" # keep on the server
Listen in the browser
One script tag and four lines. The SDK opens one WebSocket and multiplexes every channel over it, reconnecting on its own if the network drops.
connect()resolves once the broker has accepted your app key.subscribe()takes your channel name — the app namespace is added for you.bind()fires per event name, with the payload you published.
Subscribing before connect() resolves is fine; the SDK queues it and
re-establishes every subscription after a reconnect.
<script src="https://pulsely.dev/assets/js/pulsely.min.js"></script> <script> const bp = new Pulsely("pl_9f2c41ad77be0c3518ea6b94"); await bp.connect(); bp.subscribe("orders"); bp.bind("created", (data, meta) => { // meta = { channel, event, replayed } renderOrder(data); }); </script>
Publish with one signed POST
Your backend never opens a socket. It signs an HTTP request with the app secret and we fan it out to every subscriber on that channel.
- Sign
METHOD \n path \n timestamp \n sha256(body), joined by newlines. - HMAC-SHA256 with your app secret, lowercase hex.
- Send it as
X-Pulsely-Signaturewith the epoch seconds inX-Pulsely-Timestamp.
Binding the path and body hash into the signature means a captured request cannot be replayed against another app or with a swapped payload. Timestamps more than 10 minutes out are rejected.
import crypto from "node:crypto"; const body = JSON.stringify({ channel: "orders", event: "created", data: { id: 42, total: 1999 } }); const ts = String(Math.floor(Date.now() / 1000)); const path = `/apps/${APP_ID}/events`; const hash = crypto.createHash("sha256") .update(body).digest("hex"); const signature = crypto .createHmac("sha256", APP_SECRET) .update(`POST\n${path}\n${ts}\n${hash}`) .digest("hex"); await fetch(BASE_URL + path, { method: "POST", headers: { "Content-Type": "application/json", "X-Pulsely-Timestamp": ts, "X-Pulsely-Signature": signature }, body });
Keep private data private
Prefix a channel private- or presence- and nobody
subscribes without your say-so. We call your endpoint and honour its answer —
the authorization logic stays in your app, where the user session lives.
- Add a rule in the dashboard: a pattern like
private-order-*and your endpoint URL. - We POST
channel_name,socket_idanduser_tokento it. - Reply
{"authorized": true}to allow. - For
presence-channels, adduser_idanduser_infoand the subscriber joins the roster.
Anything other than an explicit yes denies — a timeout, a non-200, or a malformed body. Failures close the door rather than open it.
// POST https://your-app.com/pulsely/auth app.post("/pulsely/auth", (req, res) => { const { channel_name, user_token } = req.body; // Your session, your rules. const user = userFromToken(user_token); const orderId = channel_name.replace("private-order-", ""); res.json({ authorized: userCanSeeOrder(user, orderId) }); });
Late subscribers catch up on their own
On plans with history enabled, subscribing replays what it missed before the live stream starts — in original order, bounded by your plan's retention window. A tab opened five minutes late still renders the full picture.
- Nothing to call. Replay happens as part of
subscribe(). - Replayed messages hit the same
bind()handler as live ones. meta.replayedtells them apart when it matters.
This is the piece teams usually end up building themselves — a backfill endpoint, plus the ordering logic to stitch history onto the live stream.
bp.subscribe("orders"); bp.bind("created", (data, meta) => { if (meta.replayed) { // Backfill quietly — no toast, no sound. appendOrder(data); return; } appendOrder(data); notify(`New order ${data.id}`); });
Start free. Upgrade when your traffic does.
Every plan includes tenant isolation, the signed trigger API, and the live usage dashboard.
No credit card required
- ✓200,000 messages / day
- ✓100 concurrent connections
- × Private channels
- × Presence channels
- × Message history & replay
Billed monthly, cancel anytime
- ✓1,000,000 messages / day
- ✓500 concurrent connections
- ✓ Private channels
- × Presence channels
- × Message history & replay
Billed monthly, cancel anytime
- ✓4,000,000 messages / day
- ✓2,000 concurrent connections
- ✓ Private channels
- ✓ Presence channels
- ✓ Message history — 24h replay
Your first realtime feature is 10 minutes away.
Create a free account, grab your keys, and publish your first event before your coffee gets cold.
Free forever on Sandbox · No credit card · Upgrade only when you outgrow it