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

> Learn Plane's Django ORM database migration strategy. Discover how they use Django migrations and a custom router to manage schema changes on the primary database and optimize read replicas for queries.

- Repository: [Plane/plane](https://github.com/makeplane/plane)
- Tags: how-to-guide
- Published: 2026-06-23

---

**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`](https://github.com/makeplane/plane/blob/main/workspace.py), [`project.py`](https://github.com/makeplane/plane/blob/main/project.py), and [`issue.py`](https://github.com/makeplane/plane/blob/main/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`](https://github.com/makeplane/plane/blob/main/0121_alter_estimate_type.py) or [`0116_workspacemember_explored_features_and_more.py`](https://github.com/makeplane/plane/blob/main/0116_workspacemember_explored_features_and_more.py)). Each migration is checked into version control to ensure reproducible schema history across environments.

```bash

# 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:

```python

# 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`](https://github.com/makeplane/plane/blob/main/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.

```python

# 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:

```bash

# 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.

```python

# 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`](https://github.com/makeplane/plane/blob/main/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`](https://github.com/makeplane/plane/blob/main/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`](https://github.com/makeplane/plane/blob/main/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`](https://github.com/makeplane/plane/blob/main/issue.py) and [`workspace.py`](https://github.com/makeplane/plane/blob/main/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`](https://github.com/makeplane/plane/blob/main/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`](https://github.com/makeplane/plane/blob/main/apps/api/plane/utils/filters/filter_migrations.py) to assist with data transformations, ensuring consistency when migrating existing records to new schema structures.