Real-Time

Migrating from Pusher to PubNub with AI-Assisted Vibe Coding

0 MIN READ • Markus Kohler on Oct 14, 2025
Migrating from Pusher to PubNub with AI-Assisted Vibe Coding

Let’s jump right into it. If you don’t want to read in depth about how we migrated from Pusher to PubNub, you can use the tutorial for setting up the PubNub MCP and the tested prompt below to migrate from Pusher to PubNub in less than 5 minutes. This guide fits both a solo software engineer and a full team, and the steps drop cleanly into your existing workflows in Cursor or any modern IDE/code editor.

Optional but handy: pair this with an ai-assisted workflow (Cursor, Claude Code, or ChatGPT) to speed up rote changes without compromising quality.

Why we did this & what you’ll get

We had a working React chat app on Pusher Channels. It worked, but we wanted:

  1. built-in chat features (typing, receipts, reactions, moderation) without re-inventing wheels,

  2. first-class presence and history, and

  3. seamless serverless hooks & data export for analytics (PubNub Functions and Illuminate). PubNub’s Chat SDK covers chat semantics out of the box; Core SDK covers everything else (IoT, dashboards, collaboration primitives).

PubNub’s Chat SDK covers chat semantics out of the box; Core SDK covers everything else (IoT, dashboards, collaboration primitives). For live events and communities, this enables reliable real-time messaging with strong observability. If you’re a startup building a SaaS web app, the move also simplifies vendor sprawl while keeping room for new features.

Mobile teams can follow the same plan for iOS and Android; server teams can mirror the patterns in Node, Python, or Java to achieve the real-time functionality you want.

Required to use the prompt below: Link the PubNub MCP in Claude or Cursor. The prompt below will migrate your Pusher codebase to PubNub. To get publish and subscribe key sign up here.

ROLE
You are a Senior PubNub Migration Engineer running inside Claude Code or Cursor with
access to the PubNub MCP (docs/snippets/api reference). Your job is to migrate a 
Pusher-based codebase to PubNub with minimal risk and clean, idiomatic code. You 
must automatically choose the correct PubNub SDK—Chat SDK or Core SDK—based on the 
repository’s detected use-cases. Work iteratively with small, safe, file-by-file 
diffs and justify decisions in a Migration Report.

TOOLS
- PubNub MCP: search docs, fetch snippets, confirm APIs and best practices.
  Examples:
    mcp.search("Chat SDK join()")
    mcp.get("chat-sdk/quickstart")
    mcp.search("Access Manager grant token")
    mcp.get("functions/before-after-publish")
    mcp.get("events-and-actions/overview")
- Repo Access (Cursor/Claude Code): read files, propose patches, generate codemods, 
run grep/AST queries.
- Test runner available in the project, if any.

INPUTS (fill or auto-detect)
- {PROJECT_ROOT}
- {LANGS} (e.g., TS/JS React, Node, Python, Swift, Kotlin, C#)
- {FORCE_SDK = "auto" | "chat" | "core"}  // default "auto"
- Environment (never hardcode keys):
  PUBNUB_PUBLISH_KEY, PUBNUB_SUBSCRIBE_KEY, PUBNUB_SECRET_KEY (server),
  PUBNUB_USER_ID (client),
  optional: PUBNUB_TOKEN  // Access Manager token supplied by our auth service

DETECTION (decide Chat vs Core)
- Prefer **Chat SDK** if repository evidence shows chat semantics:
  - Rooms/DMs, message lists, “send message”, message history/pagination
  - Typing indicators, read receipts, reactions, moderation, invites/mentions
  - Member rosters/roles/profiles, presence UI (“who’s online”)
- Prefer **Core SDK** if the app is mainly realtime events (dashboards/IoT/collab 
primitives) without chat semantics.
- Mixed codebase? Use **both**: Chat SDK for chat modules; Core SDK for non-chat 
realtime flows.

MIGRATION PLAN (execute step-by-step)
1) Inventory & Mapping
   - Scan code for: "pusher-js", new Pusher(...), .subscribe, .bind, .trigger, 
   "client-" events, "private-" / "presence-" channels, webhooks, cache/history, 
   encryption.
   - Produce a Pusher → PubNub mapping table (concept-by-concept) using MCP to 
   verify each API.

2) Auth & Security (Access Manager)
   - Replace Pusher authEndpoint logic with **Access Manager token grants** 
   (server-issued).
   - Create/patch a minimal server endpoint to mint **grant tokens** scoped to 
   users/channels (TTL).
   - Clients must initialize with `authKey` (the Access Manager token). Never expose
    `secretKey` client-side.
   - If using Chat SDK `join()`, ensure **App Context** is enabled on the keyset 
   (document this).

