Architecture
rs-tunnel is designed as a self-contained monorepo with clear separation between user-facing CLI, backend API, and shared contracts. This page covers the project structure, technology choices, component interactions, and security model.Monorepo Structure
The project uses a pnpm workspace with the following layout:apps/api
The backend API is the source of truth for identity, authorization, Cloudflare tunnel lifecycle, and DNS management. Key responsibilities:- Slack OAuth authentication flow
- JWT access token + refresh token issuance
- Cloudflare tunnel creation/deletion
- DNS record provisioning
- Heartbeat tracking and lease management
- Tunnel quota enforcement
- Audit logging
- Background cleanup worker
- Runtime: Node.js 20+ with TypeScript
- Web framework: Fastify 5.x
- Database: Postgres with Drizzle ORM
- Validation: Zod schemas from
@ripeseed/shared - Auth: JWT (jsonwebtoken), Slack OpenID Connect
- Infrastructure: Cloudflare API for tunnels + DNS
apps/cli
The user-facing CLI provides commands for authentication and tunnel management. Commands:login- Slack OAuth with PKCE flowup- Start tunnel with ngrok-style dashboardlist- Show active tunnelsstop- Stop tunnel by ID or hostnamelogout- Revoke tokensdoctor- Connectivity diagnostics
- Runtime: Node.js 20+ with TypeScript
- CLI framework: Commander 13.x
- HTTP client: Native fetch
- Secure storage: keytar (optional, file fallback)
- Validation: Zod schemas from
@ripeseed/shared - Cloudflare tunnel: Spawns
cloudflaredprocess
- OAuth PKCE flow with local callback server
- Token refresh logic
- Heartbeat loop every 20 seconds
- Local reverse proxy for request telemetry
- Real-time dashboard rendering
--domainflag for self-hosted API configuration
packages/shared
Provides type-safe contracts between API and CLI using Zod schemas. Exports:- Single source of truth for API contracts
- Compile-time type safety
- Runtime validation with Zod
- Prevents API/CLI drift
packages/config
Shared configuration for:- ESLint rules
- Prettier formatting
- TypeScript
tsconfig.jsonbase
All packages use ESM with
.js import suffixes, strict TypeScript mode, and consistent code style.Database Schema
The API uses Postgres with the following core tables:users
Stores authenticated users:tunnels
Tracks tunnel lifecycle:tunnel_leases
Tracks heartbeat state:cleanup_jobs
Queues tunnels for deferred cleanup:Additional tables
- oauth_sessions: Tracks OAuth state and login codes
- refresh_tokens: Stores hashed refresh tokens
- audit_logs: Records tunnel.created and tunnel.stopped events
- tunnel_metrics: Historical connection/request metrics
- tunnel_requests: Individual HTTP request logs
- tunnel_live_metrics: Latest metrics for active tunnels
See
apps/api/src/db/schema.ts for the complete Drizzle schema definition.Component Interaction
Here’s how the components work together during common operations:Tunnel Creation Flow
API validates and enforces quota
- Validates JWT and extracts
userId - Checks active tunnel count <
MAX_ACTIVE_TUNNELS - Validates or generates slug
- Ensures slug is not already in use
API updates tunnel to active
- Update tunnel with
cfTunnelId,cfDnsRecordId, statusactive - Create initial lease entry
- Log
tunnel.createdaudit event
Heartbeat Flow
API validates tunnel ownership
- Verify JWT userId matches tunnel userId
- Ensure tunnel is in
activeorstoppingstate
Stale Tunnel Cleanup
Cleanup jobs use exponential backoff: 30s, 60s, 120s, 240s, etc. Jobs are retried indefinitely until successful.
Security Model
Principle: Least Privilege
The security model is designed around strict separation of concerns:API Runtime
Has access to:
CLOUDFLARE_API_TOKENSLACK_CLIENT_SECRETJWT_SECRETREFRESH_TOKEN_SECRET- Database credentials
- Provision infrastructure
- Issue time-limited credentials
- Enforce access policies
CLI Runtime
Has access to:
- User’s JWT access token (15 min TTL)
- User’s refresh token (30 day TTL)
- Tunnel-specific cloudflared token
- Create tunnels for other users
- Access Cloudflare API directly
- Bypass email/Slack validation
Authentication & Authorization
Authentication flow:- CLI initiates OAuth with PKCE challenge
- User authenticates via Slack OpenID Connect
- API validates:
- Email ends with
ALLOWED_EMAIL_DOMAIN - Slack team ID matches
ALLOWED_SLACK_TEAM_ID
- Email ends with
- API issues JWT access token + refresh token
- CLI uses access token for all API requests
- Access tokens: 15-minute TTL, signed with
JWT_SECRET - Refresh tokens: 30-day TTL, hashed in database, signed with
REFRESH_TOKEN_SECRET - CLI automatically refreshes expired access tokens
- Tokens revoked on logout
- Tunnel-specific: Only work for the assigned tunnel ID
- Short-lived: Issued per tunnel session
- Never logged or exposed in API responses to unrelated users
Network Security
- TLS everywhere: All tunnel traffic goes through Cloudflare’s edge with automatic SSL
- No exposed localhost: CLI’s local proxy is bound to
127.0.0.1only - Rate limiting: Fastify rate-limit plugin on all API routes
- CORS: Configured for CLI and browser OAuth flows only
Cloudflare Token Scopes
TheCLOUDFLARE_API_TOKEN should have minimum required permissions:
Configuration Management
API Configuration
Loaded from environment variables (seeapps/api/src/config/env.ts):
CLI Configuration
Stores minimal state in~/.rs-tunnel/:
--domainflag (saves to config.json)RS_TUNNEL_API_URLenvironment variable- Saved value in
config.json - Interactive prompt (first run)
CLI prefers keytar for secure token storage when available. Falls back to file storage with restrictive permissions (0600).
Deployment Architecture
Typical Production Setup
Scaling Considerations
- API: Stateless design allows horizontal scaling behind a load balancer
- Database: Connection pooling via Drizzle, indexes on hot paths
- Reaper worker: Single instance recommended to avoid duplicate cleanup jobs
- Cloudflare tunnels: No practical limit on concurrent tunnels
For high-availability deployments, run multiple API instances with a shared Postgres database and a single reaper worker instance.
Technology Decisions
Why Fastify?
- Native async/await support
- Plugin ecosystem (CORS, rate limiting)
- Excellent TypeScript support
- High performance
Why Drizzle ORM?
- Type-safe query builder
- Zero runtime overhead
- Migration system
- Raw SQL escape hatch when needed
Why Postgres?
- JSONB for flexible audit logs
- Reliable transactions for tunnel lifecycle
- Excellent indexing for lease expiry queries
- Battle-tested reliability
Why Zod?
- Runtime validation + TypeScript types from single source
- Composable schemas
- Clear error messages
- Works in both Node.js and browser
Why Commander?
- De facto standard for Node.js CLIs
- Subcommand support
- Automatic help generation
- Option parsing
Next Steps
How It Works
Deep dive into tunnel creation, heartbeats, and cleanup mechanisms
Installation
Set up rs-tunnel for local development or production
API Reference
Complete REST API documentation
Database Schema
Detailed schema reference and migration guide

