Modern Developer Workflows & Advanced Infrastructure Integrations
Discover why standard HTTP proxies drop Next.js 15+ Server Actions and Vite WebSocket HMR connections, and how to maintain sub-millisecond hot-reloading over remote tunnels.

Quick answer
Tunneling Vite 6 & Next.js Server Actions: Fix HMR: webhook testing answer
For local webhook testing, run your app locally, expose it with a public HTTPS tunnel, and paste the stable callback URL into the provider dashboard.
How do I test webhooks on localhost?
Start your local server, open a public HTTPS tunnel to that port, configure the provider webhook URL, and inspect events in your local logs.
Why does a stable webhook URL matter?
Stable URLs prevent provider dashboards from needing manual callback updates every time you restart a tunnel.
Modern full-stack architecture balances remote cloud runtimes with instantaneous feedback loops on developer machines. Achieving rapid iteration—whether in Next.js frontend applications or distributed microservices—requires routing live traffic between cloud boundaries and local developer environments.
This guide provides technical strategies for proxying local dev servers, forwarding telemetry spans, routing multi-tenant webhooks, and attaching cloud previews to ephemeral database instances.
1. Hot-Reload Across Borders: Tunneling Vite 6 and Next.js Server Actions over HMR-Aware Proxies
Traditional HTTP reverse proxies (like default Nginx or basic SSH tunnels) break stateful web applications because they evaluate traffic statelessly. Modern development tools like Vite 6 and Next.js 15+ rely on bidirectional, low-latency connections to sync client states, compile server-side modules, and process Server Actions.
+-------------------------------------------------------------------------------+
| PUBLIC CLOUD TUNNEL |
| https://dev-tenant.app.example.com |
+-------------------------------------------------------------------------------+
|
| TLS Terminal & WSS Upgrade
v
+-------------------------------------------------------------------------------+
| HMR-AWARE REVERSE TUNNEL PROXY |
| - Host Header Rewriting (Origin Verification) |
| - HTTP/1.1 Upgrade Handling (101 Switching Protocols) |
| - Keep-Alive Pings & Dynamic Client Port Mapping |
+-------------------------------------------------------------------------------+
|
+--------------------------+--------------------------+
| (gRPC / Multiplexed TCP) | (HTTP/2 Server Actions)
v v
+-----------------------+ +-----------------------+
| Vite Dev Server | | Next.js App Router |
| (Localhost:5173) | | (Localhost:3000) |
| | | |
| - HMR WebSocket Path: | | - CSRF Allowed Host: |
| /_vite_hmr | | dev-tenant.example |
+-----------------------+ +-----------------------+
Why Traditional Proxies Break HMR and Server Actions
- WebSocket Framing Dropped: Standard HTTP proxies treat incoming requests as short-lived request/response pairs. When Vite attempts an HTTP/1.1
Upgrade: websocketexchange to establish its Hot Module Replacement (HMR) channel, stateless proxies fail to maintain the persistent TCP socket. This forces Vite into continuous fallback reloads. - Next.js 15+ Host Header & CSRF Checks: Next.js Server Actions execute via HTTP POST requests using hidden endpoint IDs. To prevent Cross-Site Request Forgery (CSRF) attacks, Next.js inspects the incoming
OriginandHostheaders. If a tunnel rewritesHost: localhost:3000without matching the public tunnel URL, Next.js blocks the invocation. - Mismatched WebSocket Ports: Vite injects a client-side runtime script into the browser that attempts to open a WebSocket connection back to the HMR port. If the browser connects to
[https://app.example.com](https://app.example.com)(port 443) but Vite tells the client to open a socket tows://localhost:5173, cross-origin browser security blocks the connection.
Preservation Architecture for Vite 6
To maintain sub-millisecond HMR over public tunnels (such as Cloudflare Tunnels, frp, or ngrok), configure Vite’s internal WebSocket server to decouple its internal listening port from its public client-facing port.
// vite.config.ts
import { defineConfig } from 'vite';
export default defineConfig({
server: {
host: '0.0.0.0',
port: 5173,
strictPort: true,
hmr: {
// The public hostname exposed by your tunnel
host: 'vite-tunnel.dev.example.com',
// Force client to use HTTPS/WSS port instead of internal 5173
clientPort: 443,
protocol: 'wss',
path: '/_vite_hmr',
},
// Prevent HMR connection dropouts due to host mismatch
cors: true,
},
});
Next.js 15+ Server Action Tunnel Configuration
For Next.js 15, update next.config.ts to allow invocation of Server Actions through public dev tunnels.
// next.config.ts
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
experimental: {
serverActions: {
// Whitelist tunnel origins to pass CSRF origin checks
allowedOrigins: [
'next-tunnel.dev.example.com',
'*.tunnel.dev.example.com',
],
},
},
};
export default nextConfig;
2. Routing Distributed Traces from Cloud Staging to Your Local Jaeger UI
When debugging microservices in staging environments, tracing an asynchronous request through multiple services is vital. However, sending telemetry data to a central telemetry storage backend creates noise and makes isolation difficult.
Using OpenTelemetry (OTLP) collectors, backend engineers can proxy telemetry streams down to a local Jaeger UI instance without polluting remote storage.
+-----------------------------------------------------------------------------------+
| CLOUD STAGING CLUSTER |
| |
| +---------------------+ +---------------------+ +-------------------+ |
| | Auth Microservice | ---> | Order Microservice | ---> | Payment Service | |
| +---------------------+ +---------------------+ +-------------------+ |
| | |
| | OTLP / gRPC (Port 4317) |
| v |
| +---------------------------------------+ |
| | Cloud Staging OTLP Collector | |
| +---------------------------------------+ |
+------------------------------------------|----------------------------------------+
|
| Routing via Tailscale/SSH Reverse Tunnel
v
+-----------------------------------------------------------------------------------+
| LOCAL DEVELOPER WORKSTATION |
| |
| +-----------------------------------------------------------------------------+ |
| | Local OpenTelemetry Collector | |
| | - Receives: OTLP / gRPC on 127.0.0.1:4317 | |
| | - Filter: attributes["x-developer-id"] == "dev-alice" | |
| +-----------------------------------------------------------------------------+ |
| | |
| v OTLP / gRPC (Insecure) |
| +-----------------------------------------------------------------------------+ |
| | Local Jaeger UI Container | |
| | - Ingestion Port: 4317 | |
| | - Web UI: http://localhost:16686 | |
| +-----------------------------------------------------------------------------+ |
+-----------------------------------------------------------------------------------+
The Trace Hijacking Problem
Sending trace spans directly to cloud stores like Datadog, Honeycomb, or a shared AWS OpenSearch instance presents two distinct issues:
- Index Contamination: Debugging logs and malformed payloads clutter production and staging dashboards.
- Latency & Rate Limits: Exporting high-volume spans over internet bounds increases cloud ingestion costs and risks exceeding vendor rate limits.
Architecture: Selective OTLP Reverse Routing
Engineers can selectively isolate and forward traces using contextual headers (such as x-developer-id: dev-alice) processed by an OpenTelemetry Collector routing connector.
Step 1: Staging OpenTelemetry Collector Configuration
Configure the staging OTLP collector to evaluate incoming trace attributes and route tagged spans through a reverse gRPC pipeline back to a local machine.
# otel-collector-staging.yaml
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
processors:
batch:
timeout: 1s
send_batch_size: 256
exporters:
# Standard cloud storage destination
otlp/staging_backend:
endpoint: "tempo-staging.internal.net:4317"
tls:
insecure: true
# Reverse tunnel destination pointing to the developer machine
otlp/dev_alice_local:
endpoint: "host.docker.internal:14317"
tls:
insecure: true
connector:
routing:
default_exporters: [otlp/staging_backend]
from_attribute: "x-developer-id"
table:
- value: "dev-alice"
exporters: [otlp/dev_alice_local]
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [routing]
Step 2: Establish the Reverse Tunnel and Local Ingestion
Establish a reverse SSH tunnel from your developer workstation to the staging gateway, mapping port 14317 on the staging node to port 4317 locally.
# Establish reverse SSH gRPC tunnel
ssh -R 14317:localhost:4317 developer@staging-gateway.internal.net -N
Run a lightweight local Jaeger container (configured with native OTLP ingestion):
docker run -d --name jaeger-local \
-e COLLECTOR_OTLP_ENABLED=true \
-p 16686:16686 \
-p 4317:4317 \
jaegertracing/all-in-one:latest
When an HTTP request containing the header x-developer-id: dev-alice enters the staging environment, its entire trace tree branches off and streams directly to your local Jaeger UI at http://localhost:16686.
3. Testing Local Stripe Connect Webhooks with Dynamic Multi-Tenant Subdomains
Testing multi-tenant SaaS platforms requires isolating events for distinct organizations. When building multi-tenant architectures using Stripe Connect (where a platform manages multiple connected accounts), debugging webhooks through a single endpoint hides dynamic tenant resolution issues.
STRIPE CLOUD
|
+----------------------+----------------------+
| Connect Webhook Event |
| Account: acct_tenant_A | Account: acct_tenant_B
v v
+------------------------------+ +------------------------------+
| https://tenant-a.tunnel.dev | | https://tenant-b.tunnel.dev |
+------------------------------+ +------------------------------+
| |
+----------------------+----------------------+
|
v
+-------------------------------+
| WILDCARD INGRESS PROXY |
| *.tunnel.dev -> localhost:8080|
+-------------------------------+
|
v
+-------------------------------+
| LOCAL MULTI-TENANT ROUTER |
| Extracts Host header |
| Maps Tenant A vs Tenant B |
+-------------------------------+
Multi-Tenant Webhook Routing Setup
Using a wildcard reverse proxy (such as frp or caddy), point *.tunnel.yourdomain.dev to your local application router.
1. Caddy Reverse Proxy Configuration (Local Ingress Router)
# Caddyfile - Multi-tenant Wildcard Local Proxy
*.tunnel.dev.localhost {
tls internal
@tenant_a header_regexp host Host ^([a-zA-Z0-9-]+)\.tunnel\.dev\.localhost$
handle {
reverse_proxy localhost:3000 {
header_up Host {http.request.host}
header_up X-Forwarded-Host {http.request.host}
}
}
}
2. Advanced Multi-Tenant Webhook Forwarder script
Instead of running individual stripe listen commands per tenant, use this Node.js multi-tenant CLI proxy script. It listens to Stripe events and dynamically forwards them to the corresponding local subdomain based on the event’s account ID.
// scripts/stripe-multitenant-proxy.ts
import Stripe from 'stripe';
import axios from 'axios';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2026-01-28',
});
// Map Stripe Connected Account IDs to Local Subdomains
const TENANT_MAPPING: Record<string, string> = {
'acct_1N01AAA000000000': 'acme',
'acct_1N02BBB000000000': 'globex',
};
async function handleWebhookEvent(event: Stripe.Event) {
const connectedAccountId = event.account;
const tenantSlug = connectedAccountId ? TENANT_MAPPING[connectedAccountId] : 'app';
if (!tenantSlug) {
console.warn(`[WARN] Unmapped account ID: ${connectedAccountId}`);
return;
}
const targetUrl = `http://${tenantSlug}.tunnel.dev.localhost:3000/api/webhooks/stripe`;
try {
console.log(`[FORWARD] Event ${event.type} -> ${targetUrl}`);
await axios.post(targetUrl, event, {
headers: {
'Stripe-Signature': 'simulated_local_sig',
'Content-Type': 'application/json',
'Host': `${tenantSlug}.tunnel.dev.localhost`,
},
});
} catch (err: any) {
console.error(`[ERROR] Failed to deliver webhook: ${err.message}`);
}
}
4. Exposing Local Supabase and Postgres Instances to Cloud Vercel/Netlify Preview Deployments
Feature branch workflows automatically spin up ephemeral frontend preview deployments on platforms like Vercel or Netlify. However, providing isolated backend database state for every PR preview can be difficult to manage.
Using Layer 4 TCP reverse tunneling (such as ngrok tcp, bore, or frp), you can safely expose your local PostgreSQL or Dockerized Supabase instance directly to cloud preview deployments.
+-----------------------------------------------------------------------------+
| VERCEL / NETLIFY PREVIEW PR #42 |
| https://app-git-feature-pr42.vercel.app |
| |
| DATABASE_URL = postgresql://postgres:pass@tcp.tunnel.dev:19432/postgres |
+-----------------------------------------------------------------------------+
|
| Encrypted TCP (Port 19432)
v
+-----------------------------------------------------------------------------+
| TCP REVERSE PROXY |
| - Pass-through TLS / Pure L4 TCP Pipeline |
+-----------------------------------------------------------------------------+
|
| Secure Reverse Tunnel
v
+-----------------------------------------------------------------------------+
| LOCAL DEVELOPER WORKSTATION |
| |
| +-----------------------------------------------------------------------+ |
| | Local Postgres Firewall / PgBouncer Proxy | |
| | - Listens on 127.0.0.1:5432 | |
| | - Restricts SSL / Validates Session Limits | |
| +-----------------------------------------------------------------------+ |
| | |
| v |
| +-----------------------------------------------------------------------+ |
| | Dockerized Supabase Instance | |
| | - PostgreSQL Engine (Port 5432) | |
| | - PostgREST API (Port 54321) | |
| | - GoTrue Auth System (Port 9999) | |
| +-----------------------------------------------------------------------+ |
+-----------------------------------------------------------------------------+
Protocol Comparison: Layer 4 (TCP) vs Layer 7 (HTTP)
- HTTP Proxies (Layer 7): Unsuitable for database engines. They expect plain HTTP/WebSockets, break wire protocols like Postgres
StartupMessage, and drop non-HTTP connections. - TCP Tunnels (Layer 4): Forward raw TCP packets at the transport layer, keeping wire protocols intact.
Exposing Supabase Local Stack via FRP (Fast Reverse Proxy)
To tunnel your local Supabase stack, set up frp to expose local PostgreSQL (port 5432) and PostgREST (port 54321) over L4 TCP.
Server Configuration (frps.ini on Remote Gateway)
[common]
bind_port = 7000
vhost_extra_db_port = 19432
Client Configuration (frpc.ini on Developer Workstation)
[common]
server_addr = gateway.yourdomain.dev
server_port = 7000
[supabase-postgres-tcp]
type = tcp
local_ip = 127.0.0.1
local_port = 54322
remote_port = 19432
[supabase-postgrest-http]
type = http
local_ip = 127.0.0.1
local_port = 54321
custom_domains = api-pr42.tunnel.yourdomain.dev
Automation via GitHub Actions & Vercel API
Automate your pull request workflows by dynamically setting environment variables on ephemeral preview deployments using a GitHub Action.
# .github/workflows/preview-environment.yml
name: Preview Environment Database Injector
on:
pull_request:
types: [opened, synchronize]
jobs:
attach-local-db:
runs-on: ubuntu-latest
steps:
- name: Inject Dynamic DB Tunnel URL to Vercel Preview
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
run: |
PR_NUM=${{ github.event.number }}
# Construct dynamic database URL pointing to the developer's assigned TCP tunnel port
TUNNEL_DB_URL="postgresql://postgres:postgres@gateway.yourdomain.dev:19432/postgres?sslmode=require"
# Assign database connection string to Vercel Preview deployment environment
curl -X POST "https://api.vercel.com/v10/projects/${VERCEL_PROJECT_ID}/env" \
-H "Authorization: Bearer ${VERCEL_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"key": "DATABASE_URL",
"value": "'"${TUNNEL_DB_URL}"'",
"type": "encrypted",
"target": ["preview"],
"gitBranch": "${{ github.head_ref }}"
}'
Architectural Summary: Reverse Proxies & Integration Patterns
| Pattern | Protocol Layer | Best Used For | Key Edge Cases & Caveats |
|---|---|---|---|
| HMR Proxying | Layer 7 (HTTP/WSS) | Vite 6 / Next.js 15+ local hot reloading across remote tunnels | Requires allowedOrigins whitelist in Next.js and explicitly overriding clientPort in Vite. |
| OTLP Trace Routing | Layer 7 (gRPC / HTTP/2) | Isolating microservice trace spans locally via OpenTelemetry | Requires header-propagation middleware (x-developer-id) across service boundaries. |
| Dynamic Subdomains | Layer 7 (HTTP/SNI) | Multi-tenant SaaS webhook & checkout route simulation | Requires wildcard SSL certificates (*.domain.dev) or local CA authority installation. |
| L4 Database Reverse Tunnels | Layer 4 (Pure TCP) | Linking Vercel/Netlify cloud preview environments to local databases | High connection concurrency can exhaust local database pools; use PgBouncer locally. |
Related InstaTunnel pages
Continue from this article into the most relevant product guides and workflows.
Related Topics
Keep building with InstaTunnel
Read the docs for implementation details or compare plans before you ship.