CodeIgniter 4 Migrations: A Practical Guide to Schema Versioning in Team Environments
CodeIgniter 4's built-in migration system gives teams a lightweight, CLI-driven way to version database schemas. This post walks through the workflow, namespaced migrations, a concrete example, rollback caveats, and CI/CD integration patterns.
27 Jul 2025, 07:38 UTC

The problem: schema drift in team repositories
When multiple developers modify a database schema, sharing SQL files or manual ALTER statements leads to drift — environments diverge, onboarding breaks, and production deployments become risky. CodeIgniter 4 ships with a migration runner that treats schema changes as versioned, reviewable code. It’s not a heavy ORM migration layer; it’s a pragmatic, file-based system that fits CI4’s lightweight philosophy.
How CI4 migrations work
Each migration is a timestamp-prefixed PHP class stored under app/Database/Migrations/ (or a namespaced path). The class implements up() for forward changes and down() for rollback. The runner tracks applied migrations in a migrations table using the timestamp as primary key, so duplicate runs are prevented and rollbacks target the correct batch.
- Create:
php spark make:migration AddProductsTable→ generates2024-01-15-120000_AddProductsTable.php. - Run:
php spark migrateexecutes all pendingup()methods in timestamp order. - Rollback:
php spark migrate:rollbackruns the latest batch’sdown()methods. - Status:
php spark migrate:statusshows applied vs. pending migrations with batch numbers — handy for pre-deploy checks.
All commands exit non-zero on failure, which makes them safe to wire into CI pipelines.
Namespaced migrations for modular codebases (CI 4.2+)
Since CodeIgniter 4.2, modules or Composer packages can ship their own migrations. Configure additional paths in app/Config/Migrations.php:
public $namespace = [
'App' => APPPATH . 'Database/Migrations',
'MyModule' => ROOTPATH . 'modules/MyModule/Database/Migrations',
];
The runner merges all namespaces, orders by timestamp globally, and records the namespace in the migrations table. This lets a team split schema ownership across packages without a monolithic migration directory.
Worked example: creating a products table with a safe rollback
Generate the migration:
php spark make:migration CreateProductsTable
Edit the generated file:
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreateProductsTable extends Migration
{
public function up()
{
$this->forge->addField([
'id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
'name' => ['type' => 'VARCHAR', 'constraint' => 100],
'sku' => ['type' => 'VARCHAR', 'constraint' => 50, 'unique' => true],
'price' => ['type' => 'DECIMAL', 'constraint' => '10,2'],
'created_at' => ['type' => 'DATETIME', 'null' => true],
]);
$this->forge->addKey('id', true);
$this->forge->createTable('products', true); // true = IF NOT EXISTS
}
public function down()
{
$this->forge->dropTable('products', true); // true = IF EXISTS
}
}
Run it:
php spark migrate
Verify in your database: the products table exists and a row appears in migrations with version 2024-01-15-120000 (your timestamp). Roll back:
php spark migrate:rollback
The table is dropped and the migrations row removed. Note the IF NOT EXISTS/IF EXISTS flags — they make the migration idempotent and prevent errors if the runner is invoked twice.
Trade-offs and limitations you must plan for
- No auto-generated rollbacks. You write
down()by hand. A missing or buggydown()breaks rollback safety — test it locally before committing. - Never edit applied migrations. Changing a file that already ran in production causes checksum mismatches. Instead, create a new migration (e.g.,
RenameProductsSkuToCode). - Foreign key constraints. Dropping tables with FKs may require temporarily disabling checks in
up()/down()(e.g.,$this->db->query('SET FOREIGN_KEY_CHECKS=0')for MySQL). - Timestamp collisions. The
migrationstable uses the timestamp as PK. If two developers generate migrations in the same second on different machines, you’ll get a primary key conflict. Use a team convention (e.g., include initials) or let CI generate the timestamp during a release step. - Large tables and locking. Running migrations on read-only replicas or during peak traffic can lock tables. Schedule during maintenance windows or use tools like
pt-online-schema-changefor heavy ALTERs.
CI/CD integration pattern
Add a migration step after dependencies are installed but before the application starts. Example GitHub Actions snippet:
- name: Install dependencies
run: composer install --no-dev --prefer-dist --no-progress
- name: Run database migrations
run: php spark migrate
env:
CI: true
database.default.hostname: ${{ secrets.DB_HOST }}
database.default.database: ${{ secrets.DB_NAME }}
database.default.username: ${{ secrets.DB_USER }}
database.default.password: ${{ secrets.DB_PASS }}
database.default.DBDriver: MySQLi
The migrate command exits non-zero on failure, halting the pipeline automatically. For extra safety, run php spark migrate:status in a pre-deploy job to confirm no unexpected pending migrations exist.
Closing: make migrations a habit, not an afterthought
CodeIgniter 4’s migration system is intentionally minimal — it gives you versioned, reviewable schema changes without forcing a heavy abstraction. The engineering decision is simple: treat every schema change as a migration file, write and test the down() method, and run php spark migrate in every environment via automation. Start by generating a migration for your next schema change, verify rollback locally, then add the CLI step to your deployment pipeline. Your future self (and your teammates) will thank you when a hotfix rollback takes seconds instead of hours.
Verification checklist for your next PR
- Generate migration with
php spark make:migration. - Implement
up()using Forge; implementdown()that reverses it exactly. - Run
php spark migratelocally — confirm table/column changes. - Run
php spark migrate:rollback— confirm clean reversal and migrations table cleanup. - Run
php spark migrate:status— ensure only expected migrations show as pending/applied. - Commit the migration file; add
php spark migrateto your CI pipeline if not already present.
Note: This guide reflects CodeIgniter 4.2+ behavior. Always verify against the official documentation for your exact version.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.