Next.js Integration Guide
Overview
Jetty is the perfect companion for Next.js development, enabling you to:
- Share development previews instantly with designers, stakeholders, or clients
- Test webhooks locally from Stripe, GitHub, Vercel, and other services
- Develop API routes that external services can reach
- Test OAuth flows with real callback URLs
- Debug mobile apps connecting to your Next.js backend
- Collaborate in real-time without deploying to staging
Unlike Vercel previews that require git commits and CI builds, Jetty shares your local development server in seconds—perfect for rapid iteration and testing.
Prerequisites
Before you begin, ensure you have:
- Next.js app (version 12+, including App Router and Pages Router)
- Node.js and npm/yarn/pnpm installed
- Jetty CLI installed on your machine
- Jetty account with an API token
Installing Jetty CLI
If you haven't installed Jetty yet:
curl -fsSL https://usejetty.online/install/jetty.sh | bash
Verify the installation:
jetty --version
Getting Your API Token
- Visit usejetty.online
- Sign in or create an account
- Navigate to Settings → API Tokens
- Create a new token and save it securely
Authenticate the CLI:
jetty auth:login YOUR_API_TOKEN
Quick Start
Share your Next.js development server in under 2 minutes:
# Start your Next.js dev server
npm run dev
# In a new terminal, share it with Jetty
jetty share http://localhost:3000
You'll receive a public URL like:
Tunnel online: https://eager-curie-8x9k2.tunnels.usejetty.online
Share this URL with anyone, anywhere—they'll see your local Next.js app in real-time!
Development Server
Standard Configuration
Next.js typically runs on port 3000 by default:
# Start Next.js dev server
npm run dev
# or
yarn dev
# or
pnpm dev
# Share it via Jetty
jetty share http://localhost:3000
Custom Ports
If your Next.js app uses a custom port:
# Start on custom port
npm run dev -- -p 3001
# Share the custom port
jetty share http://localhost:3001
Or set it in package.json:
{
"scripts": {
"dev": "next dev -p 3001",
"dev:share": "jetty share http://localhost:3001"
}
}
Custom Subdomains
Reserve a memorable subdomain for your project:
jetty share http://localhost:3000 --subdomain my-nextjs-app
Your tunnel will always be at:
https://my-nextjs-app.tunnels.usejetty.online
Common Workflows
Frontend Development
Scenario: Share your UI with designers or stakeholders for feedback.
# Start Next.js with your feature branch
git checkout feature/new-dashboard
npm run dev
# Share with a descriptive subdomain
jetty share http://localhost:3000 --subdomain new-dashboard-preview
Share the URL in Slack, email, or your project management tool. Collaborators see changes in real-time as you save files—no deployments needed!
API Routes Testing
Scenario: Test your Next.js API routes with external services.
Next.js API routes live in:
- App Router:
app/api/*/route.ts - Pages Router:
pages/api/*.ts
Example API route (app/api/hello/route.ts):
export async function GET(request: Request) {
return Response.json({ message: 'Hello from Next.js!' });
}
export async function POST(request: Request) {
const body = await request.json();
return Response.json({ received: body });
}
Share your dev server:
jetty share http://localhost:3000
Test the API endpoint externally:
curl https://your-tunnel.tunnels.usejetty.online/api/hello
Webhook Development
Scenario: Receive webhooks from external services during local development.
Stripe Webhooks
Create a webhook endpoint (app/api/webhooks/stripe/route.ts):
import { headers } from 'next/headers';
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2023-10-16',
});
export async function POST(request: Request) {
const body = await request.text();
const signature = headers().get('stripe-signature')!;
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch (err) {
console.error('Webhook signature verification failed:', err);
return Response.json({ error: 'Invalid signature' }, { status: 400 });
}
// Handle the event
switch (event.type) {
case 'payment_intent.succeeded':
const paymentIntent = event.data.object;
console.log('PaymentIntent succeeded:', paymentIntent.id);
// Your business logic here
break;
case 'customer.subscription.updated':
const subscription = event.data.object;
console.log('Subscription updated:', subscription.id);
break;
default:
console.log(`Unhandled event type: ${event.type}`);
}
return Response.json({ received: true });
}
Configure Stripe to send webhooks to your tunnel:
jetty share http://localhost:3000 --subdomain my-stripe-dev
In your Stripe Dashboard:
- Go to Developers → Webhooks
- Add endpoint:
https://my-stripe-dev.tunnels.usejetty.online/api/webhooks/stripe - Select events to listen for
- Copy the webhook signing secret to your
.env.local
GitHub Webhooks
Create a GitHub webhook endpoint (app/api/webhooks/github/route.ts):
import crypto from 'crypto';
import { headers } from 'next/headers';
function verifySignature(payload: string, signature: string): boolean {
const secret = process.env.GITHUB_WEBHOOK_SECRET!;
const hmac = crypto.createHmac('sha256', secret);
const digest = 'sha256=' + hmac.update(payload).digest('hex');
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(digest));
}
export async function POST(request: Request) {
const body = await request.text();
const signature = headers().get('x-hub-signature-256');
if (!signature || !verifySignature(body, signature)) {
return Response.json({ error: 'Invalid signature' }, { status: 401 });
}
const event = JSON.parse(body);
const eventType = headers().get('x-github-event');
console.log('GitHub event:', eventType);
switch (eventType) {
case 'push':
console.log('Push to:', event.repository.full_name);
console.log('Commits:', event.commits.length);
break;
case 'pull_request':
console.log('PR action:', event.action);
console.log('PR #:', event.pull_request.number);
break;
case 'issues':
console.log('Issue action:', event.action);
break;
}
return Response.json({ received: true });
}
Configure GitHub:
jetty share http://localhost:3000 --subdomain my-github-dev
In your GitHub repository:
- Settings → Webhooks → Add webhook
- Payload URL:
https://my-github-dev.tunnels.usejetty.online/api/webhooks/github - Content type:
application/json - Secret: (set in your
.env.localasGITHUB_WEBHOOK_SECRET) - Select events or choose "Send me everything"
Vercel Deploy Hooks
Test deploy hook integrations locally:
// app/api/webhooks/deploy/route.ts
export async function POST(request: Request) {
const payload = await request.json();
console.log('Deploy webhook received:', {
deployment: payload.deployment,
target: payload.target,
url: payload.url,
});
// Trigger your post-deploy logic
// e.g., clear cache, notify team, update database
return Response.json({ success: true });
}
Mobile App Development
Scenario: Your mobile app (iOS/Android/React Native) needs to connect to your Next.js backend.
# Share with a stable subdomain
jetty share http://localhost:3000 --subdomain mobile-backend-dev
In your mobile app config:
// config.js (React Native example)
const API_BASE_URL = __DEV__
? 'https://mobile-backend-dev.tunnels.usejetty.online'
: 'https://api.production.com';
export { API_BASE_URL };
Now your mobile app can hit your local Next.js API routes during development!
Environment Variables
Next.js has special handling for environment variables. Follow these best practices with Jetty.
Understanding Next.js Environment Variables
Next.js supports two types of environment variables:
NEXT_PUBLIC_*- Exposed to the browser (inlined at build time)- Non-prefixed - Server-side only (never exposed to the browser)
Example .env.local
# Server-side only (never sent to browser)
DATABASE_URL=postgresql://localhost:5432/mydb
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
GITHUB_WEBHOOK_SECRET=your_secret_here
# Exposed to browser (use cautiously!)
NEXT_PUBLIC_API_URL=http://localhost:3000
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
Using Jetty URLs in Environment Variables
When sharing via Jetty, you might want to update your API URLs:
Option 1: Manual update
# .env.local
NEXT_PUBLIC_API_URL=https://my-app.tunnels.usejetty.online
Restart your dev server after changing .env.local.
Option 2: Dynamic API URL
// lib/config.ts
export const API_URL = process.env.NEXT_PUBLIC_API_URL ||
(typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000');
Option 3: Separate env file for tunneling
# .env.local
NEXT_PUBLIC_API_URL=http://localhost:3000
# .env.tunnel
NEXT_PUBLIC_API_URL=https://my-app.tunnels.usejetty.online
Use with:
# Load tunnel env and start dev server
env $(cat .env.tunnel) npm run dev
# In another terminal
jetty share http://localhost:3000 --subdomain my-app
Security Warning
Never put secrets in NEXT_PUBLIC_* variables! They are embedded in the browser bundle and visible to anyone.
# WRONG - Exposed to browser
NEXT_PUBLIC_STRIPE_SECRET_KEY=sk_test_...
# CORRECT - Server-side only
STRIPE_SECRET_KEY=sk_test_...
Vercel Integration
Jetty and Vercel previews serve different purposes and complement each other perfectly.
When to Use Jetty vs Vercel Preview
| Use Case | Jetty | Vercel Preview |
|---|---|---|
| Quick UI feedback (no commit) | ||
| Testing webhooks locally | ||
| Debugging with breakpoints | ||
| Rapid iteration (hot reload) | ||
| Shareable git-based previews | ||
| Production-like environment | ||
| PR previews for team review |
Recommended Workflow
-
Development phase: Use Jetty for rapid iteration and testing
jetty share http://localhost:3000 -
Ready for review: Commit and push to trigger Vercel preview
git add . git commit -m "Add new feature" git push origin feature-branch -
Final testing: Use Vercel preview URL for stakeholder review
Testing Before Deploying to Vercel
Use Jetty to catch issues before they hit Vercel:
# Test your feature locally first
jetty share http://localhost:3000 --subdomain pre-vercel-test
# Verify:
# - All pages load correctly
# - API routes work
# - Webhooks are received
# - Environment variables are set
# Then deploy to Vercel with confidence
git push origin main
API Routes
Complete Webhook Receiver Example
Here's a production-ready webhook receiver with error handling and logging:
// app/api/webhooks/generic/route.ts
import { headers } from 'next/headers';
import crypto from 'crypto';
interface WebhookPayload {
event: string;
data: unknown;
timestamp: number;
}
// Verify webhook signature (example using HMAC)
function verifyWebhookSignature(
payload: string,
signature: string,
secret: string
): boolean {
const hmac = crypto.createHmac('sha256', secret);
const digest = hmac.update(payload).digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(digest)
);
}
export async function POST(request: Request) {
const startTime = Date.now();
try {
// Get raw body for signature verification
const body = await request.text();
const signature = headers().get('x-webhook-signature');
// Verify signature if present
if (signature) {
const secret = process.env.WEBHOOK_SECRET;
if (!secret) {
console.error('WEBHOOK_SECRET not configured');
return Response.json(
{ error: 'Server configuration error' },
{ status: 500 }
);
}
if (!verifyWebhookSignature(body, signature, secret)) {
console.error('Invalid webhook signature');
return Response.json(
{ error: 'Invalid signature' },
{ status: 401 }
);
}
}
// Parse payload
const payload: WebhookPayload = JSON.parse(body);
// Log webhook receipt
console.log('Webhook received:', {
event: payload.event,
timestamp: new Date(payload.timestamp).toISOString(),
processingTime: Date.now() - startTime,
});
// Process webhook based on event type
switch (payload.event) {
case 'user.created':
// Handle user creation
break;
case 'payment.completed':
// Handle payment completion
break;
default:
console.warn('Unhandled webhook event:', payload.event);
}
return Response.json({
received: true,
processedAt: new Date().toISOString(),
processingTimeMs: Date.now() - startTime,
});
} catch (error) {
console.error('Webhook processing error:', error);
return Response.json(
{ error: 'Failed to process webhook' },
{ status: 500 }
);
}
}
OAuth Callback Handler
Example OAuth callback for third-party integrations:
// app/api/auth/callback/route.ts
import { redirect } from 'next/navigation';
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const code = searchParams.get('code');
const state = searchParams.get('state');
const error = searchParams.get('error');
// Handle OAuth errors
if (error) {
console.error('OAuth error:', error);
return redirect('/auth/error?reason=' + error);
}
// Validate state parameter
if (!state) {
return Response.json({ error: 'Missing state' }, { status: 400 });
}
// Exchange code for access token
try {
const tokenResponse = await fetch('https://oauth.provider.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
code,
client_id: process.env.OAUTH_CLIENT_ID,
client_secret: process.env.OAUTH_CLIENT_SECRET,
redirect_uri: `${process.env.NEXT_PUBLIC_API_URL}/api/auth/callback`,
grant_type: 'authorization_code',
}),
});
const tokens = await tokenResponse.json();
// Store tokens (database, session, etc.)
// ...
return redirect('/dashboard?auth=success');
} catch (error) {
console.error('Token exchange failed:', error);
return redirect('/auth/error');
}
}
Configure your OAuth app with the Jetty tunnel URL:
Callback URL: https://my-app.tunnels.usejetty.online/api/auth/callback
Reserved Subdomains
Consider reserving these subdomains for your Next.js projects:
next-staging.tunnels.usejetty.online- Staging environmentnext-preview.tunnels.usejetty.online- Preview deploymentsmy-project-dev.tunnels.usejetty.online- Personal dev environmentmy-project-mobile.tunnels.usejetty.online- Mobile app backend
Reserve a subdomain:
jetty share http://localhost:3000 --subdomain next-staging
The subdomain will be reserved for your account and can be reused across sessions.
Custom Domains
Use your own domain instead of tunnels.usejetty.online:
jetty share http://localhost:3000 --custom-domain dev.myapp.com
You'll need to configure DNS:
CNAME dev.myapp.com -> tunnels.usejetty.online
Benefits:
- Professional URLs for client demos
- Match your production domain structure
- Easier CORS configuration
- Brand consistency
Turborepo / Monorepo
Sharing Next.js apps in a Turborepo or monorepo structure:
Project Structure
my-monorepo/
├── apps/
│ ├── web/ # Next.js app (port 3000)
│ ├── admin/ # Next.js admin (port 3001)
│ └── mobile-api/ # Next.js API (port 3002)
├── packages/
│ └── ui/ # Shared components
└── turbo.json
Sharing Multiple Apps
# Terminal 1: Start all apps
npm run dev
# Terminal 2: Share web app
jetty share http://localhost:3000 --subdomain myapp-web
# Terminal 3: Share admin app
jetty share http://localhost:3001 --subdomain myapp-admin
# Terminal 4: Share mobile API
jetty share http://localhost:3002 --subdomain myapp-mobile-api
Package.json Scripts
{
"scripts": {
"dev": "turbo run dev",
"share:web": "jetty share http://localhost:3000 --subdomain myapp-web",
"share:admin": "jetty share http://localhost:3001 --subdomain myapp-admin",
"share:all": "concurrently \"npm run share:web\" \"npm run share:admin\""
},
"devDependencies": {
"concurrently": "^8.0.0"
}
}
Environment Variables in Monorepo
# apps/web/.env.local
NEXT_PUBLIC_API_URL=https://myapp-mobile-api.tunnels.usejetty.online
DATABASE_URL=postgresql://localhost:5432/myapp
# apps/admin/.env.local
NEXT_PUBLIC_API_URL=https://myapp-mobile-api.tunnels.usejetty.online
NEXT_PUBLIC_WEB_URL=https://myapp-web.tunnels.usejetty.online
Security Considerations
NEXT_PUBLIC_ Exposure
Remember that NEXT_PUBLIC_ variables are embedded in the client bundle:
// DANGEROUS - Exposed to all tunnel visitors
NEXT_PUBLIC_DATABASE_URL=postgresql://...
NEXT_PUBLIC_ADMIN_PASSWORD=secret123
// SAFE - Server-side only
DATABASE_URL=postgresql://...
ADMIN_PASSWORD=secret123
// SAFE - Public data only
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
NEXT_PUBLIC_GOOGLE_MAPS_KEY=AIza...
API Route Authentication
Always authenticate API routes, even during development:
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// Protect API routes
if (request.nextUrl.pathname.startsWith('/api/admin')) {
const authHeader = request.headers.get('authorization');
if (!authHeader || !isValidToken(authHeader)) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
}
return NextResponse.next();
}
function isValidToken(authHeader: string): boolean {
const token = authHeader.replace('Bearer ', '');
return token === process.env.API_SECRET_KEY;
}
export const config = {
matcher: '/api/:path*',
};
CORS with Tunnels
Configure CORS to allow your tunnel domains:
// app/api/data/route.ts
import { NextResponse } from 'next/server';
export async function GET(request: Request) {
const data = { message: 'Hello from API' };
const response = NextResponse.json(data);
// Allow tunnel domains
const origin = request.headers.get('origin');
const allowedOrigins = [
'http://localhost:3000',
'https://my-app.tunnels.usejetty.online',
'https://my-app-staging.tunnels.usejetty.online',
];
if (origin && allowedOrigins.includes(origin)) {
response.headers.set('Access-Control-Allow-Origin', origin);
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
}
return response;
}
export async function OPTIONS(request: Request) {
return new NextResponse(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
});
}
Rate Limiting
Protect your tunnel from abuse:
// lib/rate-limit.ts
import { NextResponse } from 'next/server';
const rateLimitMap = new Map<string, { count: number; resetTime: number }>();
export function rateLimit(
identifier: string,
limit: number = 100,
windowMs: number = 60000
): NextResponse | null {
const now = Date.now();
const record = rateLimitMap.get(identifier);
if (!record || now > record.resetTime) {
rateLimitMap.set(identifier, {
count: 1,
resetTime: now + windowMs,
});
return null;
}
if (record.count >= limit) {
return NextResponse.json(
{ error: 'Too many requests' },
{ status: 429 }
);
}
record.count++;
return null;
}
// Usage in API route
export async function POST(request: Request) {
const ip = request.headers.get('x-forwarded-for') || 'unknown';
const rateLimitResponse = rateLimit(ip, 10, 60000); // 10 req/min
if (rateLimitResponse) {
return rateLimitResponse;
}
// Process request...
}
CI/CD Integration
GitHub Actions
Use Jetty in your CI pipeline for automated testing:
# .github/workflows/test-webhooks.yml
name: Test Webhooks
on:
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Install Jetty CLI
run: curl -fsSL https://usejetty.online/install/jetty.sh | bash
- name: Authenticate Jetty
run: jetty auth:login ${{ secrets.JETTY_API_TOKEN }}
- name: Build Next.js
run: npm run build
- name: Start Next.js server
run: npm start &
env:
PORT: 3000
- name: Wait for server
run: npx wait-on http://localhost:3000
- name: Share via Jetty
run: |
jetty share http://localhost:3000 --subdomain ci-test-${{ github.run_id }} &
sleep 5
- name: Test webhooks
run: npm run test:webhooks
env:
TEST_WEBHOOK_URL: https://ci-test-${{ github.run_id }}.tunnels.usejetty.online
- name: Cleanup
if: always()
run: pkill -f "jetty share"
Vercel Build Command
Test your build with Jetty before deploying:
{
"scripts": {
"build": "next build",
"start": "next start",
"test:build": "npm run build && npm start",
"test:build:share": "npm run test:build & sleep 5 && jetty share http://localhost:3000"
}
}
Troubleshooting
Port Conflicts
Problem: Port 3000 is already in use.
Solution 1: Kill the process using port 3000
# macOS/Linux
lsof -ti:3000 | xargs kill -9
# Windows
netstat -ano | findstr :3000
taskkill /PID <PID> /F
Solution 2: Use a different port
npm run dev -- -p 3001
jetty share http://localhost:3001
HMR/Fast Refresh Issues
Problem: Hot Module Replacement (HMR) doesn't work through the tunnel.
Explanation: HMR uses WebSockets, which Jetty supports, but there can be timing issues.
Solution: Jetty fully supports WebSocket connections for HMR. Ensure you're using the latest version:
jetty update
If issues persist, try:
# Restart both dev server and tunnel
pkill -f "next dev"
pkill -f "jetty share"
npm run dev
jetty share http://localhost:3000
Asset Loading Problems
Problem: Images or CSS not loading through the tunnel.
Check 1: Verify public/hot exists when using npm run dev
# If you see styled content locally but not through tunnel
ls -la public/hot
# If hot file exists, restart Vite dev server
rm public/hot
npm run dev
Check 2: Use correct asset paths
// CORRECT - Relative to public directory
<Image src="/logo.png" alt="Logo" width={100} height={100} />
// WRONG - Absolute local path
<Image src="file:///Users/me/project/public/logo.png" />
Check 3: Check Next.js config for assetPrefix
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
// Don't set assetPrefix for local development
assetPrefix: process.env.NODE_ENV === 'production'
? 'https://cdn.myapp.com'
: undefined,
};
module.exports = nextConfig;
Slow Response Times
Problem: Tunnel responses are slower than local.
Solution 1: Use regional endpoints if available
# Check your current region
jetty config:show
# Set region closer to you
jetty config:set region us-west
Solution 2: Check your internet connection
# Test latency to Jetty
ping tunnels.usejetty.online
Solution 3: Optimize your Next.js app
// next.config.js - Enable SWC minification
const nextConfig = {
swcMinify: true,
compiler: {
removeConsole: process.env.NODE_ENV === 'production',
},
};
Webhook Not Received
Problem: External service says webhook was sent, but you don't see it.
Checklist:
-
Verify tunnel is running:
jetty status -
Check the exact URL:
# Test your endpoint curl -X POST https://your-tunnel.tunnels.usejetty.online/api/webhooks/test \ -H "Content-Type: application/json" \ -d '{"test": true}' -
Check Next.js logs for errors:
# Look for errors in your dev server output npm run dev -
Verify the route exists:
# App Router ls -la app/api/webhooks/*/route.ts # Pages Router ls -la pages/api/webhooks/*.ts -
Test with verbose logging:
export async function POST(request: Request) { console.log('=== WEBHOOK RECEIVED ==='); console.log('Headers:', Object.fromEntries(request.headers)); console.log('Body:', await request.text()); console.log('========================'); return Response.json({ received: true }); }
Tips & Tricks
Shell Aliases
Add to your ~/.zshrc or ~/.bashrc:
# Quick Next.js sharing
alias nshare='jetty share http://localhost:3000'
# Share with project name from directory
alias nshare-auto='jetty share http://localhost:3000 --subdomain $(basename "$PWD")-dev'
# Share with production-like URL
alias nshare-prod='jetty share http://localhost:3000 --subdomain $(basename "$PWD")'
# Start dev and share in one command
alias ndev='npm run dev & sleep 3 && jetty share http://localhost:3000'
Package.json Scripts
{
"scripts": {
"dev": "next dev",
"dev:share": "next dev & sleep 3 && jetty share http://localhost:3000",
"dev:share:custom": "next dev & sleep 3 && jetty share http://localhost:3000 --subdomain $npm_package_name-dev",
"share": "jetty share http://localhost:3000",
"share:prod": "npm run build && npm start & sleep 5 && jetty share http://localhost:3000 --subdomain $npm_package_name-prod"
}
}
Usage:
npm run dev:share
# or
yarn dev:share
VS Code Tasks
Create .vscode/tasks.json:
{
"version": "2.0.0",
"tasks": [
{
"label": "Next.js: Dev Server",
"type": "npm",
"script": "dev",
"problemMatcher": [],
"isBackground": true
},
{
"label": "Jetty: Share Tunnel",
"type": "shell",
"command": "jetty share http://localhost:3000 --subdomain my-project-dev",
"problemMatcher": [],
"isBackground": true
},
{
"label": "Dev + Share",
"dependsOn": [
"Next.js: Dev Server",
"Jetty: Share Tunnel"
],
"problemMatcher": []
}
]
}
Run with: Cmd+Shift+P → "Tasks: Run Task" → "Dev + Share"
QR Code for Mobile Testing
Generate a QR code for easy mobile access:
# Install qrencode (macOS)
brew install qrencode
# Share and generate QR code
TUNNEL_URL=$(jetty share http://localhost:3000 --subdomain my-app | grep -o 'https://[^ ]*')
echo $TUNNEL_URL | qrencode -t UTF8
Scan with your phone camera to open the tunnel!
Auto-Copy Tunnel URL
Automatically copy the tunnel URL to clipboard:
# macOS
jetty share http://localhost:3000 | grep -o 'https://[^ ]*' | pbcopy
# Linux (requires xclip)
jetty share http://localhost:3000 | grep -o 'https://[^ ]*' | xclip -selection clipboard
# Windows (PowerShell)
jetty share http://localhost:3000 | Select-String -Pattern 'https://[^ ]*' | Set-Clipboard
Add to package.json:
{
"scripts": {
"share:copy": "jetty share http://localhost:3000 | grep -o 'https://[^ ]*' | pbcopy && echo 'Tunnel URL copied to clipboard!'"
}
}
Environment-Specific Sharing
# Development with debug logs
NODE_ENV=development DEBUG=* npm run dev &
jetty share http://localhost:3000 --subdomain myapp-debug
# Production build testing
npm run build && NODE_ENV=production npm start &
jetty share http://localhost:3000 --subdomain myapp-prod-test
Multiple Tunnels for Different Features
# Feature branch 1
git checkout feature/new-auth
npm run dev -- -p 3000 &
jetty share http://localhost:3000 --subdomain myapp-new-auth
# Feature branch 2 (different terminal)
git checkout feature/new-dashboard
npm run dev -- -p 3001 &
jetty share http://localhost:3001 --subdomain myapp-new-dashboard
Share both URLs with your team for parallel review!
Next Steps
- Explore advanced features: Check out Custom Domains and Team Collaboration
- Integrate with your workflow: Set up CI/CD Integration
- Join the community: Share your Next.js + Jetty workflows in our Discord
Send feedback
Found an issue or have a suggestion? Let us know.