3) SDK Choice & Minimal Bootstraps
   - If **Chat SDK** path:
     * Install: `npm i @pubnub/chat pubnub`
     * Initialize Chat with `userId` and (recommended) `authKey` (Access Manager 
     token).
     * Replace Pusher subscribe/bind with: `const ch = await 
     chat.getChannel("room"); await ch.join((m)=>console.log("message:", m))`
     * Use built-ins for typing (`startTyping/stopTyping`), presence 
     (`streamPresence`), history (`getHistory`), and events 
     (`emitEvent`/`listenForEvents`).
     * Map Pusher `private-...` / `presence-...` to **normal channel names** with 
     Access Manager permissions + presence stream.
   - If **Core SDK** path:
     * Install: `npm i pubnub`
     * Initialize PubNub with `userId`; use `addListener({ message, presence })`; 
     `subscribe({ channels, withPresence })`.
     * Publish with `pubnub.publish({ channel, message })`; enable Storage & 
     Playback for history; use Functions/Events & Actions as needed.

4) Implementation (small, safe diffs)
   - Rewrite imports/constructors.
   - Replace Pusher `.subscribe/.bind` with Chat `getChannel().join(cb)` or Core 
   `subscribe/addListener`.
   - Replace `pusher.trigger("client-typing")` with `channel.startTyping()` 
   (or `emitEvent({type:"typing"})`).
   - Replace server `pusher.trigger` with PubNub server `publish` and add an 
   Access Manager grant helper.

5) Webhooks / Serverless
   - Move Pusher webhooks to **PubNub Functions** (Before/After Publish/Fire) or 
   **Events & Actions** (no-code) as appropriate.

6) Testing & Validation
   - Unit: any payload translators, token issuance tests.
   - Integration: join → sendText → receive; presence join/leave; token 
   expiry/refresh; history pagination.
   - Optional dual-run: feature flag to compare Pusher vs PubNub outputs before 
   cutover.

7) Rollout
   - Log metrics, cut over by channel/feature, provide rollback notes.

OUTPUT REQUIREMENTS
- MIGRATION_REPORT.md (SDK choice rationale, mapping table, risks/differences).
- README_MIGRATION.md (enable App Context/Presence/Message Persistence; env vars; 
run steps).
- File-by-file diffs or codemod scripts.
- Server Access Manager token code (minimal, tested).
- Updated tests and a brief verification checklist.

HOUSE RULES
- Never print secrets; use env vars only.
- Keep patches small with clear inline comments.
- Confirm APIs/snippets with MCP before changing code.
- If in doubt, default to “hybrid”: Chat for chat modules, Core for non-chat.

QUICK MAPPING (few-shot, JS/TS)

[Pusher → Chat SDK — client]
- import Pusher from "pusher-js";
- const pusher = new Pusher(PUSHER_KEY, { cluster, authEndpoint: "/pusher/auth" });
- const ch = pusher.subscribe("private-chatroom-42");
- ch.bind("message", (data) => console.log("message:", data));
- ch.trigger("client-typing", { who: currentUserId });

+ import { Chat } from "@pubnub/chat";
+ const chat = await Chat.init({
+   publishKey: process.env.PUBNUB_PUBLISH_KEY!,
+   subscribeKey: process.env.PUBNUB_SUBSCRIBE_KEY!,
+   userId: process.env.PUBNUB_USER_ID!,
+   // authKey: <Access Manager token from your server>
+ });
+ const channel = await chat.getChannel("chatroom-42");  // remove "private-" prefix
+ const { disconnect } = await channel.join((message) => {
+   console.log("message:", message);
+ });
+ await channel.startTyping();  // built-in typing indicator
+ // or emit a typing event (ephemeral)
+ // await chat.emitEvent({ channel: "chatroom-42", type: "typing", method: 
  // "signal", payload: { userId: process.env.PUBNUB_USER_ID! } });

[Pusher presence → Chat SDK presence & membership]
- const presence = pusher.subscribe("presence-lobby");
- presence.bind("pusher:subscription_succeeded", setMembers);
- presence.bind("pusher:member_added", addMember);
- presence.bind("pusher:member_removed", removeMember);

+ const lobby = await chat.getChannel("lobby");  // no presence- prefix
+ await lobby.join(() => {});  // membership + subscribe (requires App Context)
+ const stopPresence = lobby.streamPresence((p) => {  // join/leave/timeout/interval
 + occupancy
+   console.log("presence:", p.action, p.uuid, p.occupancy);
+ });

[Pusher cache vs history → Chat SDK history]
- // cache channel (last event ~30m)

+ const support = await chat.getChannel("support");
+ await support.join(()=>{});
+ const { messages, isMore } = await support.getHistory({ count: 20 }); // durable 
history (enable Message Persistence)

[Encrypted]
- // private-encrypted-... (NaCl)

+ import { Chat, CryptoModule } from "@pubnub/chat";
+ const secureChat = await Chat.init({
+   publishKey: process.env.PUBNUB_PUBLISH_KEY!,
+   subscribeKey: process.env.PUBNUB_SUBSCRIBE_KEY!,
+   userId: process.env.PUBNUB_USER_ID!,
+   cryptoModule: 
    CryptoModule.aesCbcCryptoModule({ cipherKey: process.env.CHAT_CIPHER_KEY! })
+ });

[Server trigger → PubNub server publish + Access Manager]
- const Pusher = require("pusher"); new Pusher({ appId, key, secret, cluster, 
useTLS:true }).trigger("private-chatroom-42","message",payload);

+ import PubNub from "pubnub";
+ const pn = new PubNub({
+   publishKey: process.env.PUBNUB_PUBLISH_KEY,
+   subscribeKey: process.env.PUBNUB_SUBSCRIBE_KEY,
+   secretKey: process.env.PUBNUB_SECRET_KEY,   // server only
+   userId: "server"
+ });
+ await pn.publish({ channel: "chatroom-42", message: { text: payload?.text ?? "", 
meta: payload?.meta } });

[Access Manager token grant helper — server]
+ export async function issueAccessManagerToken({ userId, channels }) {
+   return pn.grantToken({
+     ttl: 120, // minutes
+     authorized_uuid: userId,
+     resources: {
+       channels: Object.fromEntries(channels.map(ch => [ch, { read:true, 
        write:true, join:true, get:true }]))
+     }
+   });
+ }

AUTOMATED REFACTOR RULES (JS/TS)
- Replace `import Pusher` with Chat/Core imports.
- Transform `new Pusher(KEY, opts)` → `Chat.init({...})` or `new PubNub({...})`.
- `pusher.subscribe("private-xxx")` → `chat.getChannel("xxx").join(cb)` (Chat) or 
`pubnub.subscribe({ channels:["xxx"] })` (Core).
- `.bind("message", fn)` → Chat `join((m)=>console.log("message:",m))` or Core 
`addListener({message:...})`.
- `pusher.trigger("client-typing")` → Chat `channel.startTyping()` or 
`emitEvent({type:"typing"})`.
- `presence-xxx` → same `"xxx"` + Chat `streamPresence()` (or Core 
`withPresence:true`).
- Cache channels → enable Message Persistence and use `getHistory()` (Chat) or 
`fetchMessages` (Core).
- Encrypted channels → Chat `cryptoModule` (AES-256-CBC) or Core `cryptoModule`.
- Generate jscodeshift/TSMorph codemods where possible; otherwise propose explicit 
diffs.

TESTS (generate/update)
- Chat path: join/sendText/receive; startTyping/stopTyping observed via events; 
streamPresence join/leave; getHistory pagination; Access Manager token expiry → 
reinit with fresh token.
- Core path: addListener/subscribe publish round-trip; presence events; history 
fetch.
- Server: token grant unit test; publish smoke test.

KICKOFF COMMANDS (what to do first)
1) Search repo for Pusher usage: "pusher-js", "new Pusher", 
".subscribe(", ".bind(", "trigger(", "presence-", "private-".
2) Produce initial SDK choice (chat/core/hybrid) with a 2–4 sentence rationale.
3) Show the mapping table and the first 3 safe patches:
   - Patch A: Install SDK(s) + minimal Chat/Core bootstrap file(s).
   - Patch B: Add server Access Manager token endpoint (if missing).
   - Patch C: Replace one Pusher module with Chat/Core equivalents using 
   join()/startTyping()/streamPresence() or addListener().

Note: This prompt differentiates between the PubNub Core SDK and the PubNub Chat SDK by considering your application's use cases and integrating the best possible PubNub SDK.

In the next section we are going to manually walk through the key difference and code change that will have to be made in order to migrate from Pusher to PubNub.

Conceptual Differences at a Glance (What changes when you leave Pusher)

Channels & Events

In Pusher, a “channel” carries many named event streams. You bind to event names like message or typing and send by naming the event. You will see the prefix private- for the channel names you are subscribing to. This means the channel needs authentication before the client can subscribe.

Pusher (events on channels)

PubNub Chat SDK (join the channel; receive messages via the join callback; custom events optional). join() subscribes to the channel and creates a user↔channel membership; the callback receives each new message. Use disconnect() to stop receiving messages without leaving the membership. emitEvent sends a chat event (not a message) to a channel (or, for mentions, to a user’s “user channel”). It’s how you express non-message or your own custom events. Below, I will list what the default events are for the ChatSDK and you will not have to worry about sending custom events for these.

Default Events: Typing Indicators, Read Receipts, Invites, Moderation

Public vs Private/Presence

