Expose a Local Next.js App with rstream

Serve a local or self-hosted Next.js app through a public rstream URL for webhooks, OAuth callbacks, browser previews, and mobile testing.


A Next.js route can be ready before an external service can reach it. GitHub webhooks, OAuth callbacks, mobile previews, and teammate reviews all need a real HTTPS origin with provider headers, redirects, cookies, and request logs on the path the app will actually serve.

rstream lets that existing Next.js server keep its normal shape while giving it a public entrypoint. The app still runs through next dev, next start, or a self-hosted Node process, while the tunnel is created outbound from the machine that owns the app. External requests arrive at the same App Router pages, route handlers, middleware, cookies, redirects, and logs you already use.

This guide focuses on serving the Next.js application itself through rstream. A separate pattern is to use a deployed Next.js product as a control plane that lists tunnels, issues short-lived access, or provisions resources for users and devices; that architecture is covered in Build a Next.js WebRTC Video Platform with rstream.

This guide presents two paths. The CLI sidecar adds rstream to an existing app through npm scripts. The SDK-managed server creates the tunnel from the self-hosted Node process when labels, auth, stable domain selection, reconnect behavior, and shutdown belong in application code.

PathUse it when
CLI tunnel sidecarYou already have a Next.js app and want the fastest additive setup. Next.js keeps running with next dev, next start, or your existing scripts.
SDK managed serverYou self-host the Node process and want tunnel creation, labels, auth, stable domain selection, reconnect behavior, and shutdown inside application code.

Both paths keep the Next.js application itself as the HTTP server. rstream provides the public entrypoint and the outbound connection to the engine.

Add a CLI tunnel sidecar to an existing app

The CLI setup keeps the Next.js server on its normal port and runs a rstream tunnel process next to it.

GitHub, Stripe, OAuth provider, phone, teammate
        |
        | HTTPS request
        v
rstream public URL
        |
        | outbound tunnel session
        v
localhost:3000
        |
        | Next.js App Router pages and route handlers
        v
your application

For an existing repository, this is the fastest path because it exposes the app while preserving Next.js startup, request logs, code reloads, and development assets.

Install concurrently, then add a dedicated tunnel script and a combined development script.

npm install --save-dev concurrently
{
  "scripts": {
    "dev": "next dev",
    "tunnel": "rstream forward 3000 --name nextjs-preview --label service=nextjs --label env=dev",
    "dev:tunnel": "concurrently -k --raw \"npm run dev\" \"npm run tunnel\""
  }
}

Run the combined script to start Next.js and the tunnel together.

npm run dev:tunnel

Next.js keeps printing its normal development output. The rstream process prints the public URL that belongs in a webhook provider, OAuth callback configuration, mobile test device, or teammate preview.

https://<published-rstream-host>/api/webhooks/github
https://<published-rstream-host>/api/auth/callback/github

The first argument to rstream forward is the local upstream port. If the app runs on another port, only that value needs to change.

{
  "scripts": {
    "dev": "next dev --port 4000",
    "tunnel": "rstream forward 4000 --name nextjs-preview --label service=nextjs --label env=dev",
    "dev:tunnel": "concurrently -k --raw \"npm run dev\" \"npm run tunnel\""
  }
}

rstream forward creates a published HTTP tunnel and reconnects automatically by default. The script keeps only the options needed for a stable tunnel name and searchable labels.

Webhook and OAuth providers are easier to configure with a durable callback URL. When your rstream project has a stable domain, attach it to the tunnel script.

{
  "scripts": {
    "tunnel": "rstream forward 3000 --name nextjs-preview --host <your-stable-domain> --label service=nextjs --label env=dev"
  }
}

The hostname must belong to the selected rstream project or to the stable-domain namespace configured on your self-hosted engine. Provider settings can then keep the same URL while the app restarts or moves to another machine.

Webhook and OAuth callbacks can call the tunnel URL while the Next.js code stays the same. Browser previews in development need one extra setting because Next.js protects development resources such as HMR from unexpected origins.

For stable-domain browser previews, add the domain through allowedDevOrigins.

const allowedDevOrigins = (process.env.NEXT_ALLOWED_DEV_ORIGINS ?? "")
  .split(",")
  .map((origin) => origin.trim())
  .filter(Boolean);
 
/** @type {import("next").NextConfig} */
const nextConfig = {
  allowedDevOrigins,
};
 
export default nextConfig;

Then run the tunnel with the same domain.

NEXT_ALLOWED_DEV_ORIGINS=<your-stable-domain> npm run dev:tunnel

This setting only affects the development browser experience. Signed webhook delivery and OAuth callback validation use normal HTTP requests.

Keep application-level validation in the app. Webhook routes should verify provider signatures, OAuth routes should validate state and session rules, and rstream policy should protect the public URL before traffic reaches Next.js.

For machine-facing endpoints, enable token authentication.

{
  "scripts": {
    "tunnel": "rstream forward 3000 --name nextjs-preview --token-auth --label service=nextjs --label env=dev"
  }
}

Requests then present a bearer token.

