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.

  1. Sign up and create an app in the dashboard.
  2. Copy the app_id, app_key, and app_secret.
  3. 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
FieldTypeDescription
namestringEvent name delivered to subscribers.
channelsstring[]One or more channels. Max 100.
datastring | objectPayload. Serialized to JSON if object.
socket_idstring?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 channel
  • sf_subscribe — listen on a channel for N seconds, return collected events
  • sf_ask_agent — request/response to a SocketFlare agent
  • sf_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

CodeMeaning
400Invalid request body or parameters.
401HMAC signature missing, expired, or invalid.
403Quota exceeded. Upgrade your plan.
404App not found.
413Payload too large (>10KB default).
429Rate 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).