Channels

Channels are the fundamental way to organize and route real-time messages in Soketify. Pick the right channel type for your use case and everything else follows from there.

Channel Types Overview

Soketify supports five channel types, distinguished entirely by their name prefix. You do not configure channel types separately; the prefix is the type.

TypePrefixAuth RequiredUser TrackingE2E Encrypted
PublicNoneNoNoNo
Privateprivate-YesNoNo
Encryptedprivate-encrypted-YesNoYes
Presencepresence-YesYesNo
Cachecache-Optional*NoNo

* Cache channels can be public (cache-), private (private-cache-), or presence (presence-cache-).

Public Channels

Public channels have no prefix and require no authentication. Any client that knows your App Key can subscribe. Use them for data you are comfortable broadcasting openly.

Use Cases

  • Live scoreboards and sports feeds
  • Public announcements and notifications
  • Real-time price tickers
  • Activity streams visible to everyone

Example

public-channel.js
javascript
1// Client: subscribe and listen
2const channel = pusher.subscribe("live-scores");
3
4channel.bind("score-update", (data) => {
5  console.log(`${data.team}: ${data.score}`);
6});
7
8// Server: trigger an event
9await pusher.trigger("live-scores", "score-update", {
10  team: "Home",
11  score: 3,
12  minute: 67,
13});

Warning

Anyone with your App Key can subscribe to public channels. Do not broadcast sensitive user data, private messages, or anything you would not want the whole internet to read.

Private Channels

Private channels use the private- prefix. Before the client can subscribe, it must pass an authorization check against an endpoint on your server. Only clients your server approves will receive events.

Use Cases

  • Per-user notification feeds (e.g., order updates)
  • Admin-only event streams
  • Private DMs between specific users
  • Financial or medical data feeds

Example

private-channel.js
javascript
1// Client: subscribe — auth happens automatically
2const channel = pusher.subscribe("private-user-42");
3
4channel.bind("pusher:subscription_succeeded", () => {
5  console.log("Auth passed, subscribed!");
6});
7
8channel.bind("pusher:subscription_error", (err) => {
9  console.error("Auth failed:", err.status);
10});
11
12channel.bind("order-update", (data) => {
13  console.log("Order:", data.orderId, "→", data.status);
14});
15
16// Server: trigger an event to a specific user
17await pusher.trigger("private-user-42", "order-update", {
18  orderId: "ORD-1234",
19  status: "shipped",
20  trackingUrl: "https://track.example.com/ORD-1234",
21});

See the Authentication guide for how to implement the server-side auth endpoint.

Encrypted Channels

Encrypted channels use the private-encrypted- prefix. They work like private channels, but event payloads are encrypted end-to-end using NaCl Secretbox before they leave your server. Soketify routes the ciphertext but cannot read it. cannot read it. Only subscribers who have been through your auth endpoint get the decryption key.

Use Cases

  • Healthcare data that must stay confidential end-to-end
  • Financial transactions or account data
  • Any scenario where you need to guarantee that the WebSocket infrastructure itself cannot inspect your messages

Server Setup

Encrypted channels require a 32-byte master encryption key on your server. Generate one with:

Terminal
bash
openssl rand -base64 32

Then configure your server SDK with it:

server.js
javascript
1const Pusher = require("pusher");
2
3const pusher = new Pusher({
4  appId: process.env.SOKETIFY_APP_ID,
5  key: process.env.SOKETIFY_APP_KEY,
6  secret: process.env.SOKETIFY_APP_SECRET,
7  host: "api.soketify.com",
8  port: "443",
9  useTLS: true,
10  cluster: "default",
11  // 32-byte base64 master key — never expose this
12  encryptionMasterKeyBase64: process.env.SOKETIFY_ENCRYPTION_KEY,
13});

Client Setup

On the client side, you subscribe exactly like a private channel. The Pusher SDK handles decryption transparently once the auth endpoint distributes the channel key.

client.js
javascript
1// Requires pusher-js v6+ built with encryption support
2// npm install pusher-js
3
4const channel = pusher.subscribe("private-encrypted-medical-records");
5
6channel.bind("pusher:subscription_succeeded", () => {
7  console.log("Subscribed and decryption key received");
8});
9
10channel.bind("patient-update", (data) => {
11  // data is already decrypted — Soketify never saw the plaintext
12  console.log("Patient update:", data);
13});

Triggering Events

server.js
javascript
1// Server: trigger — encryption happens automatically
2await pusher.trigger(
3  "private-encrypted-medical-records",
4  "patient-update",
5  {
6    patientId: "P-9876",
7    status: "discharged",
8    notes: "Sensitive clinical data here",
9  }
10);

