local.dev Tunnel Service - Design Analysis & Revised Plan
Date: November 19, 2025 Status: CRITICAL REVIEW - Architectural & Business Model Revisions Required
Executive Summary
After comprehensive analysis of the PRD, significant revisions are required before proceeding with implementation. The original design has fundamental architectural inefficiencies, unrealistic business assumptions, and an unclear competitive positioning.
Key Findings
🔴 CRITICAL ISSUES:
- Per-customer Fly Machines model is expensive and over-engineered
- 10% conversion rate assumption is 2-3x higher than industry reality
- 12-week timeline underestimates complexity by 50-75%
- Free tier is too restrictive to build product trust
- No clear differentiation from ngrok/Cloudflare
🟡 MAJOR CONCERNS:
- Mixed security model (HTTP for free tier) is dangerous
- Missing scalability architecture for 10K+ concurrent tunnels
- Incomplete database schema for production features
- No operational excellence strategy (SLOs, monitoring, incident response)
- Enterprise features ($499 tier) too ambitious for MVP
🟢 STRENGTHS:
- Fly.io platform choice is solid for global edge
- WebSocket tunnel protocol is appropriate
- Market opportunity exists for preview environments
- Basic CLI design is reasonable foundation
Part 1: Architectural Analysis
Current PRD Architecture
┌─────────────────────────────────────┐
│ Fly Edge → Gateway App │
│ ↓ │
│ Per-Customer Fly Machines │
│ (CoreDNS + Caddy + Node Proxy) │
└─────────────────────────────────────┘Critical Flaws
1. Per-Customer Machine Model (BLOCKER)
Problem: Each customer gets dedicated Fly Machine with CoreDNS, Caddy, and Node proxy.
Issues:
- Stopped machines still incur storage costs ($2-5/customer)
- At 1000 customers = $2000-5000/month infrastructure (not $500 as claimed)
- Unnecessary complexity managing N machines via Machines API
- DNS/SSL duplication per customer is wasteful
Evidence:
// From PRD workspace-manager.js
async createCustomerWorkspace(customer, tier) {
const machine = await this.client.create({
// Creates NEW machine per customer
config: {
image: 'localdev/workspace:latest',
// Each has CoreDNS, Caddy, Node - duplicated infrastructure
}
});
}2. No Request Multiplexing (HIGH PRIORITY)
Problem: CLI client has no concurrent request handling.
Issues:
- Each request blocks until response received
- Modern web apps need 10-50 concurrent requests (JS, CSS, images, API)
- Single-threaded forwarding creates 10-50x latency
Evidence:
// From PRD cli.js - synchronous request handling
async forwardToLocal(request) {
return new Promise((resolve) => {
const req = http.request({ /* ... */ }, (res) => {
// Blocks until complete - no concurrency
});
});
}3. Mixed Security Model (SECURITY RISK)
Problem: Free tier is "HTTP only" while paid tiers get HTTPS.
Issues:
- Developers testing OAuth/webhooks need HTTPS (won't work on free tier)
- Sensitive data exposure on public internet
- Contradicts claim of "all traffic encrypted via TLS 1.3"
Recommendation: ALL tiers must have TLS. Limit free tier in other ways (bandwidth, session time, features).
4. No Scalability Strategy (FUTURE BLOCKER)
Problem: No discussion of how to scale to 10,000+ concurrent tunnels.
Missing:
- Connection pooling and state management
- Gateway instance coordination (which instance owns which tunnel?)
- Routing table caching strategy (Redis lookups per request = latency)
- Stateless design for horizontal scaling
Proposed Simplified Architecture
┌──────────────────────────────────────────────────────────┐
│ STATELESS GATEWAY TIER │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Fly.io Gateway Instances (Horizontal Scaling) │ │
│ │ - WS Connection Handling │ │
│ │ - TLS Termination (Caddy/Let's Encrypt) │ │
│ │ - HTTP Proxy Logic │ │
│ │ - Session Routing via Redis │ │
│ └────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
↓
┌──────────────────────┐
│ ROUTING REGISTRY │
│ │
│ Redis (Cache) │
│ subdomain → ws_id │
│ │
│ PostgreSQL (Source) │
│ tunnels table │
└──────────────────────┘Architecture Benefits
| Aspect | Original Design | Simplified Design |
|---|---|---|
| Per-customer cost | $2-5/month (stopped machine storage) | $0 (stateless) |
| Scalability | Complex (N machines × M regions) | Simple (stateless horizontal) |
| Operational complexity | High (Machines API orchestration) | Low (standard web app) |
| Latency | 2 hops (Gateway → Machine → localhost) | 1 hop (Gateway → localhost) |
| Infrastructure duplication | Yes (DNS/SSL per customer) | No (centralized) |
Technical Implementation Details
Gateway Request Flow
1. Public HTTP(S) Request
↓
2. Gateway receives on subdomain (e.g., abc123.local.dev)
↓
3. Lookup in Redis: subdomain → ws_connection_id
↓
4. If not in Redis, query PostgreSQL, cache result
↓
5. Send request to WS connection (identified by ws_connection_id)
↓
6. CLI client receives request, forwards to localhost:PORT
↓
7. CLI client sends response back via WS
↓
8. Gateway streams response to original HTTP requesterWebSocket Message Protocol
// Client → Gateway (tunnel registration)
{
type: 'register',
apiKey: string,
localPort: number,
requestedSubdomain?: string // Optional custom subdomain
}
// Gateway → Client (registration success)
{
type: 'registered',
tunnelId: string,
subdomain: string,
publicUrl: string
}
// Gateway → Client (HTTP request)
{
type: 'http_request',
requestId: string,
method: string,
path: string,
headers: Record<string, string>,
body?: string
}
// Client → Gateway (HTTP response)
{
type: 'http_response',
requestId: string,
statusCode: number,
headers: Record<string, string>,
body: string
}
// Heartbeat (bidirectional)
{
type: 'ping' | 'pong',
timestamp: number
}Request Multiplexing Strategy
// Client-side concurrent request handling
class TunnelClient {
constructor() {
this.pendingRequests = new Map(); // requestId → Promise
this.requestQueue = [];
this.maxConcurrent = 50; // Handle 50 concurrent requests
}
async handleIncomingRequest(message) {
// Non-blocking: add to queue, process concurrently
this.requestQueue.push(message);
this.processQueue();
}
async processQueue() {
while (this.requestQueue.length > 0 &&
this.pendingRequests.size < this.maxConcurrent) {
const request = this.requestQueue.shift();
// Fire and forget - doesn't block
this.forwardToLocal(request).catch(err => {
this.handleError(request.id, err);
});
}
}
}DNS and Proxy Implementation
DNS Architecture - Centralized vs. Per-Customer
Original PRD Design (Inefficient):
- Each customer workspace runs CoreDNS instance
- Duplicated DNS configuration N times
- Additional resource overhead per customer
Simplified Design (Efficient):
- Single wildcard DNS entry at domain registrar
- Fly.io handles all subdomain routing
- Zero per-customer DNS overhead
DNS Configuration
One-Time Setup:
# DNS records at your registrar (e.g., Cloudflare, Route53)
A local.dev → [Fly.io IPv4 from: fly ips list]
AAAA local.dev → [Fly.io IPv6 from: fly ips list]
CNAME *.local.dev → localdev-gateway.fly.dev
# Fly.io automatically routes ALL subdomains to gateway:
# - abc123.local.dev → Gateway
# - pr-456.local.dev → Gateway
# - anything.local.dev → GatewayGateway Subdomain Routing:
// Gateway extracts subdomain and routes to correct tunnel
const express = require('express');
const app = express();
app.use(async (req, res, next) => {
// Extract subdomain from hostname
const hostname = req.hostname; // e.g., "abc123.local.dev"
const subdomain = hostname.replace('.local.dev', ''); // "abc123"
// Look up which WebSocket connection owns this subdomain
let wsConnectionId = await redis.get(`subdomain:${subdomain}`);
if (!wsConnectionId) {
// Not in cache, check database
const tunnel = await db.query(
'SELECT ws_connection_id FROM tunnels WHERE subdomain = $1 AND status = $2',
[subdomain, 'active']
);
if (!tunnel) {
return res.status(404).send('Tunnel not found');
}
wsConnectionId = tunnel.ws_connection_id;
// Cache for 60 seconds
await redis.setex(`subdomain:${subdomain}`, 60, wsConnectionId);
}
// Attach to request for proxy middleware
req.tunnelConnectionId = wsConnectionId;
next();
});
// Proxy middleware handles actual forwarding
app.use(proxyMiddleware);Custom Domain Support
User Flow:
1. User adds custom domain in dashboard: "their-app.com"
2. System generates DNS verification record
3. User adds CNAME record at their registrar
4. System validates DNS record exists
5. System provisions SSL certificate via Caddy
6. Requests to their-app.com → Gateway → TunnelImplementation:
// POST /api/domains - User adds custom domain
app.post('/api/domains', authenticate, async (req, res) => {
const { domain } = req.body;
const userId = req.user.id;
// Generate verification token
const verificationToken = crypto.randomBytes(16).toString('hex');
const verificationSubdomain = `_localdev-verify-${verificationToken}`;
// Store in database
await db.query(
`INSERT INTO custom_domains (user_id, domain, validation_token, dns_verified)
VALUES ($1, $2, $3, false)`,
[userId, domain, verificationToken]
);
res.json({
domain,
status: 'pending_verification',
dns_records: [
{
type: 'TXT',
name: verificationSubdomain,
value: `localdev-verification=${verificationToken}`,
ttl: 300
},
{
type: 'CNAME',
name: domain,
value: 'localdev-gateway.fly.dev',
ttl: 3600
}
],
instructions: `Add these DNS records at your domain registrar, then click 'Verify'`
});
});
// POST /api/domains/:domain/verify - Verify DNS
app.post('/api/domains/:domain/verify', authenticate, async (req, res) => {
const { domain } = req.params;
const userId = req.user.id;
// Get verification token
const record = await db.query(
'SELECT validation_token FROM custom_domains WHERE user_id = $1 AND domain = $2',
[userId, domain]
);
if (!record) {
return res.status(404).json({ error: 'Domain not found' });
}
// Check DNS records
const dns = require('dns').promises;
const verificationSubdomain = `_localdev-verify-${record.validation_token}`;
try {
const txtRecords = await dns.resolveTxt(verificationSubdomain);
const verified = txtRecords.some(records =>
records.includes(`localdev-verification=${record.validation_token}`)
);
if (verified) {
// Mark as verified
await db.query(
'UPDATE custom_domains SET dns_verified = true, verified_at = NOW() WHERE domain = $1',
[domain]
);
// Trigger SSL certificate provisioning
await provisionSSL(domain);
return res.json({
status: 'verified',
message: 'Domain verified successfully. SSL certificate provisioning in progress.'
});
}
} catch (err) {
return res.status(400).json({
error: 'DNS verification failed',
details: 'TXT record not found or incorrect'
});
}
});
// Gateway routing for custom domains
app.use(async (req, res, next) => {
const hostname = req.hostname;
// Check if it's a subdomain OR custom domain
let wsConnectionId;
if (hostname.endsWith('.local.dev')) {
// Subdomain routing (existing logic)
const subdomain = hostname.replace('.local.dev', '');
wsConnectionId = await redis.get(`subdomain:${subdomain}`);
} else {
// Custom domain routing
wsConnectionId = await redis.get(`domain:${hostname}`);
if (!wsConnectionId) {
// Check database
const domain = await db.query(
`SELECT t.ws_connection_id
FROM custom_domains cd
JOIN tunnels t ON t.user_id = cd.user_id
WHERE cd.domain = $1 AND cd.dns_verified = true AND t.status = 'active'
LIMIT 1`,
[hostname]
);
if (domain) {
wsConnectionId = domain.ws_connection_id;
await redis.setex(`domain:${hostname}`, 60, wsConnectionId);
}
}
}
if (!wsConnectionId) {
return res.status(404).send('Tunnel not found');
}
req.tunnelConnectionId = wsConnectionId;
next();
});Automatic SSL with Caddy
Caddy Configuration (Caddyfile):
{
# Automatic HTTPS for all domains
email ssl@local.dev
# On-demand TLS: issue certs as requests come in
on_demand_tls {
ask http://localhost:3000/caddy/cert-allowed
interval 2m
burst 5
}
}
# Wildcard for all *.local.dev subdomains
*.local.dev {
reverse_proxy localhost:3000
tls {
on_demand
}
}
# Custom domains (validated before cert issuance)
:443 {
reverse_proxy localhost:3000
tls {
on_demand
}
}Cert Validation Endpoint:
// Caddy asks: "Should I issue a cert for this domain?"
app.get('/caddy/cert-allowed', async (req, res) => {
const domain = req.query.domain;
// Allow *.local.dev (our subdomains)
if (domain.endsWith('.local.dev')) {
return res.status(200).send('OK');
}
// Check if custom domain is verified
const verified = await db.query(
'SELECT 1 FROM custom_domains WHERE domain = $1 AND dns_verified = true',
[domain]
);
if (verified) {
return res.status(200).send('OK');
}
// Reject unknown domains
res.status(403).send('Domain not allowed');
});Benefits:
- ✅ Automatic cert issuance (no manual CSR/private keys)
- ✅ Auto-renewal (90-day Let's Encrypt certs renewed automatically)
- ✅ Zero per-domain configuration
- ✅ Wildcard cert for *.local.dev
- ✅ Individual certs for custom domains
HTTP/HTTPS Proxy Implementation
Gateway Proxy Middleware:
const WebSocket = require('ws');
const { v4: uuidv4 } = require('uuid');
class TunnelProxyMiddleware {
constructor() {
this.activeConnections = new Map(); // wsConnectionId → WebSocket
this.pendingRequests = new Map(); // requestId → { resolve, reject, timeout }
}
// Register a WebSocket connection when CLI connects
registerConnection(wsConnectionId, ws) {
this.activeConnections.set(wsConnectionId, ws);
// Handle responses from CLI client
ws.on('message', (data) => {
const message = JSON.parse(data);
if (message.type === 'http_response') {
this.handleResponse(message);
}
});
ws.on('close', () => {
this.activeConnections.delete(wsConnectionId);
});
}
// Middleware function
async handle(req, res, next) {
const wsConnectionId = req.tunnelConnectionId;
if (!wsConnectionId) {
return res.status(500).send('No tunnel connection ID');
}
const ws = this.activeConnections.get(wsConnectionId);
if (!ws || ws.readyState !== WebSocket.OPEN) {
return res.status(503).send('Tunnel disconnected');
}
try {
// Collect request body
const chunks = [];
req.on('data', chunk => chunks.push(chunk));
req.on('end', async () => {
const body = Buffer.concat(chunks);
// Send HTTP request over WebSocket tunnel
const response = await this.sendRequest(ws, {
method: req.method,
path: req.url,
headers: req.headers,
body: body.toString('base64')
});
// Stream response back to client
res.status(response.statusCode);
Object.entries(response.headers || {}).forEach(([key, value]) => {
res.setHeader(key, value);
});
res.send(Buffer.from(response.body, 'base64'));
});
} catch (err) {
if (err.message === 'Tunnel timeout') {
return res.status(504).send('Tunnel timeout');
}
return res.status(500).send('Tunnel error');
}
}
sendRequest(ws, requestData) {
const requestId = uuidv4();
return new Promise((resolve, reject) => {
// Set 30-second timeout
const timeout = setTimeout(() => {
this.pendingRequests.delete(requestId);
reject(new Error('Tunnel timeout'));
}, 30000);
// Store pending request
this.pendingRequests.set(requestId, { resolve, reject, timeout });
// Send request to CLI client via WebSocket
ws.send(JSON.stringify({
type: 'http_request',
requestId,
...requestData
}));
});
}
handleResponse(message) {
const { requestId, statusCode, headers, body } = message;
const pending = this.pendingRequests.get(requestId);
if (pending) {
clearTimeout(pending.timeout);
this.pendingRequests.delete(requestId);
pending.resolve({ statusCode, headers, body });
}
}
}
// Usage in Express app
const proxyMiddleware = new TunnelProxyMiddleware();
app.use((req, res) => proxyMiddleware.handle(req, res));WebSocket Proxy (for WS-enabled apps)
Handling WebSocket Upgrade Requests:
const server = require('http').createServer(app);
const wss = new WebSocket.Server({ noServer: true });
server.on('upgrade', async (req, socket, head) => {
const hostname = req.headers.host;
const subdomain = hostname.replace('.local.dev', '');
// Find tunnel connection
const wsConnectionId = await redis.get(`subdomain:${subdomain}`);
if (!wsConnectionId) {
socket.destroy();
return;
}
const tunnelWs = proxyMiddleware.activeConnections.get(wsConnectionId);
if (!tunnelWs || tunnelWs.readyState !== WebSocket.OPEN) {
socket.destroy();
return;
}
// Notify CLI client about WebSocket upgrade
const upgradeId = uuidv4();
tunnelWs.send(JSON.stringify({
type: 'websocket_upgrade',
upgradeId,
path: req.url,
headers: req.headers
}));
// Accept the WebSocket upgrade on gateway side
wss.handleUpgrade(req, socket, head, (clientWs) => {
// Create bidirectional tunnel
// User's WS ←→ Gateway ←→ Tunnel WS ←→ CLI ←→ localhost WS
clientWs.on('message', (data) => {
// Forward to tunnel
tunnelWs.send(JSON.stringify({
type: 'websocket_data',
upgradeId,
data: data.toString('base64')
}));
});
clientWs.on('close', () => {
tunnelWs.send(JSON.stringify({
type: 'websocket_close',
upgradeId
}));
});
// Listen for data from tunnel
const handleTunnelMessage = (msg) => {
const message = JSON.parse(msg);
if (message.upgradeId === upgradeId) {
if (message.type === 'websocket_data') {
clientWs.send(Buffer.from(message.data, 'base64'));
} else if (message.type === 'websocket_close') {
clientWs.close();
}
}
};
tunnelWs.on('message', handleTunnelMessage);
});
});Visual Request Flow Diagrams
Subdomain Request (abc123.local.dev):
┌─────────────┐
│ Browser │ GET https://abc123.local.dev/api/users
└──────┬──────┘
│ 1. DNS Lookup
↓
┌─────────────────┐
│ DNS Registrar │ *.local.dev → CNAME → localdev-gateway.fly.dev
└────────┬────────┘
│ 2. Resolves to Fly.io IP
↓
┌─────────────────────────────────────┐
│ Fly.io Anycast (Global) │ Routes to nearest region
└─────────────┬───────────────────────┘
│ 3. TLS termination (Caddy)
↓
┌─────────────────────────────────────┐
│ Gateway Instance │
│ a) Extract: subdomain = "abc123" │
│ b) Redis: subdomain:abc123 → ws_7 │
│ c) WebSocket send: http_request │
└─────────────┬───────────────────────┘
│ 4. WebSocket message
↓
┌─────────────────────────────────────┐
│ CLI Client (Developer) │
│ a) Receive http_request │
│ b) HTTP to localhost:3000 │
│ c) Get response │
│ d) WebSocket send: http_response │
└─────────────┬───────────────────────┘
│ 5. localhost:3000
↓
┌─────────────────────────────────────┐
│ Local Dev Server (Next.js/etc) │ Returns JSON response
└─────────────────────────────────────┘Custom Domain Request (their-app.com):
┌─────────────┐
│ Browser │ GET https://their-app.com/
└──────┬──────┘
│ 1. DNS Lookup
↓
┌─────────────────┐
│ User's Registrar│ CNAME → localdev-gateway.fly.dev
└────────┬────────┘
│ 2. Resolves to Fly.io IP
↓
┌─────────────────────────────────────┐
│ Fly.io Gateway │
│ a) Caddy: Check SSL cert │
│ - If exists: use it │
│ - If not: ask /caddy/cert-allowed│
│ - Issue Let's Encrypt cert │
│ b) Extract: hostname = "their-app.com"│
│ c) Redis: domain:their-app.com → ws│
│ d) Proxy via WebSocket (same flow) │
└─────────────────────────────────────┘Architecture Comparison: DNS & Proxy
| Feature | Original (Per-Customer) | Simplified (Centralized) |
|---|---|---|
| DNS Servers | N CoreDNS instances | 0 (registrar + Fly.io) |
| DNS Configuration | N configs to manage | 1 wildcard CNAME |
| SSL Certificates | N Caddy instances | 1 Caddy (on-demand certs) |
| Custom Domain Setup | Per-workspace DNS | Centralized validation |
| Proxy Instances | N Node.js proxies | 1 Gateway tier (horizontal) |
| Cost per Customer | $2-5/month | $0 (shared infrastructure) |
| Operational Complexity | HIGH (manage N machines) | LOW (single gateway app) |
| Request Latency | 2 hops | 1 hop |
| Scalability | Complex (N×M regions) | Simple (stateless) |
Key Insight: By centralizing DNS and proxy at the gateway tier, we eliminate per-customer infrastructure overhead while maintaining ALL the same functionality (subdomains, custom domains, automatic SSL). This is the core efficiency gain of the simplified architecture.
Part 2: Business Model Analysis
Revenue Projections - Reality Check
| Metric | PRD Assumption | Industry Reality | Revised Estimate |
|---|---|---|---|
| Conversion Rate | 10% free-to-paid | 2-5% for dev tools | 3-5% |
| Month 12 Customers | 2000 paid | - | 600-1000 paid |
| Month 12 MRR | $88,000 | - | $26,400-44,000 |
| CAC | <$20 | $30-100 for dev tools | $50-75 |
| LTV | >$500 | Churn = 20-30%/year | $200-400 |
Pricing Strategy Issues
Original Pricing (from PRD)
| Tier | Price | Issues |
|---|---|---|
| FREE | $0 | 1hr session, 100MB/day, HTTP-only → TOO restrictive |
| DEVELOPER | $19 | Higher than ngrok Pro ($8) → Not competitive |
| PROFESSIONAL | $49 | Unclear differentiation from Developer |
| TEAM | $149 | Missing key features (SSO, RBAC need dev) |
| ENTERPRISE | $499+ | Too ambitious for MVP |
Competitive Comparison
ngrok Pricing:
- Free: Generous (40 requests/min, HTTPS included)
- Personal ($8/mo): Custom domains, 60 requests/min
- Pro ($20/mo): Reserved domains, multiple tunnels
- Business ($29/user/mo): Teams, SSO
Our Position: Currently priced HIGHER with FEWER features and LESS brand trust.
Revised Pricing Strategy
Focus: "The tunnel service for preview environments and CI/CD"
| Tier | Price | Features | Target Audience |
|---|---|---|---|
| FREE | $0 | • 10 tunnels/month• 24hr sessions• HTTPS included• 5GB bandwidth• Branding banner | Individual developers, testing |
| STARTER | $12/mo | • 50 tunnels/month• Custom subdomains• 50GB bandwidth• No branding• Email support | Freelancers, small projects |
| PRO | $39/mo | • Unlimited tunnels• Custom domains• 500GB bandwidth• API access• Priority support | Professional developers, agencies |
| TEAM | $99/mo | • Everything in Pro• Team workspace (5 users)• Shared tunnels• 2TB bandwidth• Audit logs | Development teams, startups |
Key Changes:
- ✅ Generous free tier to build trust and allow proper evaluation
- ✅ Competitive pricing vs. ngrok ($12 vs $8 is only 50% higher, not 140%)
- ✅ Clear value ladder - each tier has obvious upgrade benefits
- ✅ Delayed enterprise tier - focus on PMF first
Differentiation Strategy
DON'T compete on: Features parity with ngrok (they have 10-year head start)
DO compete on: Specific use case excellence
Target Niche: Preview Environments for CI/CD
Pain Points:
- Vercel/Netlify preview deployments are expensive for non-Next.js apps
- ngrok requires manual CLI invocation (not automated)
- Cloudflare Tunnels require Cloudflare account
- No good solution for "deploy PR to preview URL" automation
Our Solution:
# .github/workflows/preview.yml
- name: Deploy PR Preview
uses: localdev/preview-action@v1
with:
port: 3000
subdomain: pr-${{ github.event.number }}
# Automatically comments PR with URLValue Proposition: "Vercel-style preview URLs for any framework, in your CI/CD"
Part 3: Revised Implementation Plan
Realistic Timeline: 6 Months to Production
Phase 1: Core MVP (Weeks 1-8)
Week 1-2: Infrastructure Setup
- Fly.io account, organization, billing setup
- Domain configuration (local.dev DNS → Fly.io)
- PostgreSQL database deployment
- Redis instance deployment
- Basic monitoring (Fly.io metrics)
Week 3-5: Gateway Implementation
- Express/Fastify server with WS support
- Subdomain routing logic (*.local.dev → gateway)
- TLS termination via Caddy auto-HTTPS
- Basic authentication (API key → JWT)
- Redis session storage (subdomain → ws_connection_id)
- HTTP proxy to WS connection
- Request ID correlation system
Week 6-7: CLI Client v1
- Basic WS connection with authentication
- Request multiplexing (concurrent handling)
- Auto-reconnection on disconnect
- Local port forwarding
- Basic error handling and logging
- Config file support (.localdev.yml)
Week 8: Integration Testing
- End-to-end tunnel testing
- Load testing (100 concurrent requests)
- Connection stability testing (24hr session)
- Error recovery testing
- Security testing (auth bypass attempts)
Phase 2: Free Tier Features (Weeks 9-12)
Week 9-10: Usage Tracking & Limits
- Bandwidth tracking per tunnel
- Request rate limiting (Redis-based)
- Session timeout implementation
- Monthly tunnel limit enforcement
- Usage dashboard (web UI)
Week 11: Web Dashboard v1
- Authentication (email/password)
- API key generation
- Active tunnels view
- Usage statistics
- Account settings
Week 12: Polish & Documentation
- CLI installation docs
- Quick start guide
- API documentation
- Troubleshooting guide
- Security documentation
Phase 3: Monetization (Weeks 13-16)
Week 13-14: Stripe Integration
- Stripe Connect setup
- Subscription management
- Webhook handling (payment success/failure)
- Tier enforcement (check subscription on connection)
- Billing portal integration
Week 15: Premium Features
- Custom subdomain selection
- No-branding mode
- Priority routing queue
- Email support system
Week 16: Beta Launch Prep
- Marketing site (landing page)
- Pricing page
- Documentation site
- Beta sign-up form
- Analytics integration
Phase 4: Advanced Features (Weeks 17-24)
Week 17-18: Custom Domains
- DNS validation flow
- Cloudflare API integration
- Automatic SSL for custom domains
- Domain management UI
Week 19-20: API Development
- REST API for tunnel management
- API key scoping (read/write permissions)
- Webhook support (tunnel events)
- API documentation (OpenAPI)
Week 21-22: Team Features
- Multi-user workspace
- Invitation system
- Role-based permissions (owner/member)
- Shared tunnel management
- Team usage dashboard
Week 23-24: Production Hardening
- Comprehensive monitoring (Grafana/Prometheus)
- Alerting (PagerDuty integration)
- Backup and disaster recovery
- Performance optimization
- Security audit
- Load testing (10K concurrent tunnels)
Week 24: Public Launch
Part 4: Database Schema (Revised)
-- =======================
-- CORE TABLES
-- =======================
-- Users (authentication)
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
email_verified BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- API Keys
CREATE TABLE api_keys (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
key_hash VARCHAR(255) UNIQUE NOT NULL,
scopes TEXT[] DEFAULT ARRAY['tunnel:create', 'tunnel:read'],
last_used_at TIMESTAMP,
created_at TIMESTAMP DEFAULT NOW(),
revoked_at TIMESTAMP
);
-- Subscriptions
CREATE TABLE subscriptions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
tier VARCHAR(50) NOT NULL DEFAULT 'free',
stripe_customer_id VARCHAR(255),
stripe_subscription_id VARCHAR(255),
status VARCHAR(50) DEFAULT 'active',
current_period_start TIMESTAMP,
current_period_end TIMESTAMP,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- =======================
-- TUNNEL MANAGEMENT
-- =======================
-- Active Tunnels (volatile, cleared on restart)
CREATE TABLE tunnels (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
subdomain VARCHAR(255) UNIQUE NOT NULL,
ws_connection_id VARCHAR(255) UNIQUE NOT NULL,
local_port INTEGER NOT NULL,
gateway_instance VARCHAR(255), -- Which gateway instance owns this
status VARCHAR(50) DEFAULT 'active',
created_at TIMESTAMP DEFAULT NOW(),
last_heartbeat TIMESTAMP DEFAULT NOW(),
closed_at TIMESTAMP
);
-- Tunnel Sessions (historical record)
CREATE TABLE tunnel_sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id),
tunnel_id UUID,
subdomain VARCHAR(255),
duration_seconds INTEGER,
bytes_transferred BIGINT DEFAULT 0,
requests_count INTEGER DEFAULT 0,
started_at TIMESTAMP DEFAULT NOW(),
ended_at TIMESTAMP
);
-- Custom Domains
CREATE TABLE custom_domains (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
domain VARCHAR(255) UNIQUE NOT NULL,
dns_verified BOOLEAN DEFAULT FALSE,
ssl_enabled BOOLEAN DEFAULT FALSE,
ssl_cert_id VARCHAR(255),
validation_token VARCHAR(255),
created_at TIMESTAMP DEFAULT NOW(),
verified_at TIMESTAMP
);
-- =======================
-- USAGE & BILLING
-- =======================
-- Usage Metrics (aggregated daily)
CREATE TABLE usage_metrics (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id),
date DATE NOT NULL,
metric_type VARCHAR(50) NOT NULL, -- 'tunnels_created', 'bandwidth', 'requests'
value BIGINT NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE(user_id, date, metric_type)
);
-- =======================
-- TEAMS (for Team tier)
-- =======================
-- Teams
CREATE TABLE teams (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
owner_id UUID REFERENCES users(id),
subscription_id UUID REFERENCES subscriptions(id),
created_at TIMESTAMP DEFAULT NOW()
);
-- Team Members
CREATE TABLE team_members (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
team_id UUID REFERENCES teams(id) ON DELETE CASCADE,
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
role VARCHAR(50) DEFAULT 'member', -- 'owner', 'admin', 'member'
joined_at TIMESTAMP DEFAULT NOW(),
UNIQUE(team_id, user_id)
);
-- =======================
-- INDEXES
-- =======================
CREATE INDEX idx_tunnels_user ON tunnels(user_id);
CREATE INDEX idx_tunnels_subdomain ON tunnels(subdomain);
CREATE INDEX idx_tunnels_ws_conn ON tunnels(ws_connection_id);
CREATE INDEX idx_tunnels_status ON tunnels(status) WHERE status = 'active';
CREATE INDEX idx_sessions_user_date ON tunnel_sessions(user_id, started_at);
CREATE INDEX idx_usage_user_date ON usage_metrics(user_id, date);
CREATE INDEX idx_api_keys_user ON api_keys(user_id) WHERE revoked_at IS NULL;
CREATE INDEX idx_subscriptions_user ON subscriptions(user_id);Part 5: Risk Assessment & Mitigation
Technical Risks
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| WebSocket stability issues | HIGH | CRITICAL | Comprehensive reconnection logic, heartbeat monitoring, extensive testing |
| Scaling bottlenecks | MEDIUM | HIGH | Redis caching, connection pooling, stateless design, load testing |
| SSL certificate failures | LOW | HIGH | Caddy auto-renewal, monitoring, manual fallback process |
| Database performance | MEDIUM | MEDIUM | Proper indexing, query optimization, read replicas if needed |
| Security vulnerabilities | MEDIUM | CRITICAL | Security audit, penetration testing, bug bounty program |
Business Risks
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Low conversion rate (<3%) | MEDIUM | HIGH | Generous free tier, excellent onboarding, clear upgrade path |
| High churn rate (>30%) | MEDIUM | HIGH | Product quality, customer support, engagement campaigns |
| ngrok/Cloudflare competition | HIGH | MEDIUM | Niche focus (CI/CD previews), community building, unique features |
| Abuse of free tier | HIGH | MEDIUM | Strict rate limiting, abuse detection, aggressive banning |
| Slow customer acquisition | MEDIUM | HIGH | Content marketing, community engagement, open-source strategy |
Operational Risks
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Infrastructure cost overruns | MEDIUM | HIGH | Conservative free tier, usage monitoring, cost alerts |
| Extended downtime | LOW | CRITICAL | Multi-region deployment, automated failover, incident response plan |
| Support overwhelm | MEDIUM | MEDIUM | Self-service docs, community forum, tiered support |
| Compliance issues (GDPR) | LOW | HIGH | Privacy policy, data processing agreement, user data controls |
Part 6: Go-to-Market Strategy
Phase 1: Stealth Beta (Month 1-2)
Goal: Validate core product with 50-100 developers
Tactics:
- Personal network outreach (founders, friends, colleagues)
- Beta landing page with waitlist
- Private Discord community for feedback
- Weekly feedback sessions
- Rapid iteration based on usage patterns
Success Metrics:
- 50+ active beta users
-
30% weekly active rate
- Net Promoter Score >40
- <5 critical bugs
Phase 2: Public Launch (Month 3-4)
Goal: Acquire first 1,000 users (50-100 paid conversions)
Channels:
- Product Hunt: Launch with special early-bird pricing (50% off first 3 months)
- Hacker News: "Show HN" post with technical deep-dive
- Dev.to: Tutorial series on preview environments
- Reddit: r/webdev, r/javascript, r/devops (not promotional, helpful content)
- Twitter: Technical threads, product updates, feature highlights
Content Marketing:
- "Building Preview Environments with GitHub Actions"
- "Why We Built Yet Another Tunnel Service (and Why It's Different)"
- "The Complete Guide to Secure Tunneling for Local Development"
- "Comparing ngrok, Cloudflare Tunnels, and local.dev"
Success Metrics:
- 1,000 signups
- 3-5% conversion to paid
- $1,500-3,000 MRR
- <$50 CAC
Phase 3: Growth (Month 5-6)
Goal: Scale to 5,000 users (150-250 paid)
Channels:
- GitHub Actions Marketplace: List preview environment action
- Integrations: GitLab CI, CircleCI, Jenkins plugins
- Partnerships: Framework communities (Next.js, Remix, SvelteKit)
- Affiliate Program: 30% recurring commission for referrals
- YouTube: Screencasts, tutorials, use case demos
Success Metrics:
- 5,000 total users
- $7,500-12,500 MRR
- 20% MoM growth
- <$40 CAC
Positioning Statement
For development teams building web applications Who need reliable preview environments for pull requests and staging local.dev is a tunneling service That integrates seamlessly with your CI/CD pipeline Unlike ngrok or Cloudflare Tunnels local.dev provides automated preview URLs with zero manual configuration
Key Messages
- Developer Experience: "From PR to preview URL in 60 seconds"
- Reliability: "99.9% uptime backed by Fly.io's global network"
- Integration: "Works with GitHub, GitLab, and every CI/CD platform"
- Pricing: "Pay for what you use, not what you might need"
- Simplicity: "No complex setup, no account required for free tier"
Part 7: Success Metrics & KPIs
Product Metrics
| Metric | Target | Measurement |
|---|---|---|
| Tunnel Creation Success Rate | >99% | tunnels_created / attempts |
| Average Tunnel Duration | >4 hours | AVG(closed_at - created_at) |
| Requests per Tunnel | >100 | AVG(requests_count) per session |
| Connection Stability | <1% drop rate | disconnections / total_connections |
| p95 Latency | <50ms | Gateway processing time |
Business Metrics
| Metric | Month 1 | Month 3 | Month 6 |
|---|---|---|---|
| Total Users | 100 | 1,000 | 5,000 |
| Paid Users | 5-10 | 30-50 | 150-250 |
| MRR | $200-400 | $1,500-2,500 | $7,500-12,500 |
| Conversion Rate | 5-10% | 3-5% | 3-5% |
| Churn Rate | N/A | <5%/mo | <3%/mo |
| CAC | <$20 | <$50 | <$40 |
Infrastructure Metrics
| Metric | Target | Alert Threshold |
|---|---|---|
| Gateway Uptime | 99.9% | <99.5% |
| Database Query p95 | <10ms | >50ms |
| Redis Hit Rate | >95% | <90% |
| Monthly Cost | <30% of revenue | >50% |
| CPU Usage | <70% avg | >85% |
Part 8: Critical Success Factors
Technical Excellence
-
Reliability First
- Multi-region deployment from day 1
- Automated health checks and failover
- Incident response playbook
- 99.9% uptime SLO
-
Performance Obsession
- <50ms p95 latency target
- Regular load testing (simulate 10K tunnels)
- Performance budgets and monitoring
- CDN for static assets
-
Developer Experience
- CLI install in 30 seconds
- Tunnel running in 60 seconds
- Clear error messages
- Comprehensive documentation
Product Differentiation
-
CI/CD Integration
- GitHub Actions integration (primary)
- GitLab CI/CD support
- Generic webhook API
- Automated PR comments with URLs
-
Preview Environment Focus
- Subdomain per PR number
- Automatic cleanup after merge
- Team collaboration features
- Environment variable injection
-
Developer Community
- Active Discord community
- Open-source components
- Public roadmap
- Transparent communication
Business Execution
-
Realistic Expectations
- 6-month timeline to production
- 3-5% conversion rate goal
- Conservative revenue projections
- Sufficient runway (12+ months)
-
Customer Success
- Excellent onboarding flow
- Proactive support
- Usage monitoring and outreach
- Win-back campaigns for churned users
-
Cost Control
- Usage-based infrastructure
- Aggressive abuse prevention
- Regular cost optimization
- Pricing that covers costs + 70% margin
Conclusion & Recommendations
Summary of Required Changes
| Area | PRD Approach | Recommended Approach |
|---|---|---|
| Architecture | Per-customer Fly Machines | Stateless gateway + Redis routing |
| Free Tier | 1hr, 100MB, HTTP-only | 24hr, 5GB, HTTPS included |
| Pricing | $19/$49/$149/$499 | $12/$39/$99 (delay enterprise) |
| Timeline | 12 weeks to production | 24 weeks to production |
| Positioning | "ngrok competitor" | "CI/CD preview environments" |
| Conversion Goal | 10% | 3-5% |
GO / NO-GO Decision Criteria
✅ PROCEED IF:
- 6-month timeline acceptable
- $50K+ development budget available
- Founder has technical capability to build reliable WebSocket infrastructure
- Willingness to focus on niche (CI/CD) rather than broad competition
- Realistic understanding of 3-5% conversion rates
🛑 DO NOT PROCEED IF:
- Need immediate revenue (this is 6+ month journey)
- Cannot commit to operational excellence (99.9% uptime)
- Unwilling to compete against ngrok/Cloudflare
- Expecting 10%+ conversion rates
- Cannot allocate time for customer support and community building
Next Steps
If decision is to proceed:
- Week 1: Validate revised architecture with Fly.io infrastructure prototype
- Week 2: Build basic WebSocket tunnel MVP (no database, hardcoded routing)
- Week 3: Test with 5-10 friendly developers, gather feedback
- Week 4: Decision point - continue or pivot based on early feedback
Final Verdict: The opportunity exists, but execution must be significantly different from the original PRD. Focus on technical excellence, realistic business model, and clear niche positioning.
Document Version: 1.0 Last Updated: November 19, 2025 Next Review: After MVP prototype completion