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

AspectOriginal DesignSimplified Design
Per-customer cost$2-5/month (stopped machine storage)$0 (stateless)
ScalabilityComplex (N machines × M regions)Simple (stateless horizontal)
Operational complexityHigh (Machines API orchestration)Low (standard web app)
Latency2 hops (Gateway → Machine → localhost)1 hop (Gateway → localhost)
Infrastructure duplicationYes (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 requester

WebSocket 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 → Gateway

Gateway 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 → Tunnel

Implementation:

// 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

FeatureOriginal (Per-Customer)Simplified (Centralized)
DNS ServersN CoreDNS instances0 (registrar + Fly.io)
DNS ConfigurationN configs to manage1 wildcard CNAME
SSL CertificatesN Caddy instances1 Caddy (on-demand certs)
Custom Domain SetupPer-workspace DNSCentralized validation
Proxy InstancesN Node.js proxies1 Gateway tier (horizontal)
Cost per Customer$2-5/month$0 (shared infrastructure)
Operational ComplexityHIGH (manage N machines)LOW (single gateway app)
Request Latency2 hops1 hop
ScalabilityComplex (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

MetricPRD AssumptionIndustry RealityRevised Estimate
Conversion Rate10% free-to-paid2-5% for dev tools3-5%
Month 12 Customers2000 paid-600-1000 paid
Month 12 MRR$88,000-$26,400-44,000
CAC<$20$30-100 for dev tools$50-75
LTV>$500Churn = 20-30%/year$200-400

Pricing Strategy Issues

Original Pricing (from PRD)

TierPriceIssues
FREE$01hr session, 100MB/day, HTTP-only → TOO restrictive
DEVELOPER$19Higher than ngrok Pro ($8) → Not competitive
PROFESSIONAL$49Unclear differentiation from Developer
TEAM$149Missing 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"

TierPriceFeaturesTarget Audience
FREE$0• 10 tunnels/month• 24hr sessions• HTTPS included• 5GB bandwidth• Branding bannerIndividual developers, testing
STARTER$12/mo• 50 tunnels/month• Custom subdomains• 50GB bandwidth• No branding• Email supportFreelancers, small projects
PRO$39/mo• Unlimited tunnels• Custom domains• 500GB bandwidth• API access• Priority supportProfessional developers, agencies
TEAM$99/mo• Everything in Pro• Team workspace (5 users)• Shared tunnels• 2TB bandwidth• Audit logsDevelopment teams, startups

Key Changes:

  1. Generous free tier to build trust and allow proper evaluation
  2. Competitive pricing vs. ngrok ($12 vs $8 is only 50% higher, not 140%)
  3. Clear value ladder - each tier has obvious upgrade benefits
  4. 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 URL

Value 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

RiskLikelihoodImpactMitigation
WebSocket stability issuesHIGHCRITICALComprehensive reconnection logic, heartbeat monitoring, extensive testing
Scaling bottlenecksMEDIUMHIGHRedis caching, connection pooling, stateless design, load testing
SSL certificate failuresLOWHIGHCaddy auto-renewal, monitoring, manual fallback process
Database performanceMEDIUMMEDIUMProper indexing, query optimization, read replicas if needed
Security vulnerabilitiesMEDIUMCRITICALSecurity audit, penetration testing, bug bounty program

Business Risks

RiskLikelihoodImpactMitigation
Low conversion rate (<3%)MEDIUMHIGHGenerous free tier, excellent onboarding, clear upgrade path
High churn rate (>30%)MEDIUMHIGHProduct quality, customer support, engagement campaigns
ngrok/Cloudflare competitionHIGHMEDIUMNiche focus (CI/CD previews), community building, unique features
Abuse of free tierHIGHMEDIUMStrict rate limiting, abuse detection, aggressive banning
Slow customer acquisitionMEDIUMHIGHContent marketing, community engagement, open-source strategy

Operational Risks

RiskLikelihoodImpactMitigation
Infrastructure cost overrunsMEDIUMHIGHConservative free tier, usage monitoring, cost alerts
Extended downtimeLOWCRITICALMulti-region deployment, automated failover, incident response plan
Support overwhelmMEDIUMMEDIUMSelf-service docs, community forum, tiered support
Compliance issues (GDPR)LOWHIGHPrivacy 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:

  1. Personal network outreach (founders, friends, colleagues)
  2. Beta landing page with waitlist
  3. Private Discord community for feedback
  4. Weekly feedback sessions
  5. 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:

  1. Product Hunt: Launch with special early-bird pricing (50% off first 3 months)
  2. Hacker News: "Show HN" post with technical deep-dive
  3. Dev.to: Tutorial series on preview environments
  4. Reddit: r/webdev, r/javascript, r/devops (not promotional, helpful content)
  5. 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:

  1. GitHub Actions Marketplace: List preview environment action
  2. Integrations: GitLab CI, CircleCI, Jenkins plugins
  3. Partnerships: Framework communities (Next.js, Remix, SvelteKit)
  4. Affiliate Program: 30% recurring commission for referrals
  5. 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

  1. Developer Experience: "From PR to preview URL in 60 seconds"
  2. Reliability: "99.9% uptime backed by Fly.io's global network"
  3. Integration: "Works with GitHub, GitLab, and every CI/CD platform"
  4. Pricing: "Pay for what you use, not what you might need"
  5. Simplicity: "No complex setup, no account required for free tier"

Part 7: Success Metrics & KPIs

Product Metrics

MetricTargetMeasurement
Tunnel Creation Success Rate>99%tunnels_created / attempts
Average Tunnel Duration>4 hoursAVG(closed_at - created_at)
Requests per Tunnel>100AVG(requests_count) per session
Connection Stability<1% drop ratedisconnections / total_connections
p95 Latency<50msGateway processing time

Business Metrics

MetricMonth 1Month 3Month 6
Total Users1001,0005,000
Paid Users5-1030-50150-250
MRR$200-400$1,500-2,500$7,500-12,500
Conversion Rate5-10%3-5%3-5%
Churn RateN/A<5%/mo<3%/mo
CAC<$20<$50<$40

Infrastructure Metrics

MetricTargetAlert Threshold
Gateway Uptime99.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

  1. Reliability First

    • Multi-region deployment from day 1
    • Automated health checks and failover
    • Incident response playbook
    • 99.9% uptime SLO
  2. Performance Obsession

    • <50ms p95 latency target
    • Regular load testing (simulate 10K tunnels)
    • Performance budgets and monitoring
    • CDN for static assets
  3. Developer Experience

    • CLI install in 30 seconds
    • Tunnel running in 60 seconds
    • Clear error messages
    • Comprehensive documentation

Product Differentiation

  1. CI/CD Integration

    • GitHub Actions integration (primary)
    • GitLab CI/CD support
    • Generic webhook API
    • Automated PR comments with URLs
  2. Preview Environment Focus

    • Subdomain per PR number
    • Automatic cleanup after merge
    • Team collaboration features
    • Environment variable injection
  3. Developer Community

    • Active Discord community
    • Open-source components
    • Public roadmap
    • Transparent communication

Business Execution

  1. Realistic Expectations

    • 6-month timeline to production
    • 3-5% conversion rate goal
    • Conservative revenue projections
    • Sufficient runway (12+ months)
  2. Customer Success

    • Excellent onboarding flow
    • Proactive support
    • Usage monitoring and outreach
    • Win-back campaigns for churned users
  3. Cost Control

    • Usage-based infrastructure
    • Aggressive abuse prevention
    • Regular cost optimization
    • Pricing that covers costs + 70% margin

Conclusion & Recommendations

Summary of Required Changes

AreaPRD ApproachRecommended Approach
ArchitecturePer-customer Fly MachinesStateless gateway + Redis routing
Free Tier1hr, 100MB, HTTP-only24hr, 5GB, HTTPS included
Pricing$19/$49/$149/$499$12/$39/$99 (delay enterprise)
Timeline12 weeks to production24 weeks to production
Positioning"ngrok competitor""CI/CD preview environments"
Conversion Goal10%3-5%

GO / NO-GO Decision Criteria

PROCEED IF:

  1. 6-month timeline acceptable
  2. $50K+ development budget available
  3. Founder has technical capability to build reliable WebSocket infrastructure
  4. Willingness to focus on niche (CI/CD) rather than broad competition
  5. Realistic understanding of 3-5% conversion rates

🛑 DO NOT PROCEED IF:

  1. Need immediate revenue (this is 6+ month journey)
  2. Cannot commit to operational excellence (99.9% uptime)
  3. Unwilling to compete against ngrok/Cloudflare
  4. Expecting 10%+ conversion rates
  5. Cannot allocate time for customer support and community building

Next Steps

If decision is to proceed:

  1. Week 1: Validate revised architecture with Fly.io infrastructure prototype
  2. Week 2: Build basic WebSocket tunnel MVP (no database, hardcoded routing)
  3. Week 3: Test with 5-10 friendly developers, gather feedback
  4. 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

On this page

local.dev Tunnel Service - Design Analysis & Revised PlanExecutive SummaryKey FindingsPart 1: Architectural AnalysisCurrent PRD ArchitectureCritical Flaws1. Per-Customer Machine Model (BLOCKER)2. No Request Multiplexing (HIGH PRIORITY)3. Mixed Security Model (SECURITY RISK)4. No Scalability Strategy (FUTURE BLOCKER)Proposed Simplified ArchitectureArchitecture BenefitsTechnical Implementation DetailsGateway Request FlowWebSocket Message ProtocolRequest Multiplexing StrategyDNS and Proxy ImplementationDNS Architecture - Centralized vs. Per-CustomerDNS ConfigurationCustom Domain SupportAutomatic SSL with CaddyHTTP/HTTPS Proxy ImplementationWebSocket Proxy (for WS-enabled apps)Visual Request Flow DiagramsArchitecture Comparison: DNS & ProxyPart 2: Business Model AnalysisRevenue Projections - Reality CheckPricing Strategy IssuesOriginal Pricing (from PRD)Competitive ComparisonRevised Pricing StrategyDifferentiation StrategyTarget Niche: Preview Environments for CI/CDPart 3: Revised Implementation PlanRealistic Timeline: 6 Months to ProductionPhase 1: Core MVP (Weeks 1-8)Phase 2: Free Tier Features (Weeks 9-12)Phase 3: Monetization (Weeks 13-16)Phase 4: Advanced Features (Weeks 17-24)Part 4: Database Schema (Revised)Part 5: Risk Assessment & MitigationTechnical RisksBusiness RisksOperational RisksPart 6: Go-to-Market StrategyPhase 1: Stealth Beta (Month 1-2)Phase 2: Public Launch (Month 3-4)Phase 3: Growth (Month 5-6)Positioning StatementKey MessagesPart 7: Success Metrics & KPIsProduct MetricsBusiness MetricsInfrastructure MetricsPart 8: Critical Success FactorsTechnical ExcellenceProduct DifferentiationBusiness ExecutionConclusion & RecommendationsSummary of Required ChangesGO / NO-GO Decision CriteriaNext Steps