Encrypted Channel Limitations

Encrypted channels have a few constraints you should know upfront:

  • Only private channels can be encrypted: private-encrypted- is the only valid prefix
  • You cannot trigger to multiple channels in a single API call
  • Clients cannot send client events on encrypted channels
  • Channel and event names are not encrypted, only the payload
  • Requires pusher-js v4.3.0+ (v6+ needs the pusher-with-encryption build)

Key Rotation

To rotate the encryption key, update encryptionMasterKeyBase64 on your server and redeploy. Clients that fail decryption automatically request a fresh channel key from your auth endpoint. Note that messages sent during the rotation window may be lost, so plan for this if continuity is critical.

Presence Channels

Presence channels use the presence- prefix. They extend private channels by tracking exactly who is subscribed at any moment. Every subscriber must provide user identification during authentication, and all members can see who else is in the channel.

Use Cases

  • Chat rooms with live online user lists
  • Collaborative documents with active viewer awareness
  • Multiplayer game lobbies
  • Support queues showing available agents

Limits

  • Maximum 100 members per presence channel
  • user_info object max 1 KB
  • User ID max 128 characters

Example

presence-channel.js
javascript
1const channel = pusher.subscribe("presence-chat-room-1");
2
3// Initial member list on subscription success
4channel.bind("pusher:subscription_succeeded", (members) => {
5  console.log("Online:", members.count);
6
7  // Your own info
8  console.log("I am:", members.me.info.name);
9
10  // Iterate all members
11  members.each((member) => {
12    addUserToList(member.id, member.info);
13  });
14});
15
16// Someone joined
17channel.bind("pusher:member_added", (member) => {
18  console.log(`${member.info.name} came online`);
19  addUserToList(member.id, member.info);
20});
21
22// Someone left
23channel.bind("pusher:member_removed", (member) => {
24  console.log(`${member.info.name} went offline`);
25  removeUserFromList(member.id);
26});
27
28// Chat messages
29channel.bind("message", (data) => {
30  console.log(`${data.sender}: ${data.text}`);
31});

What Goes in user_info

The user_info object you provide in your auth endpoint is visible to every member of the channel. Keep it to what makes sense publicly within the channel: display name, avatar URL, role. Do not include emails, internal IDs, or anything sensitive unless the channel is scoped tightly enough for that to be safe.

Cache Channels

Cache channels use the cache- prefix. They automatically store the last triggered event and replay it to new subscribers the moment they subscribe. This eliminates the classic "stale initial state" problem: a client that connects at any point gets the current state immediately without needing a separate API call.

Cache channels can be combined with other types:

  • cache-: public cache channel
  • private-cache-: authenticated cache channel
  • presence-cache-: presence + cache

Use Cases

  • Device location tracking: new viewers get the last known position immediately
  • IoT sensor dashboards: show current readings without a polling request
  • Match scores, stock prices, or any "current value" that updates periodically

Example

cache-channel.js
javascript
1// Client: subscribing gives you the last event immediately (if cached)
2const channel = pusher.subscribe("cache-vehicle-location");
3
4channel.bind("location-update", (data) => {
5  // This fires immediately on subscribe if there's a cached value,
6  // then again every time the server triggers the event
7  updateMapMarker(data.lat, data.lng);
8});
9
10// Handle cache miss: no event has been triggered yet
11channel.bind("pusher:cache_miss", () => {
12  // The cache is empty — fetch current state yourself or wait
13  fetchInitialLocation().then((loc) => updateMapMarker(loc.lat, loc.lng));
14});
15
16// Server: trigger events as normal — the last one is automatically cached
17await pusher.trigger("cache-vehicle-location", "location-update", {
18  vehicleId: "V-001",
19  lat: 40.7128,
20  lng: -74.0060,
21  timestamp: Date.now(),
22});

Handling Cache Misses with Webhooks (Recommended)

When multiple clients subscribe to an empty cache channel simultaneously, you do not want each one triggering a refill. The smarter approach is to configure a webhook for the cache_miss event. for the cache_miss event. Soketify deduplicates these notifications and sends one webhook to your server, which then triggers a single event to repopulate the cache.

webhook-handler.js
javascript
1// Your webhook handler receives a cache_miss event:
2// { name: "cache_miss", channel: "cache-vehicle-location" }
3
4// Repopulate the cache by triggering the event once:
5app.post("/webhooks/pusher", async (req, res) => {
6  const { events } = req.body;
7  for (const event of events) {
8    if (event.name === "cache_miss") {
9      const location = await fetchCurrentLocation(event.channel);
10      await pusher.trigger(event.channel, "location-update", location);
11    }
12  }
13  res.sendStatus(200);
14});

