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
ReadReplicaRouterclass inapps/api/plane/utils/core/dbrouters.pyenforces primary-only migration execution through itsallow_migratemethod, which validatesdb == "default" - Migration files are auto-generated via
python manage.py makemigrationsand stored inapps/api/plane/db/migrations/for version control - Production deployments execute
python manage.py migrate --noinputduring container startup to apply pending schema changes - Complex data transformations leverage
RunPythonoperations with support utilities fromapps/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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →