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:
built-in chat features (typing, receipts, reactions, moderation) without re-inventing wheels,
first-class presence and history, and
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 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
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; legacy
cipherKeyor modern
cryptoModule`) 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”
Pusher | PubNub | Notes |
---|---|---|
Client API / Connection | PubNub JS Publish/Subscribe, Subscriptions | Single listener receives message/presence/status; then subscribe({ channels, withPresence }). |
User authentication / Authorizing users | Access Manager token grants | Time-limited, scoped permissions for channels/users; server only. |
Public / Private / Presence channels | Same channel names; add withPresence; enforce access via PAM | No required prefixes; presence events: join/leave/timeout/interval; occupancy provided. |
Encrypted channels | cryptoModule / cipherKey | Client-side encryption, AES-256; PubNub doesn't decrypt. |
Cache channels | Storage & Playback (history) or Signals/storeInHistory:false for ephemeral | Cache channel = last event only (~30m). PubNub history is durable (opt-in). |
Events / Client events | Publish with message.type, optional Signal or Fire | Pusher client events: client- prefix + private/presence only; PubNub: governed by token permissions. |
Webhooks | Functions / Events & Actions | Before/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
Set up the PubNub MCP in Claude or Cursor → PubNub MCP server guide
Paste the migration prompt above and run it on your repo.
Use Access Manager tokens for gated rooms → Access Manager
Build with the right primitives
Chat SDK (rooms/DMs, typing, receipts, moderation) → Overview
Presence & membership (who’s online) → Presence
History & pagination → Message History
Edge logic & integrations → Functions & Events & Actions
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.