Cache TTL

Cached messages expire after up to 30 minutes, though they may expire sooner. Cache channels are best for values that update frequently enough that a 30-minute-old cache would be refreshed naturally. If you need guaranteed persistence, store data in your own database and serve it via your API.

Cache Channel Limitations

Only server-triggered events are cached. Client events (client-prefix) are not cached. Cached data does not include a built-in timestamp; add one to your event payload if freshness matters.

Channel Naming Conventions

Channel names must follow these rules:

  • Maximum 200 characters
  • Allowed characters: a-zA-Z0-9_\-=@.,;
  • No spaces or special characters outside the allowed set
  • Reserved prefixes: private-, private-encrypted-, presence-, cache-, private-cache-, presence-cache-

Recommended Patterns

Naming examples
// Public channels
"live-scores"
"news-feed"
"stock-prices"

// Private channels — scope to a resource + ID
"private-user-42"
"private-order-ORD-1234"
"private-team-engineering"

// Encrypted channels — same scoping, private data
"private-encrypted-patient-P-9876"
"private-encrypted-account-ACC-001"

// Presence channels — scope to a shared space
"presence-chat-room-general"
"presence-document-doc-456"
"presence-game-lobby-abc"

// Cache channels — current-value streams
"cache-vehicle-V-001"
"private-cache-sensor-readings-room-7"
"presence-cache-live-auction-XYZ"

Subscribing and Unsubscribing

subscribe-unsubscribe.js
javascript
1// Subscribe to a channel
2const channel = pusher.subscribe("my-channel");
3
4// Bind to events
5channel.bind("my-event", callback);
6
7// Unbind a specific callback
8channel.unbind("my-event", callback);
9
10// Unbind all callbacks for an event
11channel.unbind("my-event");
12
13// Unbind everything on this channel
14channel.unbind_all();
15
16// Unsubscribe from the channel entirely
17pusher.unsubscribe("my-channel");

Tip

Unsubscribe from channels when they are no longer needed, for instance when a user navigates away, closes a modal, or leaves a room. In React, return a cleanup function from useEffect:

return () => pusher.unsubscribe(channelName);

Client Events

Client events let one connected client send events directly to other subscribers on the same private or presence channel, without going through your server. Event names must be prefixed with client-.

Requirements

  • Channel must be private- or presence-
  • Event name must start with client-
  • Client events must be enabled for your app in the Dashboard
  • Wait for pusher:subscription_succeeded before triggering
  • The sender does not receive its own client events
  • Rate limited to 10 client events per second per connection

Example: Typing Indicators

typing-indicator.js
javascript
1const channel = pusher.subscribe("private-chat-room-1");
2
3// Wait for subscription before triggering
4channel.bind("pusher:subscription_succeeded", () => {
5  // Now safe to trigger client events
6
7  inputEl.addEventListener("input", () => {
8    channel.trigger("client-typing", { user: "Alice", isTyping: true });
9  });
10
11  inputEl.addEventListener("blur", () => {
12    channel.trigger("client-typing", { user: "Alice", isTyping: false });
13  });
14});
15
16// Receive client events from other subscribers
17channel.bind("client-typing", (data) => {
18  if (data.isTyping) {
19    showTypingIndicator(data.user);
20  } else {
21    hideTypingIndicator(data.user);
22  }
23});

Rate Limiting Tip

The 10/sec rate limit is per connection. For high-frequency events like mouse position or cursor tracking, debounce or throttle your triggers. Sending on every UI event will hit the limit and start dropping messages.

Trust the metadata, not the data

On presence channels, client event callbacks receive a second metadata argument with the sender's user_id. Use that instead of embedding a user ID in the event data, which any client could spoof:

channel.bind("client-msg", (data, metadata) => { console.log("From:", metadata.user_id); // trusted });

Subscription Count

When enabled in your app settings, Soketify fires a pusher:subscription_count event whenever the subscriber count on a channel changes. This lets you display live viewer counts without using a full presence channel.

subscription-count.js
javascript
1const channel = pusher.subscribe("live-match-final");
2
3channel.bind("pusher:subscription_count", (data) => {
4  viewerCountEl.textContent = `${data.subscription_count} watching`;
5});

Info

On channels with more than 100 subscribers, this event fires at most every 5 seconds rather than on every change, to avoid flooding all subscribers with count updates.

Next Steps