Database Migrations
Fluxbase supports automatic database migrations that run on startup. The platform includes a dual migration system:
- System Migrations - Built-in migrations embedded in the Fluxbase binary
- User Migrations - Custom migrations you can provide via the filesystem
Overview
Section titled “Overview”Migrations are powered by golang-migrate and use PostgreSQL as the database backend. Fluxbase automatically tracks migration state in separate tables:
_fluxbase.schema_migrations- Tracks system migrations_fluxbase.user_migrations- Tracks your custom migrations
This separation ensures that system and user migrations don’t conflict with each other.
Dual Migration System
Section titled “Dual Migration System”Fluxbase maintains two independent migration tracks to separate platform updates from application changes:
graph LR subgraph "System Migrations" S1[Embedded in Binary] --> S2[_fluxbase.schema_migrations] S2 --> S3[Platform Tables & RLS] end
subgraph "User Migrations" U1[Filesystem Directory] --> U2[_fluxbase.user_migrations] U2 --> U3[Application Schema] end
S3 -.-> DB[(PostgreSQL)] U3 -.-> DBSystem Migrations
Section titled “System Migrations”Purpose: Platform infrastructure managed by Fluxbase
System migrations are embedded into the Fluxbase binary at compile time and include:
- Core authentication tables
- OAuth provider tables
- Row-level security policies
- Webhook configuration
- Storage metadata tables
- Jobs and functions infrastructure
Tracking: _fluxbase.schema_migrations table
Execution: Automatically run on every startup (idempotent - only new migrations applied)
Management: Controlled by Fluxbase releases, not user-modifiable
User Migrations
Section titled “User Migrations”Purpose: Application-specific schema managed by developers
User migrations allow you to add your own custom database schema and data migrations without modifying Fluxbase source code.
Tracking: _fluxbase.user_migrations table
Execution: Run after system migrations if DB_USER_MIGRATIONS_PATH is configured
Management: You create and maintain these files
When to Use Each
Section titled “When to Use Each”| Use System Migrations | Use User Migrations |
|---|---|
| Never (managed by Fluxbase) | Application tables |
| Custom indexes | |
| Data transformations | |
| Business logic triggers | |
| Application-specific RLS policies |
Migration State Machine
Section titled “Migration State Machine”stateDiagram-v2 [*] --> Pending: Migration file created Pending --> Running: Execution starts Running --> Applied: Success Running --> Failed: Error Failed --> Pending: Fix and retry Applied --> RolledBack: Rollback executed RolledBack --> Pending: Can reapply Applied --> [*]
note right of Failed: Migration marked as "dirty"<br/>Must be manually fixedMigration States:
- Pending: Not yet executed
- Running: Currently executing (rare to see)
- Applied: Successfully completed
- Failed: Error occurred (database in “dirty” state)
- RolledBack: Reverted via down migration
Migration File Format
Section titled “Migration File Format”User migrations follow the standard golang-migrate format:
001_create_users_table.up.sql001_create_users_table.down.sql002_add_timestamps.up.sql002_add_timestamps.down.sqlEach migration has two files:
.up.sql- Applied when migrating forward.down.sql- Applied when rolling back (optional but recommended)
Migration Numbering
Section titled “Migration Numbering”Migrations are executed in numerical order based on the prefix. Best practices:
- Use sequential numbering:
001,002,003, etc. - Zero-pad numbers for proper sorting
- Never reuse or skip numbers
- Never modify a migration that has already been applied
Example Migration
Section titled “Example Migration”001_create_products_table.up.sql:
-- Create products table in public schemaCREATE TABLE IF NOT EXISTS public.products ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, description TEXT, price DECIMAL(10,2) NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW());
-- Add RLS policiesALTER TABLE public.products ENABLE ROW LEVEL SECURITY;
-- Allow all authenticated users to read productsCREATE POLICY "Products are viewable by authenticated users" ON public.products FOR SELECT TO authenticated USING (true);
-- Allow only admins to insert/update/delete productsCREATE POLICY "Products are manageable by admins" ON public.products FOR ALL TO authenticated USING (auth.role() = 'admin') WITH CHECK (auth.role() = 'admin');001_create_products_table.down.sql:
-- Drop the table (this will also drop policies)DROP TABLE IF EXISTS public.products CASCADE;Configuration
Section titled “Configuration”Docker Compose
Section titled “Docker Compose”To enable user migrations in Docker Compose:
- Create a directory for your migrations:
mkdir -p deploy/migrations/user-
Add your migration files to this directory
-
Update
docker-compose.yml:
services: fluxbase: environment: # Enable user migrations DB_USER_MIGRATIONS_PATH: /migrations/user volumes: # Mount migrations directory (read-only) - ./migrations/user:/migrations/user:ro- Restart Fluxbase:
docker-compose restart fluxbaseKubernetes (Helm)
Section titled “Kubernetes (Helm)”To enable user migrations in Kubernetes:
- Create a ConfigMap or PVC with your migration files
Option A: Using ConfigMap (for small migrations):
kubectl create configmap user-migrations \ --from-file=migrations/user/ \ -n fluxbaseOption B: Using PVC (recommended for production):
migrationsPersistence: enabled: true size: 100Mi storageClass: "" # Use cluster default
config: database: userMigrationsPath: /migrations/user- Install or upgrade the Helm chart:
helm upgrade --install fluxbase ./deploy/helm/fluxbase \ --namespace fluxbase \ --create-namespace \ -f values.yaml- Copy your migration files to the PVC:
# Find a podPOD_NAME=$(kubectl get pod -n fluxbase -l app.kubernetes.io/name=fluxbase -o jsonpath="{.items[0].metadata.name}")
# Copy migrationskubectl cp migrations/user/ fluxbase/$POD_NAME:/migrations/user/- Restart the deployment:
kubectl rollout restart deployment/fluxbase -n fluxbaseEnvironment Variables
Section titled “Environment Variables”You can configure user migrations via environment variables:
| Variable | Description | Default |
|---|---|---|
DB_USER_MIGRATIONS_PATH | Path to user migrations directory | "" (disabled) |
When DB_USER_MIGRATIONS_PATH is empty or not set, user migrations are skipped.
CLI Commands
Section titled “CLI Commands”For local development, Fluxbase provides Make commands for migration management:
# Run all pending migrationsmake migrate-up
# Rollback the last migrationmake migrate-down
# Create a new migration file pairmake migrate-create NAME=add_users_table# Creates: migrations/XXX_add_users_table.up.sql and .down.sql
# Check current migration versionmake migrate-version
# Force set migration version (use with caution)make migrate-force VERSION=5Common workflow:
# 1. Create new migrationmake migrate-create NAME=add_products
# 2. Edit the generated files# migrations/001_add_products.up.sql# migrations/001_add_products.down.sql
# 3. Apply migrationmake migrate-up
# 4. Test rollbackmake migrate-down
# 5. Reapplymake migrate-upPrerequisites: These commands require:
- Local PostgreSQL running
- Database connection configured in
.envorfluxbase.yaml migrateCLI installed (automatically available in DevContainer)
Migration Execution
Section titled “Migration Execution”When Fluxbase starts, migrations are executed in this order:
- System migrations are applied first from the embedded filesystem
- User migrations are applied second from the configured directory (if enabled)
Both migration sets maintain their own version tracking, so they can be at different versions.
Migration progress is logged during startup:
INFO Running database migrations...INFO Running system migrations...INFO Migrations applied successfully source=system version=6INFO Running user migrations... path=/migrations/userINFO Migrations applied successfully source=user version=3INFO Database migrations completed successfullyIf no new migrations are found:
INFO No new migrations to apply source=systemINFO No new migrations to apply source=userBest Practices
Section titled “Best Practices”1. Test Migrations Locally First
Section titled “1. Test Migrations Locally First”Always test migrations in a development environment before applying to production:
# Start local environmentdocker-compose up -d
# Check logs for migration successdocker-compose logs fluxbase | grep -i migration2. Use Transactions
Section titled “2. Use Transactions”Wrap DDL statements in transactions when possible:
BEGIN;
CREATE TABLE products (...);CREATE INDEX IF NOT EXISTS idx_products_name ON products(name);
COMMIT;3. Make Migrations Idempotent
Section titled “3. Make Migrations Idempotent”Use conditional statements to make migrations safe to re-run:
-- Good: Uses IF NOT EXISTSCREATE TABLE IF NOT EXISTS products (...);
-- Bad: Will fail if table existsCREATE TABLE products (...);4. Add Indexes Concurrently
Section titled “4. Add Indexes Concurrently”For large tables, create indexes without locking:
-- Add indexes concurrently (won't block reads/writes)CREATE INDEX CONCURRENTLY idx_products_category ON products(category);5. Plan for Rollbacks
Section titled “5. Plan for Rollbacks”Always include .down.sql files to support rollback scenarios:
-- down.sql should reverse the up.sql changesDROP INDEX IF EXISTS idx_products_category;DROP TABLE IF EXISTS products;6. Document Complex Migrations
Section titled “6. Document Complex Migrations”Add comments explaining the purpose of complex migrations:
-- Migration: Add full-text search to products-- Author: Your Name-- Date: 2024-01-15-- Reason: Enable product search functionality
ALTER TABLE products ADD COLUMN search_vector tsvector;
CREATE INDEX IF NOT EXISTS idx_products_search ON products USING gin(search_vector);Common Migration Tasks
Section titled “Common Migration Tasks”Adding a New Table
Section titled “Adding a New Table”-- 002_create_orders_table.up.sqlCREATE TABLE public.orders ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.app_users(id) ON DELETE CASCADE, total DECIMAL(10,2) NOT NULL, status TEXT NOT NULL DEFAULT 'pending', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW());
CREATE INDEX IF NOT EXISTS idx_orders_user_id ON orders(user_id);CREATE INDEX IF NOT EXISTS idx_orders_status ON orders(status);
ALTER TABLE public.orders ENABLE ROW LEVEL SECURITY;Adding a Column
Section titled “Adding a Column”-- 003_add_product_sku.up.sqlALTER TABLE public.products ADD COLUMN IF NOT EXISTS sku TEXT UNIQUE;
-- Add index for lookupsCREATE INDEX CONCURRENTLY IF NOT EXISTS idx_products_sku ON products(sku);Modifying a Column
Section titled “Modifying a Column”-- 004_change_price_precision.up.sql-- Increase price precision from DECIMAL(10,2) to DECIMAL(12,4)ALTER TABLE public.products ALTER COLUMN price TYPE DECIMAL(12,4);Adding an Enum Type
Section titled “Adding an Enum Type”-- 005_add_order_status_enum.up.sql-- Create enum typeCREATE TYPE order_status AS ENUM ('pending', 'processing', 'shipped', 'delivered', 'cancelled');
-- Migrate existing dataALTER TABLE orders ALTER COLUMN status TYPE order_status USING status::order_status;Troubleshooting
Section titled “Troubleshooting”Migration Failed - Dirty State
Section titled “Migration Failed - Dirty State”If a migration fails partway through, the database may be in a “dirty” state:
ERROR failed to run user migrations: Dirty database version X. Fix and force version.Fluxbase will automatically attempt to recover from dirty state by forcing the version. If this fails, you can manually fix it:
-- Connect to databasepsql -h localhost -U fluxbase -d fluxbase
-- Check migration stateSELECT * FROM _fluxbase.user_migrations;
-- Force version if needed (replace X with the correct version)DELETE FROM _fluxbase.user_migrations;INSERT INTO _fluxbase.user_migrations (version, dirty) VALUES (X, false);Migration Not Running
Section titled “Migration Not Running”If your migration isn’t being applied:
- Check file naming: Ensure files follow the format
NNN_name.up.sql - Check file location: Verify files are in the correct directory
- Check permissions: Ensure Fluxbase can read the migration files
- Check logs: Look for migration errors in Fluxbase logs
- Check version: Verify the migration version is newer than the current version
Checking Migration Status
Section titled “Checking Migration Status”To see which migrations have been applied:
-- Check system migrationsSELECT * FROM _fluxbase.schema_migrations ORDER BY version;
-- Check user migrationsSELECT * FROM _fluxbase.user_migrations ORDER BY version;Advanced Topics
Section titled “Advanced Topics”Running Migrations Separately
Section titled “Running Migrations Separately”In some cases, you may want to run migrations separately from application startup (e.g., during CI/CD).
Fluxbase currently runs migrations automatically on startup. For manual migration control, you can:
- Use the golang-migrate CLI directly
- Use a separate init container in Kubernetes
- Run migrations in a CI/CD pipeline before deploying
Migration Locking
Section titled “Migration Locking”Fluxbase uses advisory locks to prevent concurrent migrations. This is handled automatically by golang-migrate.
Schema Versioning
Section titled “Schema Versioning”Each migration source (system and user) maintains its own version number independently. This allows:
- System migrations to be updated without affecting user migrations
- User migrations to be rolled back without affecting system migrations
- Clear separation of concerns