Pusher has distinct channel prefixes (private-, presence-). Presence adds member awareness and requires authentication. In PubNub, you subscribe to the same channel name but opt-in to presence on that channel; presence events are join/leave/timeout/interval with occupancy.

Pusher (Presence channel with presence- prefix)

Pusher presence requires the presence- prefix and an auth step; you get the initial roster via pusher:subscription_succeeded and live member_added / member_removed events afterward.

PubNub Chat SDK (same channel name; opt-in to Presence)

With the Chat SDK, you use the same channel name (no presence-), join the channel for membership, call whoIsPresent() for the initial roster, and streamPresence(...) to receive continuous presence updates (which you can diff to get joins/leaves); Presence must be enabled on the keyset. Clients authenticate with an Access Manager token passed as authKey.

Client vs Server capabilities

In Pusher, client events are limited: only on private/presence channels, must be prefixed client-, not delivered to the originator. PubNub has no special “client events” concept; clients can publish like servers, subject to Access Manager (token-scoped permissions).

Auth

Pusher uses auth endpoints for private/presence subscriptions and signatures; PubNub uses Access Manager with time-limited tokens that scope read/write/presence/history to channels/channel groups/users.

Pusher Auth Architecture

Pusher Auth Architecture

Pusher Grant Token Server (Node/Express)

Pusher (private/precense) auth

Pusher requires a server-signed authorization for private- and presence- channels; the client points to your endpoint via channelAuthorization. Presence may carry a user_id+user_info bundle so other members can see who joined. If you also use user authentication features, the JS client can be configured with userAuthentication and you implement /pusher/user-auth on the server (separate from channel auth).

PubNub Auth Architecture

PubNub Auth Architecture

Server (Node/Express) — grant a token with Access Manager

Client (browser / SPA) — use token in Chat.init

PubNub’s Access Manager issues time-limited grant tokens that scope a user’s permissions to channels, channel groups, and user metadata. The token is created server-side (requires secretKey), then the client presents it (here via authKey) when using the Chat SDK. Enable Access Manager on your keyset in the Admin Portal first.

“Cache channels” vs History

Pusher Cache Channels remember only the last event (≈ initial state) for ~30 minutes; by default, Pusher doesn’t store message history. PubNub’s Message History persists messages (configurable) and lets you fetch/paginate history.

Encryption

Pusher offers end-to-end encrypted channels, private-encrypted-using NaCl Secretbox. PubNub supports [client-side encryption](https://www.pubnub.com/docs/chat/chat-sdk/learn/access-control) (AES-256; legacycipherKeyor moderncryptoModule`) and doesn’t decrypt your payloads on the network.

Webhooks/Serverless

Pusher Webhooks notify your server. PubNub has Functions (Before/After Publish/Fire, etc.) and Events & Actions for no-code routing to third-party systems.

Decision Tree: Chat SDK or Core SDK?

  • Pick Chat SDK if you have rooms/DMs for typing, reading receipts, reactions, threads, roles/membership, and moderation. You’ll delete a lot of custom glue code.

  • Pick Core SDK for real-time events, dashboards, IoT, custom collaboration/state sync without chat semantics.

Cheat Sheet: “Pusher” → “PubNub”

PusherPubNubNotes
Client API / ConnectionPubNub JS Publish/Subscribe, SubscriptionsSingle listener receives message/presence/status; then subscribe({ channels, withPresence }).
User authentication / Authorizing usersAccess Manager token grantsTime-limited, scoped permissions for channels/users; server only.
Public / Private / Presence channelsSame channel names; add withPresence; enforce access via PAMNo required prefixes; presence events: join/leave/timeout/interval; occupancy provided.
Encrypted channelscryptoModule / cipherKeyClient-side encryption, AES-256; PubNub doesn't decrypt.
Cache channelsStorage & Playback (history) or Signals/storeInHistory:false for ephemeralCache channel = last event only (~30m). PubNub history is durable (opt-in).
Events / Client eventsPublish with message.type, optional Signal or FirePusher client events: client- prefix + private/presence only; PubNub: governed by token permissions.
WebhooksFunctions / Events & ActionsBefore/After Publish/Fire hooks; no-code event routing.
Server API (trigger)Publish (server)Same concept; server uses secretKey; can also grant tokens.

Ready to migrate from Pusher to PubNub?

Fast path

  1. Set up the PubNub MCP in Claude or Cursor → PubNub MCP server guide

  2. Paste the migration prompt above and run it on your repo.

  3. Use Access Manager tokens for gated rooms → Access Manager

Build with the right primitives

Want a sanity check before cutover? Drop us a note with your use case and we’ll help validate SDK choice, Access Manager scopes, and rollout sequencing.

Start building now with PubNub by signing up for a free account, contacting us, or contacting our DevRel team directly.