SSR & Next.js
SSR & Next.js
SSR & Next.js
@signalwire/js and @signalwire/web-components are browser-only.
They depend on WebSocket, RTCPeerConnection, navigator.mediaDevices,
and customElements — APIs that don’t exist in Node.js. Importing
either package at the top of a server-rendered module will crash the
server during build or during SSR.
Two boundaries to hold across any server-rendered framework: load the SDK only on the client, and mint tokens only on the server. The patterns below are Next.js–specific, but the same shape applies in Nuxt, SvelteKit, Remix, Astro, or Gatsby — wrap the SDK in that framework’s client-only escape hatch, and put token minting behind a server route.
Wrap any code that touches the SDK in a "use client" component. The
import won’t execute on the server.
A "use client" file’s transitive imports are fine — they’re bundled
for the browser, not Node. You don’t need dynamic() for ordinary
SDK use.
dynamic() is needednext/dynamic with ssr: false is only required when an import has
side effects that run at module evaluation time (custom element
registration, top-level new SignalWire(...), etc.). The web
components package registers customElements.define on import — so in
the App Router, import the components from inside an effect or use
next/dynamic:
For the embed bundle (signalwire-web-components-embed.iife.js)
loaded via <script>, use next/script with strategy="lazyOnload"
or "afterInteractive":
Mint tokens in a route handler. This is the only place your SignalWire project credentials are allowed to live.
On the client, wrap the route in a CredentialProvider
so the SDK can re-fetch a fresh SAT when the current one nears
expiry. expiry_at is a Date.now()-style millisecond timestamp.
See Authentication › Refresh strategies for refresh-token-based flows that avoid re-authenticating the user on every rollover.
For a pages/-based Next.js app, the only difference is that there’s
no "use client" directive. Use next/dynamic({ ssr: false }) for any
component that imports the SDK:
Token endpoints live under pages/api/sw-token.ts and use the same
REST flow as the App Router example.
useEffect / onMount / <ClientOnly> —
not in the initial server render. Otherwise hydration will mismatch
(the server rendered "idle", the client mounts with "connected").window in module scope. Even inside a "use client"
file, the module body runs once during client hydration. Wrap any
window. / document. access in an effect.<video> srcObject won’t survive serialization. Bind it from
an effect after the stream observable emits — never from a prop on
the first render.In Next.js, NEXT_PUBLIC_* is the only prefix that gets inlined into
the client bundle. Project ID and auth token must stay on the server
side of that boundary, always.
See Authentication for the full token flow.