Skip to main content

Overview

TrailBase uses a migration system based on SQL files to manage database schema changes. Migrations are automatically discovered and applied on server startup, ensuring your database schema stays in sync with your application.

Migration System

Migration Types

TrailBase supports two types of migrations, indicated by filename prefix:
string
Versioned migrations that run exactly once. Cannot be modified after being applied.Format: V<version>__<name>.sql
string
Undo-safe migrations that use timestamps as versions. Can be created dynamically.Format: U<timestamp>__<name>.sql
TrailBase’s CLI generates U (undo-safe) migrations by default using Unix timestamps, which ensures unique ordering and prevents version conflicts.

Migration Locations

Migrations are discovered from multiple locations:
  1. Embedded migrations - Built into the TrailBase binary
  2. User migrations - Located in <data-dir>/migrations/<database>/
Directory structure:
Place database-specific migrations in migrations/<database>/ subdirectories. The legacy approach of placing files directly in migrations/ still works but is deprecated.

Creating Migrations

Create a New Migration

Use the trail migration command to create a new migration file:
string
Optional descriptive suffix for the migration filename. Defaults to “update” if not provided.
string
Target database name. Defaults to “main” if not provided.

Migration File Format

Generated migration files contain a comment header:
Add your SQL statements below this comment:

Applying Migrations

Migrations are automatically discovered and applied when TrailBase starts:
Startup logs:

Migration Order

Migrations are applied in this order:
  1. Embedded base migrations - Core system tables
  2. Embedded main migrations - Built-in functionality
  3. User migrations - Your custom migrations from migrations/<db>/
  4. Legacy migrations - Direct SQL files in migrations/ (deprecated)
Within each category, migrations are sorted by version/timestamp.

Migration State

TrailBase tracks applied migrations in the _schema_history table:
Schema:
Do not manually edit the _schema_history table unless you know what you’re doing. Manual changes can lead to inconsistent state.

Migration Workflows

Adding a New Table

Modifying Existing Schema

SQLite has limited ALTER TABLE support. To add a column:
For complex changes, use the recreate pattern:

Working with Multiple Databases

Data Migrations

Migration Best Practices

Naming Conventions

Use descriptive suffixes that explain what the migration does:

Writing Safe Migrations

DO:
  • Test migrations on a copy of production data
  • Use transactions (implicit in SQLite)
  • Add indexes for foreign key columns
  • Include IF NOT EXISTS for idempotency when possible
  • Use COALESCE() for default values when migrating data
DON’T:
  • Modify migrations after they’ve been applied
  • Delete migration files that have been applied
  • Use database-specific features (TrailBase uses SQLite)
  • Assume data format without validation
  • Drop tables or columns without backing up data first

Idempotent Migrations

When possible, make migrations safe to run multiple times:

Backward Compatibility

When adding constraints, consider existing data:

Troubleshooting

Migration Failed to Apply

Symptoms: Server fails to start with migration error
Solution:
  1. Fix the SQL syntax error in the migration file
  2. Restart the server
If the migration partially applied, you may need to manually clean up the database before restarting.

Divergent Migrations

Symptoms: Warning about migrations with same version but different names
Solution: This typically happens when:
  • Multiple developers create migrations with the same version
  • Migration files are renamed after being applied
Fix by:
  1. Ensuring all migration files are in version control
  2. Using U prefix migrations (timestamp-based) to avoid conflicts

Migration Skipped

Symptoms: Migration exists but isn’t applied Causes:
  1. Invalid filename format - Must match [U|V]<version>__<name>.sql
  2. Lower version than applied - Check _schema_history for highest version
  3. File not in correct directory - Place in migrations/<db>/
Check applied migrations:

Can’t Modify Applied Migration

Problem: You found a bug in an applied migration Solution: Create a new migration to fix it:

Database Locked

Symptoms: Migration fails with “database is locked” Causes:
  • Another process has the database open
  • Previous migration didn’t complete cleanly
Solution:
  1. Stop all TrailBase processes
  2. Check for stale lock files: <data-dir>/data/*.db-wal
  3. Restart TrailBase

Migration Internals

Schema History Table

TrailBase tracks migrations in _schema_history:

Migration Runner

The migration runner:
  1. Discovers migrations from all sources
  2. Sorts by version (numeric or timestamp)
  3. Checks _schema_history for applied migrations
  4. Applies pending migrations in transaction
  5. Records each migration in _schema_history

Timestamp Generation

Timestamps are Unix timestamps (seconds since epoch):
The CLI ensures unique timestamps by incrementing if multiple migrations are created in the same second.

Advanced Topics

Embedded Migrations

TrailBase includes embedded migrations for system tables:
  • Base migrations - Core tables (_user, _session)
  • Main migrations - Additional functionality
  • Logs migrations - Logging tables
These run before user migrations and cannot be modified.

Multi-Database Migrations

Create separate migration directories for each database:
Configure databases in config.textproto:

Testing Migrations

Test migrations on a copy of production data:

Rollback Strategies

SQLite doesn’t support rollback migrations directly. Strategies:
  1. Backup before migration:
  2. Create compensating migration:
  3. Restore from backup: