Database Migration Strategy for Plane's Django ORM Models: A Complete Technical Guide

Plane uses Django’s native migration framework combined with a custom database router to ensure all schema changes execute exclusively on the primary database while read replicas handle query traffic.

Plane’s backend is a Django application located in apps/api/plane. The database migration strategy follows a model-first approach where ORM definitions in apps/api/plane/db/models/ drive schema evolution, with corresponding migration scripts maintained in apps/api/plane/db/migrations/. A custom ReadReplicaRouter enforces safety by restricting migration operations to the primary database only, preventing accidental schema modifications on read replicas.

Model-First Development Workflow

Defining Models in the Repository

Developers define persisted data structures in Python modules under apps/api/plane/db/models/. Key files include workspace.py, project.py, and issue.py, each containing Django ORM model classes that serve as the single source of truth for database schema. When fields require modification—such as adding a priority field to the Issue model—developers edit these model files directly.

Auto-Generating Migration Files

After model changes, the team runs Django’s makemigrations command to generate new migration scripts. These files are regular Python modules using django.db.migrations and follow the naming convention ####_description.py (e.g., 0121_alter_estimate_type.py or 0116_workspacemember_explored_features_and_more.py). Each migration is checked into version control to ensure reproducible schema history across environments.


# Add a new field to the Issue model (apps/api/plane/db/models/issue.py)

# Then generate a migration:

python manage.py makemigrations issue

The resulting migration file contains sequential operations:


# apps/api/plane/db/migrations/0122_alter_issue_priority.py

from django.db import migrations, models

class Migration(migrations.Migration):
    dependencies = [
        ("issue", "0121_alter_estimate_type"),
    ]

    operations = [
        migrations.AddField(
            model_name="issue",
            name="priority",
            field=models.IntegerField(default=0),
        ),
    ]

Primary-Only Migration Safety

ReadReplicaRouter Implementation

Plane implements a custom database router in apps/api/plane/utils/core/dbrouters.py that strictly controls migration targets. The ReadReplicaRouter.allow_migrate method inspects the database alias and returns True only when db == "default", ensuring migrations never execute on read replicas.


# apps/api/plane/utils/core/dbrouters.py

def allow_migrate(self, db: str, app_label: str, model_name: str = None, **hints) -> bool:
    allowed = db == "default"          # ✅ Migrations run only on the primary DB

    if not allowed:
        logger.debug(f"Blocking migration for {app_label} on {db} database")
    return allowed

This router simultaneously directs read queries to replica databases during normal operations, but the allow_migrate check guarantees that schema-changing commands like migrate only affect the primary database.

Deployment Integration and Execution

Container Startup Commands

During deployment, the Plane service applies pending migrations automatically. The Docker compose configuration executes the migration command during container startup:


# Inside the Docker container or CI step:

python manage.py migrate --noinput

Sequential Migration Application

The migrate command walks through all unapplied migrations in the plane.db.migrations package, applying them sequentially to the primary database. Because ReadReplicaRouter.allow_migrate only permits operations on the "default" alias, this command safely skips any configured replica databases, ensuring schema consistency without risking read-replica corruption.

Advanced Data Migration Patterns

Complex Transformations with RunPython

For scenarios requiring data manipulation alongside schema changes, Plane utilizes Django’s RunPython operation. Migration files can invoke custom Python functions to transform existing records while maintaining transactional safety.


# apps/api/plane/db/migrations/0115_auto_20260105_1406.py

from django.db import migrations

def migrate_existing_api_tokens(apps, schema_editor):
    # Custom Python logic that updates token records

    ...

class Migration(migrations.Migration):
    dependencies = [...]
    operations = [
        migrations.RunPython(migrate_existing_api_tokens, reverse_code=migrations.RunPython.noop),
    ]

Migration Helper Utilities

Plane ships utility functions in apps/api/plane/utils/filters/filter_migrations.py specifically designed to support complex data migrations. These helpers ensure data consistency across schema changes when invoked from a migration’s RunPython hook.

Summary

  • Model-first development drives the database migration strategy, with ORM definitions in apps/api/plane/db/models/ serving as the schema source of truth
  • The ReadReplicaRouter class in apps/api/plane/utils/core/dbrouters.py enforces primary-only migration execution through its allow_migrate method, which validates db == "default"
  • Migration files are auto-generated via python manage.py makemigrations and stored in apps/api/plane/db/migrations/ for version control
  • Production deployments execute python manage.py migrate --noinput during container startup to apply pending schema changes
  • Complex data transformations leverage RunPython operations with support utilities from apps/api/plane/utils/filters/filter_migrations.py

Frequently Asked Questions

How does Plane prevent migrations from running on read replicas?

The ReadReplicaRouter class implements the allow_migrate method to return True only when the database alias equals "default". This logic blocks migration operations on any replica database, ensuring schema changes occur exclusively on the primary database while replicas remain read-only.

Where are the Django models and migration files located in the Plane repository?

Model definitions reside in apps/api/plane/db/models/ with files like issue.py and workspace.py, while migration scripts are stored in apps/api/plane/db/migrations/. Each model package maintains a corresponding migration module following Django’s conventions, with files like 0121_alter_estimate_type.py representing specific schema changes.

What command generates new migrations in Plane’s development workflow?

Developers run python manage.py makemigrations after modifying model fields. This creates a new Python module in the migrations directory containing the necessary schema change operations, which is then committed to version control to maintain a reproducible database history.

How are data migrations handled for complex transformations?

Plane uses Django’s RunPython operation within migration files to execute custom Python logic during the migration process. The repository provides utility functions in apps/api/plane/utils/filters/filter_migrations.py to assist with data transformations, ensuring consistency when migrating existing records to new schema structures.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →