Migration System Overview
Drizzle provides a type-safe, SQL-first migration system:- Schema definition: TypeScript schema in
apps/api/src/db/schema.ts - Migration files: SQL files in
apps/api/drizzle/ - Migration runner: Node.js script in
apps/api/src/db/migrate.ts - Metadata: JSON journal in
apps/api/drizzle/meta/
Migrations are idempotent and safe to run multiple times. Drizzle tracks applied migrations automatically.
Database Schema Structure
The rs-tunnel database consists of 9 tables:Core Tables
users
User accounts from Slack OAuth.Key fields:
id(UUID primary key)email(unique)slack_user_idslack_team_idstatus(active/inactive)
tunnels
Tunnel lifecycle and configuration.Key fields:
id(UUID)user_id(foreign key)slug(tunnel subdomain)hostname(full domain)cf_tunnel_id(Cloudflare tunnel ID)cf_dns_record_id(DNS record ID)status(creating/active/stopped)
tunnel_leases
Heartbeat-based tunnel leases.Key fields:
tunnel_id(foreign key, unique)last_heartbeat_atexpires_at
oauth_sessions
OAuth flow state tracking.Key fields:
state(OAuth state parameter)code_challenge(PKCE challenge)login_code(6-digit code)status(pending/authorized/consumed)
Authentication Tables
refresh_tokens: Long-lived refresh tokens (30 days default)audit_logs: User action audit trail
Metrics Tables
tunnel_live_metrics: Real-time tunnel metrics (ttl, opn, latency, etc.)tunnel_metrics: Historical metrics snapshotstunnel_requests: HTTP request logs (method, path, status, duration)
Cleanup Tables
cleanup_jobs: Background cleanup tasks for stale tunnels
Running Migrations
Prerequisites
- PostgreSQL is running and accessible
DATABASE_URLenvironment variable is set- Database exists (created automatically by PostgreSQL Docker container)
Run Migrations
Set DATABASE_URL
Ensure
.env contains:Use port 23432 when connecting from host machine (Docker exposes it on this port).
Migration Files
All migration SQL files are inapps/api/drizzle/:
Migration Runner
The migration script is inapps/api/src/db/migrate.ts:1-20:
migrate.ts
- Resolves migration folder path:
apps/api/drizzle/ - Calls
migrate()from Drizzle ORM - Drizzle reads
drizzle/meta/_journal.jsonto determine which migrations to apply - Executes unapplied SQL files in order
- Updates migration tracking table
- Closes database connection
Drizzle creates a
__drizzle_migrations table to track which migrations have been applied.Generating New Migrations
When you modifyschema.ts, generate a new migration:
Database Indexes
rs-tunnel uses several indexes for query performance:Unique Indexes
tunnels_hostname_idx: Ensures only one active tunnel per hostname (partial index:WHERE status != 'stopped')oauth_sessions_state_idx: Fast OAuth state lookupoauth_sessions_login_code_idx: Fast login code validationrefresh_tokens_token_hash_idx: Fast refresh token validationtunnel_leases_tunnel_id_idx: One lease per tunnel
Performance Indexes
tunnels_user_status_idx: User’s active tunnels lookup (schema.ts:88)tunnels_slug_status_idx: Slug availability checks (schema.ts:89)tunnel_leases_expires_at_idx: Expired lease cleanup (schema.ts:105)tunnel_live_metrics_received_at_idx: Recent metrics queries (schema.ts:129)tunnel_metrics_tunnel_captured_at_idx: Historical metrics (schema.ts:152)tunnel_requests_tunnel_ingested_at_idx: Request log queries (schema.ts:174)
Foreign Key Relationships
Cascade delete rules:- Deleting a user cascades to:
refresh_tokenstunnels(and all child tables)
- Deleting a tunnel cascades to:
tunnel_leasestunnel_live_metricstunnel_metricstunnel_requestscleanup_jobs
Troubleshooting
Error: Can't find meta/_journal.json
Error: Can't find meta/_journal.json
Cause: Migration metadata is missing.Solution:
- Verify
apps/api/drizzle/meta/_journal.jsonexists in the repository - If missing, regenerate migrations:
- This file should be committed to version control
Error: client password must be a string
Error: client password must be a string
Cause:
DATABASE_URL is not set or invalid.Solution:- Check
.envfile exists and is loaded - Verify format:
postgres://user:password@host:port/database - Run migration from API directory:
Error: database 'rs_tunnel' does not exist
Error: database 'rs_tunnel' does not exist
Cause: Database not created.Solution:
Create the database manually:Or use the Docker environment variable
POSTGRES_DB=rs_tunnel (already set in docker-compose.yml).Migration hangs or times out
Migration hangs or times out
Cause: Database not ready or network issue.Solution:
- Check PostgreSQL is running:
- Test connection:
- Check PostgreSQL logs:
Error: relation 'users' already exists
Error: relation 'users' already exists
Cause: Migration already applied, or manual schema creation.Solution:
This is usually safe to ignore. Migrations use
CREATE TABLE IF NOT EXISTS.If you need to start fresh:Schema Validation
Drizzle provides type-safe schema validation at runtime. The schema is defined in TypeScript and enforced by Zod in the API routes. Example validation fromenv.ts:16-52:
Production Best Practices
Migration Checklist
-
Test in local environment
-
Backup production database
-
Apply to staging
-
Test application functionality
- Verify API starts successfully
- Test user authentication
- Create/stop test tunnels
-
Apply to production during maintenance window
-
Monitor application logs
Rollback Strategy
Drizzle doesn’t have built-in rollback. For safety:- Always backup before migrations
- Write reversible migrations when possible
- Test rollback in staging:
Next Steps
Prerequisites
System requirements and account setup
Environment Variables
Complete configuration reference

