SocketFlare documentation
SocketFlare is a realtime messaging platform. You publish events to channels via our HTTP API; every client subscribed to that channel receives them over an open WebSocket. You pay per publish — delivery is always free, no matter how many users are listening.
The API and WebSocket protocol are Pusher-compatible. Every official Pusher SDK — for Laravel, Node, Ruby, Python, Go, iOS, Android, Flutter, React Native — works with SocketFlare by changing the host. No rewrites. No new concepts to learn.
Quickstart
Three steps to your first realtime event.
- Sign up and create an app in the dashboard.
- Copy the
app_id,app_key, andapp_secret. - Publish and subscribe with the snippets below.
# Server (Node.js)
import Pusher from "pusher";
const sf = new Pusher({
appId: APP_ID,
key: APP_KEY,
secret: APP_SECRET,
host: "api.socketflare.com",
port: 443,
useTLS: true,
});
await sf.trigger("orders", "order-updated", { id: 42 });
// Client (Browser)
import Pusher from "pusher-js";
const sf = new Pusher(APP_KEY, {
wsHost: "ws.socketflare.com",
wsPort: 443, wssPort: 443, forceTLS: true,
enabledTransports: ["ws", "wss"],
cluster: "edge",
});
sf.subscribe("orders").bind("order-updated", (data) => {
console.log(data); // { id: 42 }
});
Authentication
Server-side publishes are authenticated with an HMAC-SHA256 signature over the request. Every official Pusher server library handles this automatically — you just provide the key and secret.
Client connections use the public app_key. Private and presence channels are authorized by a short-lived token your server generates for the subscribing user (see Private channels).
String to sign
<METHOD>\n<path>\nauth_key=...&auth_timestamp=...&auth_version=1.0&body_md5=...
The signature is HMAC-SHA256(app_secret, string_to_sign), hex-encoded, passed as the auth_signature query parameter. Timestamps outside a 10-minute window are rejected.
Publish API (REST)
POST https://api.socketflare.com/apps/:app_id/events
| Field | Type | Description |
|---|---|---|
| name | string | Event name delivered to subscribers. |
| channels | string[] | One or more channels. Max 100. |
| data | string | object | Payload. Serialized to JSON if object. |
| socket_id | string? | Exclude this socket (the publisher's own). |
// Raw request (any language, if you want to roll your own)
const body = JSON.stringify({
name: "order-updated",
channels: ["orders"],
data: JSON.stringify({ id: 42 }),
});
const ts = Math.floor(Date.now() / 1000);
const md5 = crypto.createHash("md5").update(body).digest("hex");
const params = new URLSearchParams({
auth_key: APP_KEY, auth_timestamp: ts, auth_version: "1.0", body_md5: md5,
});
const path = `/apps/${APP_ID}/events`;
const toSign = `POST\n${path}\n${[...params].sort().map(([k,v]) => k+"="+v).join("&")}`;
params.set("auth_signature", crypto.createHmac("sha256", APP_SECRET).update(toSign).digest("hex"));
await fetch(`https://api.socketflare.com${path}?${params}`, { method: "POST", body });
Batch publishing
Send the same event to up to 100 channels in one request. Each channel counts as one API call against your quota.
await sf.trigger(
["orders", "shipments", "analytics"],
"order-updated",
{ id: 42 }
);
Excluding a socket (avoid echo)
If a user publishes an event (via your backend) and already has the page open, you usually don't want them to receive their own echo. Pass the connection's socket_id as the socket_id parameter to exclude it.
// Client — read socket_id from the Pusher instance
const socketId = pusher.connection.socket_id;
// Include it in your AJAX request to your backend.
// Server
await sf.trigger("orders", "order-updated", { id: 42 }, { socket_id: req.body.socket_id });
JavaScript (pusher-js)
The official pusher-js library works unchanged. Just set the host.
npm install pusher-js
import Pusher from "pusher-js";
const sf = new Pusher(APP_KEY, {
wsHost: "ws.socketflare.com",
wsPort: 443, wssPort: 443, forceTLS: true,
enabledTransports: ["ws", "wss"],
cluster: "edge",
// For private/presence channels, point at your own auth endpoint:
// authEndpoint: "/socketflare/auth",
});
// Public channel
const orders = sf.subscribe("orders");
orders.bind("order-updated", (data) => console.log(data));
// Unsubscribe when you're done
sf.unsubscribe("orders");
React / Next.js
Use pusher-js directly in a useEffect. Keep the Pusher instance in a ref or module-level singleton so it's created once per tab.
"use client";
import { useEffect, useState } from "react";
import Pusher from "pusher-js";
let sf; // module-level singleton — one instance per tab
export function useOrdersFeed() {
const [orders, setOrders] = useState([]);
useEffect(() => {
sf ??= new Pusher(process.env.NEXT_PUBLIC_SF_KEY, {
wsHost: "ws.socketflare.com",
wsPort: 443, wssPort: 443, forceTLS: true,
enabledTransports: ["ws", "wss"],
cluster: "edge",
});
const ch = sf.subscribe("orders");
ch.bind("order-updated", (o) => setOrders((prev) => [o, ...prev]));
return () => sf.unsubscribe("orders");
}, []);
return orders;
}
Vue / Nuxt
<script setup>
import { onMounted, onBeforeUnmount, ref } from "vue";
import Pusher from "pusher-js";
const orders = ref([]);
let sf, channel;
onMounted(() => {
sf = new Pusher(import.meta.env.VITE_SF_KEY, {
wsHost: "ws.socketflare.com",
wsPort: 443, wssPort: 443, forceTLS: true,
enabledTransports: ["ws", "wss"],
cluster: "edge",
});
channel = sf.subscribe("orders");
channel.bind("order-updated", (o) => orders.value.unshift(o));
});
onBeforeUnmount(() => {
if (sf) sf.unsubscribe("orders");
});
</script>
React Native
npm install pusher-js
import Pusher from "pusher-js/react-native";
const sf = new Pusher(APP_KEY, {
wsHost: "ws.socketflare.com",
wsPort: 443, wssPort: 443, forceTLS: true,
enabledTransports: ["ws", "wss"],
cluster: "edge",
});
sf.subscribe("chat").bind("message", (m) => console.log(m));
Flutter
Use the official pusher_channels_flutter plugin with our host.
# pubspec.yaml dependencies: pusher_channels_flutter: ^2.2.1
import 'package:pusher_channels_flutter/pusher_channels_flutter.dart'; final sf = PusherChannelsFlutter.getInstance(); await sf.init( apiKey: 'YOUR_APP_KEY', cluster: 'edge', host: 'ws.socketflare.com', wsPort: 443, wssPort: 443, useTLS: true, onEvent: (event) => print(event), ); await sf.connect(); await sf.subscribe(channelName: 'orders');
iOS (Swift)
Use the official PusherSwift library.
// Podfile pod 'PusherSwift', '~> 10.1'
import PusherSwift
let options = PusherClientOptions(
host: .host("ws.socketflare.com"),
port: 443,
useTLS: true
)
let pusher = Pusher(key: "YOUR_APP_KEY", options: options)
let channel = pusher.subscribe("orders")
channel.bind(eventName: "order-updated") { event in
print(event.data ?? "")
}
pusher.connect()
Android (Kotlin)
Use the official pusher-websocket-java library.
// build.gradle implementation 'com.pusher:pusher-java-client:2.4.4'
import com.pusher.client.Pusher
import com.pusher.client.PusherOptions
val options = PusherOptions()
.setHost("ws.socketflare.com")
.setWsPort(443)
.setWssPort(443)
.setUseTLS(true)
val pusher = Pusher("YOUR_APP_KEY", options)
val channel = pusher.subscribe("orders")
channel.bind("order-updated") { event ->
println(event.data)
}
pusher.connect()
Laravel
SocketFlare is a drop-in Pusher replacement for Laravel's broadcasting system. Keep your ShouldBroadcast events, Echo listeners, and channel routes exactly as they are — only .env changes.
1. .env
BROADCAST_DRIVER=pusher
PUSHER_APP_ID=your-app-id
PUSHER_APP_KEY=your-app-key
PUSHER_APP_SECRET=your-app-secret
PUSHER_APP_CLUSTER=edge
PUSHER_HOST=api.socketflare.com
PUSHER_PORT=443
PUSHER_SCHEME=https
VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
VITE_PUSHER_HOST=ws.socketflare.com
VITE_PUSHER_PORT=443
2. config/broadcasting.php
'connections' => [
'pusher' => [
'driver' => 'pusher',
'key' => env('PUSHER_APP_KEY'),
'secret' => env('PUSHER_APP_SECRET'),
'app_id' => env('PUSHER_APP_ID'),
'options' => [
'cluster' => env('PUSHER_APP_CLUSTER', 'edge'),
'host' => env('PUSHER_HOST', 'api.socketflare.com'),
'port' => env('PUSHER_PORT', 443),
'scheme' => env('PUSHER_SCHEME', 'https'),
'encrypted' => true,
'useTLS' => true,
],
],
],
3. Broadcast an event
<?php
// app/Events/OrderShipped.php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
class OrderShipped implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets;
public function __construct(public Order $order) {}
public function broadcastOn(): Channel
{
return new Channel('orders');
}
public function broadcastAs(): string
{
return 'order-updated';
}
}
// Dispatch from anywhere
broadcast(new OrderShipped($order));
4. Laravel Echo (frontend)
// resources/js/bootstrap.js
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
window.Pusher = Pusher;
window.Echo = new Echo({
broadcaster: 'pusher',
key: import.meta.env.VITE_PUSHER_APP_KEY,
cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER,
wsHost: import.meta.env.VITE_PUSHER_HOST,
wsPort: import.meta.env.VITE_PUSHER_PORT,
wssPort: import.meta.env.VITE_PUSHER_PORT,
forceTLS: true,
enabledTransports: ['ws', 'wss'],
});
Echo.channel('orders')
.listen('.order-updated', (e) => {
console.log(e.order);
});
5. Private channels
// routes/channels.php
Broadcast::channel('orders.{userId}', function (\$user, \$userId) {
return (int) \$user->id === (int) \$userId;
});
// app/Events/OrderShipped.php — use PrivateChannel instead
return new PrivateChannel('orders.' . \$this->order->user_id);
Laravel Echo will POST to /broadcasting/auth for authorization. SocketFlare uses the same JWT format Pusher does, so no changes are needed.
Node.js
npm install pusher
import Pusher from "pusher";
const sf = new Pusher({
appId: process.env.SF_APP_ID,
key: process.env.SF_APP_KEY,
secret: process.env.SF_APP_SECRET,
host: "api.socketflare.com",
port: 443,
useTLS: true,
});
// Single publish
await sf.trigger("orders", "order-updated", { id: 42 });
// Batch publish
await sf.trigger(["orders", "analytics"], "order-updated", { id: 42 });
// Exclude a socket (avoid echo to the publisher)
await sf.trigger("orders", "order-updated", { id: 42 }, {
socket_id: req.body.socket_id,
});
PHP (plain, no framework)
composer require pusher/pusher-php-server
<?php
require __DIR__ . '/vendor/autoload.php';
$sf = new Pusher\Pusher(
'YOUR_APP_KEY',
'YOUR_APP_SECRET',
'YOUR_APP_ID',
[
'cluster' => 'edge',
'host' => 'api.socketflare.com',
'port' => 443,
'scheme' => 'https',
'useTLS' => true,
]
);
$sf->trigger('orders', 'order-updated', ['id' => 42]);
Ruby / Rails
# Gemfile gem "pusher"
# config/initializers/pusher.rb
require "pusher"
Pusher.app_id = ENV["SF_APP_ID"]
Pusher.key = ENV["SF_APP_KEY"]
Pusher.secret = ENV["SF_APP_SECRET"]
Pusher.host = "api.socketflare.com"
Pusher.port = 443
Pusher.scheme = "https"
Pusher.encrypted = true
# Publish from anywhere
Pusher.trigger("orders", "order-updated", { id: 42 })
Python / Django
pip install pusher
import pusher
sf = pusher.Pusher(
app_id="YOUR_APP_ID",
key="YOUR_APP_KEY",
secret="YOUR_APP_SECRET",
cluster="edge",
host="api.socketflare.com",
port=443,
ssl=True,
)
sf.trigger("orders", "order-updated", {"id": 42})
# Batch publish
sf.trigger(["orders", "analytics"], "order-updated", {"id": 42})
Go
go get github.com/pusher/pusher-http-go/v5
package main
import "github.com/pusher/pusher-http-go/v5"
func main() {
sf := &pusher.Client{
AppID: "YOUR_APP_ID",
Key: "YOUR_APP_KEY",
Secret: "YOUR_APP_SECRET",
Host: "api.socketflare.com",
Secure: true,
}
sf.Trigger("orders", "order-updated", map[string]int{"id": 42})
}
Java / Kotlin
// build.gradle implementation 'com.pusher:pusher-http-java:1.2.4'
import com.pusher.rest.Pusher;
Pusher sf = new Pusher("YOUR_APP_ID", "YOUR_APP_KEY", "YOUR_APP_SECRET");
sf.setHost("api.socketflare.com");
sf.setEncrypted(true);
Map<String, Integer> payload = new HashMap<>();
payload.put("id", 42);
sf.trigger("orders", "order-updated", payload);
.NET / C#
dotnet add package PusherServer
using PusherServer;
var options = new PusherOptions {
HostName = "api.socketflare.com",
Encrypted = true,
Cluster = "edge",
};
var sf = new Pusher("YOUR_APP_ID", "YOUR_APP_KEY", "YOUR_APP_SECRET", options);
await sf.TriggerAsync("orders", "order-updated", new { id = 42 });
Elixir / Phoenix
# mix.exs
{:pusher, "~> 0.5"}
# config/config.exs
config :pusher,
app_id: "YOUR_APP_ID",
key: "YOUR_APP_KEY",
secret: "YOUR_APP_SECRET",
host: "api.socketflare.com",
port: 443,
scheme: "https"
# Publish
Pusher.trigger("orders", "order-updated", %{id: 42})
Public channels
Public channels are the default. Any client with the app key can subscribe. Use them for broadcast data that isn't private.
// Client
sf.subscribe("orders").bind("order-updated", console.log);
// Server
await server.trigger("orders", "order-updated", { id: 42 });
Channel names must be ≤ 200 characters and contain only [A-Za-z0-9_=@,.;-].
Private channels
Private channels require server-side authorization. Prefix the channel name with private-. Your backend signs a short token granting the requesting user access.
Client
const sf = new Pusher(APP_KEY, {
wsHost: "ws.socketflare.com", wsPort: 443, wssPort: 443,
forceTLS: true, cluster: "edge",
authEndpoint: "/socketflare/auth", // your backend
});
sf.subscribe("private-user-123").bind("new-message", console.log);
Server (Node) — auth endpoint
app.post("/socketflare/auth", (req, res) => {
const { socket_id, channel_name } = req.body;
if (!currentUserIsAllowed(req.user, channel_name)) {
return res.status(403).end();
}
const auth = sf.authorizeChannel(socket_id, channel_name);
res.json(auth);
});
Server (Laravel)
// routes/channels.php
Broadcast::channel('user.{userId}', function (\$user, \$userId) {
return (int) \$user->id === (int) \$userId;
});
Presence channels
Presence channels extend private channels with a member list. Use them for "who's online" indicators. Prefix the channel name with presence-.
// Server auth endpoint returns user identity
app.post("/socketflare/auth", (req, res) => {
const { socket_id, channel_name } = req.body;
const auth = sf.authorizeChannel(socket_id, channel_name, {
user_id: req.user.id,
user_info: { name: req.user.name, avatar: req.user.avatar_url },
});
res.json(auth);
});
// Client
const ch = sf.subscribe("presence-room-42");
ch.bind("pusher:subscription_succeeded", (members) => {
console.log("Members:", members.count);
members.each((m) => console.log(m.id, m.info));
});
ch.bind("pusher:member_added", (m) => console.log(m.id, "joined"));
ch.bind("pusher:member_removed", (m) => console.log(m.id, "left"));
Client events
Clients can emit events directly to other subscribers on private and presence channels, without a round-trip through your server. Event names must start with client-.
const ch = sf.subscribe("private-chat-42");
ch.bind("pusher:subscription_succeeded", () => {
ch.trigger("client-typing", { user: "alice" });
});
ch.bind("client-typing", (data) => {
showTypingIndicator(data.user);
});
Client events are useful for ephemeral signals like typing indicators or cursor positions.
AI agents on SocketFlare
SocketFlare ships two first-class tools for AI workflows: a CLI with a built-in agent protocol, and an MCP server that exposes realtime publish/subscribe to Claude Desktop, Cursor, Continue, or any MCP client. They speak the same channel layout, so a Claude Desktop user can ask a question that's served by a CLI process running on a server somewhere — over a SocketFlare channel.
Concretely:
- Two CLIs piping to each other through a private channel —
socketflare pipe room - One CLI hosting an agent (a shell script, a Python program, a Claude Code session) —
socketflare agent serve bob --exec "..." - An MCP-equipped chat (Claude Desktop, Cursor) calling that agent —
sf_ask_agent("bob", "...") - Multi-agent orchestration: one agent fans out work to many, each replies on its own private channel
Everything runs on the public Pusher protocol — no proprietary primitives. If your client speaks Pusher, it speaks the agent protocol.
CLI
Install:
npm i -g @socketflare/cli socketflare login # paste an sf_acct_... token from /dashboard?tab=settings socketflare apps create my-bot # → app saved locally; subsequent commands use it by default
Pub / sub
# In terminal A — listen
socketflare subscribe orders
# In terminal B — publish
socketflare publish orders order.created '{"id":"A1029","total":48.00}'
Two-CLI pipe (terminal chat)
Identical command on each side; messages relay over a private channel via WebSocket-to-WebSocket client events. No HTTP publish, no quota cost.
# On Alice's machine socketflare pipe room-42 # On Bob's machine socketflare pipe room-42 # → bidirectional terminal-to-terminal chat
Agent serve / ask
An agent is anything that can read from stdin and write to stdout — a shell pipeline, a Python script, an LLM CLI. The serve command turns it into a network-callable service over a SocketFlare channel.
# Serve: an uppercase echo bot socketflare agent serve shouter --exec "tr a-z A-Z" # Serve: any LLM CLI as the brain socketflare agent serve claude-bot --exec "claude --print" # Ask, from anywhere with the same app credentials socketflare agent ask shouter "hello world" # → HELLO WORLD socketflare agent ask claude-bot "summarize this in one sentence: ..."
MCP server (Claude Desktop, Cursor, Continue)
SocketFlare ships an MCP server (@socketflare/mcp) that exposes six tools to any MCP-aware host:
sf_publish— publish an event to a channelsf_subscribe— listen on a channel for N seconds, return collected eventssf_ask_agent— request/response to a SocketFlare agentsf_list_apps,sf_create_app,sf_app_usage
Claude Desktop config
Edit ~/Library/Application Support/Claude/claude_desktop_config.json on macOS:
{
"mcpServers": {
"socketflare": {
"command": "npx",
"args": ["-y", "@socketflare/mcp"],
"env": {
"SOCKETFLARE_TOKEN": "sf_acct_...",
"SOCKETFLARE_APP_ID": "...",
"SOCKETFLARE_APP_KEY": "sf_...",
"SOCKETFLARE_APP_SECRET": "..."
}
}
}
}
Restart Claude Desktop. The six tools appear under the hammer icon. Now ask Claude:
"Subscribe to channel orders for 30 seconds and summarize what came through."
Or, with an agent running somewhere via socketflare agent serve bob --exec ...:
"Ask agent bob to refactor this function and bring me the result."
Agent protocol
A small convention layered on top of standard Pusher channels. No server-side support needed — it's all client-side wiring on top of publish/subscribe.
Channels
private-agent.<name>— agent listens here for incoming requests.private-reply.<random_id>— caller listens here for the reply (one-shot).
Request envelope
Event name agent.request:
{
"id": "abcdef123456", // unique per request
"msg": "your prompt or task",
"reply_to": "private-reply.abcdef123", // where the agent must publish its reply
"from": "<caller socket_id>",
"ts": 1715296000000
}
Reply envelope
Event name agent.reply, published on the reply_to channel:
{
"id": "abcdef123456", // matches the request
"msg": "result text or JSON",
"ts": 1715296000123
}
That's it. The CLI's agent serve / ask and the MCP's sf_ask_agent are both thin wrappers around this. You can implement either side in any language with a Pusher SDK.
Connection states
The pusher-js client exposes a connection state you can subscribe to:
sf.connection.bind("state_change", (states) => {
console.log("from", states.previous, "to", states.current);
});
// Possible values: initialized, connecting, connected,
// unavailable, failed, disconnected
Error codes
| Code | Meaning |
|---|---|
| 400 | Invalid request body or parameters. |
| 401 | HMAC signature missing, expired, or invalid. |
| 403 | Quota exceeded. Upgrade your plan. |
| 404 | App not found. |
| 413 | Payload too large (>10KB default). |
| 429 | Rate limit hit. Back off and retry. |
Limits & quotas
- Max payload size: 10 KB per event (default). Contact support to raise.
- Max channels per publish: 100.
- Max channel name length: 200 characters.
- Idle connections: unlimited and free — they don't count against compute.
- Per-app rate limits: configured per plan; returns 429 on overage.
- Over-quota: blocked with 403 (optionally metered with overage billing).