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.
Next.js App Router
Client components
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.
When dynamic() is needed
next/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":
Token route handler
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.
Pages Router
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.
Hydration caveats
- Don’t render call state from the server. Anything driven by an
observable belongs in a
useEffect/onMount/<ClientOnly>— not in the initial server render. Otherwise hydration will mismatch (the server rendered"idle", the client mounts with"connected"). - Don’t read
windowin module scope. Even inside a"use client"file, the module body runs once during client hydration. Wrap anywindow./document.access in an effect. <video>srcObjectwon’t survive serialization. Bind it from an effect after the stream observable emits — never from a prop on the first render.
Environment variables
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.