curl -H "Authorization: Bearer <rstream-token>" \
  https://<published-rstream-host>/api/health

For browser-facing internal previews, use rstream Auth when it is enabled for the project.

{
  "scripts": {
    "tunnel": "rstream forward 3000 --name nextjs-preview --rstream-auth --label service=nextjs --label env=dev"
  }
}

The CLI authenticates the local agent to the engine with the selected rstream context or environment variables. --token-auth and --rstream-auth protect callers reaching the public URL.

For a self-hosted Node process, keep the same split between the application command and the tunnel command.

{
  "scripts": {
    "build": "next build",
    "start": "next start",
    "tunnel": "rstream forward 3000 --name nextjs-preview --label service=nextjs --label env=preview",
    "start:tunnel": "concurrently -k --raw \"npm run start\" \"npm run tunnel\""
  }
}

Build the app, then run the combined start command.

npm run build
npm run start:tunnel

Use separate tunnel:dev, tunnel:preview, or tunnel:prod scripts when environments need different tunnel names, stable domains, labels, or access policy.

Serve Next.js directly from the SDK

In the CLI setup, Next.js listens on a local port and rstream forward publishes that port. In the SDK setup, a self-hosted Node process creates the rstream tunnel from code and gives its http.Server directly to the rstream runtime.

Use this mode when the tunnel lifecycle belongs inside the application process. Labels, auth flags, stable domain selection, reconnect behavior, and shutdown then live next to the custom server code.

External caller
        |
        | HTTPS request
        v
rstream public URL
        |
        | outbound rstream session
        v
Node process
        |
        | http.Server
        v
Next.js request handler

The reference sample implements this SDK server.

rstream examples / nextjs-rstream-previewOpen the Next.js SDK tunnel sample.
git clone https://github.com/rstreamlabs/rstream-examples.git
cd rstream-examples/nextjs-rstream-preview
npm install
npm run verify

The sample exposes a GitHub-compatible webhook route and a webhook inbox while keeping the default Next.js command available.

export GITHUB_WEBHOOK_SECRET=local-dev-secret
npm run dev

Send a signed webhook to the local app.

payload='{"action":"opened","repository":{"full_name":"acme/site"},"sender":{"login":"alice"}}'
signature="$(
  printf '%s' "$payload" |
    openssl dgst -sha256 -hmac "$GITHUB_WEBHOOK_SECRET" -binary |
    xxd -p -c 256
)"
 
curl -i \
  -H "Content-Type: application/json" \
  -H "X-GitHub-Event: pull_request" \
  -H "X-GitHub-Delivery: local-1" \
  -H "X-Hub-Signature-256: sha256=${signature}" \
  --data "$payload" \
  http://127.0.0.1:3000/api/webhooks/github

Open http://127.0.0.1:3000 to see the delivery in the dashboard.

Start the same app through the SDK server.

GITHUB_WEBHOOK_SECRET="$(openssl rand -hex 24)" \
RSTREAM_TUNNEL_NAME=nextjs-preview \
RSTREAM_TUNNEL_LABELS=service=nextjs,env=dev \
npm run dev:tunnel

The process prints the public URL.

Public URL: https://<published-rstream-host>

Use that origin as the provider callback base.

https://<published-rstream-host>/api/webhooks/github

The central SDK call binds a Node HTTP server to the tunnel.

const ctrl = await Client.fromEnv().connect();
const tunnel = await ctrl.createTunnel({
  httpVersion: "http/1.1",
  labels,
  name: "nextjs-preview",
  protocol: "http",
  publish: true,
});
 
await tunnel.serve(server, { signal });

The sample wraps this with signal handling, HTTP upgrade forwarding for Next.js development, tunnel cleanup, and exponential backoff after transient disconnects.

Configure stable domains, labels, and edge authentication with environment variables.

NEXT_ALLOWED_DEV_ORIGINS="<your-stable-domain>" \
RSTREAM_TUNNEL_HOSTNAME="<your-stable-domain>" \
RSTREAM_TUNNEL_LABELS=service=nextjs,env=preview,owner=payments \
RSTREAM_TUNNEL_TOKEN_AUTH=1 \
npm run dev:tunnel

Use RSTREAM_TUNNEL_RSTREAM_AUTH=1 for browser-facing previews when rstream Auth is enabled for the project.

For a self-hosted production server, build first and then start the tunnel server.

npm run build
RSTREAM_TUNNEL_LABELS=service=nextjs,env=preview npm run start:tunnel

Keep preview inventory searchable

Labels make preview URLs easy to find from scripts, dashboards, and automation when they are applied consistently in either CLI or SDK mode.

rstream tunnel list \
  --filter 'labels.service=nextjs,labels.env=preview' \
  -o json |
  jq -r '.[] | [.name, .labels.env, .status, (.hostname // "")] | @tsv'

A product backend can also use rstream APIs to show live preview state, generate short-lived access tokens, or clean up old preview tunnels. That is a separate architecture where Next.js controls rstream resources for users, while the tunnel agent still runs on the machine, device, or host that owns the upstream service.