Quickstart
Portal gives your app realtime channels and a per-user inbox over a single WebSocket, with reactive bindings for React. This page gets a chat-shaped channel on screen with zero backend, then shows what changes once you have real, identified users.
Install
npm install @portalsdk/core @portalsdk/react
@portalsdk/react has a peer dependency on react (>=18 <20).
Zero-backend: anonymous mode
You don't need a backend, a token endpoint, or even a signed-up user to try Portal. Construct a client with just your publishable API key, and every user who loads the page gets a stable anonymous identity for free:
import { Portal } from "@portalsdk/core";
import { PortalProvider, useChannel } from "@portalsdk/react";
// Construct once, at module scope. Construction is synchronous and passive —
// nothing happens on the wire until a component actually mounts a channel.
const portal = new Portal({ apiKey: "pk_your_publishable_key" });
interface ChatMessage {
text: string;
}
function ChatRoom() {
const { messages, send } = useChannel<ChatMessage>({ channelId: "lobby" });
return (
<div>
<ul>
{messages.map((m) => (
<li key={m.id}>{m.content.text}</li>
))}
</ul>
<button onClick={() => send({ content: { text: "hello" } })}>Send</button>
</div>
);
}
export function App() {
return (
<PortalProvider client={portal}>
<ChatRoom />
</PortalProvider>
);
}
A few things worth noticing:
-
portal.channel(id)(whatuseChannelcalls under the hood) returns the same handle for the same id, so multiple components watching"lobby"share one socket. -
Anonymous users get a stable identity across page refreshes, an empty inbox, and are
refused from any channel whose config sets
anonymous: false(see Tokens & auth). apiKeyis a publishable key — safe to ship in a browser bundle.
Add real users
When you're ready to attach Portal to your own auth, pass a token — either directly
to the Portal constructor, or later via PortalProvider's token prop (which forwards
to client.setToken). A token is a string or an async callback that resolves to one;
the callback form is recommended, since it's re-invoked on connect, reconnect, and
expiry.
The token itself is a signed JWT identifying the user, issued by your backend. Stand up a small endpoint that authenticates the request however your app already does, then returns that token:
import { Portal } from "@portalsdk/core";
import { PortalProvider, useChannel } from "@portalsdk/react";
const portal = new Portal({ apiKey: "pk_your_publishable_key" });
async function fetchPortalToken(): Promise<string> {
const res = await fetch("/api/portal-token", { credentials: "include" });
const { token } = (await res.json()) as { token: string };
return token;
}
interface ChatMessage {
text: string;
}
function ChatRoom() {
const { messages, send } = useChannel<ChatMessage>({ channelId: "lobby" });
return (
<ul>
{messages.map((m) => (
<li key={m.id}>{m.content.text}</li>
))}
</ul>
);
}
export function App() {
return (
<PortalProvider client={portal} token={fetchPortalToken}>
<ChatRoom />
</PortalProvider>
);
}
On login, call portal.setToken(fetchPortalToken) directly (or update the token prop)
to move the client from anonymous to identified without remounting. On logout,
portal.setToken(undefined) returns the client to anonymous mode. Live channels and the
inbox re-authenticate in place — see Tokens & auth for the full
picture, and Authoring portal.config.ts for verifying your
own JWTs.
Doc gap: the exact contract for
/api/portal-tokenabove is intentionally left generic. The material available to write these docs covers verifying your own JWTs (authinportal.config.ts) in detail, but doesn't document a public endpoint or process for the alternative path — tokens "minted by Portal" — that@portalsdk/configreferences as the default. If your project relies on Portal-minted tokens rather than your own JWTs, that flow isn't covered here yet.
Next steps
- Core SDK → Client setup for the full client surface.
- React → PortalProvider and useChannel for the hook API.
- Guides → Realtime chat for a complete, copy-pasteable room.