
Django Migrations
FreeStreamline your Django migration processes with safety guidelines.
Free · Opens the source repo
What Django Migrations does
The Django Migrations skill provides a structured approach to managing migrations within Django applications, particularly for PostHog. It emphasizes best practices for creating, adjusting, and reviewing migrations, ensuring that developers can implement changes without introducing risks to their database integrity. The skill is particularly useful for handling complex migration scenarios such as non-blocking index changes, multi-phase schema modifications, and data backfills, which are common in evolving applications.
When using this skill, developers are guided through a series of best practices and workflows that help classify changes as either additive or risky. This classification is crucial for determining the appropriate migration strategy. For instance, the skill provides commands to safely generate migrations and apply necessary safety rules, ensuring that developers can validate their changes before deployment. Additionally, it includes specific instructions for handling deletion or retirement of models and tables, which is often a delicate process in migration management.
The skill also addresses the unique challenges posed by hot tables, which are frequently accessed and can lead to performance issues during migration operations. It provides strategies to mitigate these risks, such as using extension models or concurrent index operations. By following the guidelines laid out in the skill, developers can avoid common pitfalls associated with migrations and maintain a stable application environment.
Overall, this skill is designed for Django developers who need to navigate the complexities of database migrations safely and efficiently, particularly within the context of PostHog's architecture.
When to use it
Use this skill when creating or modifying Django/Postgres migrations, especially in complex scenarios.
When not to use it
This skill is not suitable for simple migrations that do not require adherence to strict safety guidelines or for migrations involving ClickHouse, which has its own dedicated skill.
What you can build with it
Complex Schema Changes
When implementing multi-phase schema changes, this skill guides you through safe practices to avoid downtime.
Handling Hot Tables
If your migration involves hot tables, the skill provides strategies to minimize performance impact during the migration process.
Retiring Features Safely
When you need to retire a model or feature, the skill outlines the necessary steps to ensure a smooth transition without data loss.
How to install Django Migrations
View source1. Install with the skills CLI
npx skills add posthog/posthog/django-migrations --agent claude-code2. Or install it manually
Download the skill folder and drop it into ~/.claude/skills/ for all projects, or .claude/skills/ to scope it to one repo. Restart Claude Code so it picks up the new skill.
Anthropic's agentic coding CLI, and the reference implementation of Agent Skills. Drop a skill folder into ~/.claude/skills and Claude Code loads it automatically whenever a task matches the skill's description. Claude Code docs
Inside SKILL.md
Written by posthogDjango migrations
Read these files first, before writing or editing a migration:
docs/published/handbook/engineering/developing-locally.md(## Django migrations,### Non-blocking migrations,### Resolving merge conflicts)docs/published/handbook/engineering/safe-django-migrations.mddocs/published/handbook/engineering/databases/schema-changes.mdproducts/README.md(## Adding or moving backend models and migrations) when working inproducts/*
If the task is a ClickHouse migration, use clickhouse-migrations instead.
Never delete a migration file
Adding migrations is fine. Deleting a historical one — any */migrations/NNNN_*.py already on master, even an app's 0001_initial.py, even to "undo" a schema change — is not. Deleting the file undoes nothing: the table and its constraints stay in every database where the migration ran, fresh databases never recreate them, and the "Migration Risk Analysis" CI job re-flags the file as a phantom new migration on every open PR that predates the deletion. The deleted-migration check in the repo-checks CI job (the hogli lint:migration-deletions command) blocks this. Genuinely intentional, reviewed deletions — a product/app move, a revert, a squash — are acknowledged in .github/scripts/migration-deletion-allowlist.txt, never by disabling the guard.
If a task asks you to delete a migration file, stop and flag it instead.
To retire a model/table:
- Remove all usage and the model class.
makemigrations, then wrap the generatedDeleteModelinmigrations.SeparateDatabaseAndState(state_operations=[...])(state only, no DB change). KEEP this file. Keep the app inINSTALLED_APPS. - Deploy, wait at least one full deploy cycle.
- Optionally
DROP TABLElater in a NEWRunSQLmigration — never by deleting old files.
Full guide: safe-django-migrations.md (## Dropping Tables, ### Removing a whole product or app). Deleting a migration your branch added but never merged to master is allowed (regenerating).
Workflow
- Classify the change as additive (new nullable column, new table) or risky (drop/rename,
NOT NULL, indexes, constraints, large data updates, model moves). A change is also risky if it touches a hot table, regardless of how additive it looks. See also the cross-languageNOT NULLhazard below. - Generate:
DEBUG=1 ./manage.py makemigrations [app_label]. For merge conflicts:python manage.py rebase_migration <app> && git add <app>/migrations(posthogoree). - Apply safety rules from
safe-django-migrations.md— the doc covers multi-phase rollouts,SeparateDatabaseAndState, concurrent operations, idempotency, and all risky patterns in detail. - Validate:
./manage.py sqlmigrate <app> <migration_number>, run tests, confirm linear migration sequence.
Use the migration helpers
posthog.migration_helpers has drop-in operations for the risky-but-common cases. Reach for these first; they track Django state, disable timeouts, and are idempotent under bin/migrate retries:
- Add/drop an index →
SafeAddIndexConcurrently/SafeRemoveIndexConcurrently(model_name+models.Index). Never use Django'sAddIndexConcurrently— CI blocks it. - Add a CHECK constraint →
AddConstraintNotValidthenValidateConstraintin a later migration (or same migration withatomic = False). - Add a ForeignKey to a hot table → declare the FK with
db_constraint=Falseon the model (soCreateModel/AddFieldemit no parent lock), then add the DB constraint back withAddForeignKeyNotValidand follow up withValidateForeignKeyin a later migration. See foreign keys to hot tables. - Index expressed only as raw SQL (no Django
Index) →CreateIndexConcurrently/DropIndexConcurrentlywrapped inSeparateDatabaseAndState.
All concurrent-index ops require atomic = False.
Meta-principle when you hit a risky-but-common pattern with no helper: don't hand-roll the safe DDL from docs — ship a drop-in helper in posthog/migration_helpers and point the CI policy at it. A one-import helper beats a wall of caveated RunSQL every time.
Hot table hazard
posthog_team, posthog_user, posthog_organization, and posthog_project are read on virtually every request. Any ALTER TABLE on them — including a plain nullable AddField, which is "safe" everywhere else — needs an ACCESS EXCLUSIVE lock, and while that lock request waits behind in-flight queries, every later query on the table queues behind it. Even a metadata-only ADD COLUMN can stall site-wide traffic in waves (one per bin/migrate retry) until the ALTER wins the lock race. This has caused production 5xx incidents.
Before writing a migration that touches one of these models:
- For
Team: put domain-specific fields on a Team extension model instead —posthog/models/team/README.md. That's aCREATE TABLE, no lock onposthog_team. CREATE INDEX CONCURRENTLY(viaSafeAddIndexConcurrently) is fine —SHARE UPDATE EXCLUSIVEdoesn't block reads or writes.- If the field genuinely belongs on the hot table (core identity, cross-product settings, SDK config), the
HotTableAlterPolicyanalyzer blocks the migration in CI until<app_label>.<migration_name>is added toposthog/management/migration_analysis/hot_table_acknowledged_migrations.txt. That acknowledgment also means coordinating the deploy with infra for a low-traffic window.
Foreign keys to hot tables
A ForeignKey targeting a hot table is the same hazard from the other side, and it bites from any app — a plain product-app CreateModel or AddField with to="posthog.team" (or settings.AUTH_USER_MODEL, which is posthog_user). Creating the FK constraint takes a SHARE ROW EXCLUSIVE lock on the referenced parent, which conflicts with the ROW EXCLUSIVE every INSERT/UPDATE/DELETE on the parent holds; under write traffic the lock queues and lock_timeout cancels it on each bin/migrate retry. HotTableAlterPolicy now flags this case. Two ways out:
db_constraint=Falseon theForeignKey— emits no FK constraint and takes no lock on the parent at all (app-level enforcement only). This is the only truly lock-free path.- A real DB constraint, two-phase — declare the FK
db_constraint=False, then add it back as a DB constraint withAddForeignKeyNotValid, andValidateForeignKeyin a later migration. Be honest:ADD CONSTRAINT ... NOT VALIDstill takes a briefSHARE ROW EXCLUSIVElock on the parent for the metadata add — it skips the row scan, so it shrinks the lock window but does not eliminate it.VALIDATEthen runs lock-free on the parent.
Cross-language NOT NULL hazard
posthog_user, posthog_team, and other core tables in the main Postgres database are written by Django and by nodejs/ (plugin-server tests via insertRow), rust/ services, and Temporal workers. Those non-Django writers issue raw INSERTs that only list the columns they care about, so any new NOT NULL column without a Postgres-level DEFAULT will break them with null value in column "<col>" violates not-null constraint.
Django's default= alone does not create a Postgres-level default — by design, Django treats it as a Python-only attribute applied at Model.__init__:
- Callable defaults (
default=list,default=dict,default=uuid.uuid4) are never emitted into SQL at all. - Scalar defaults (
default=False,default=0,default="") are emitted asADD COLUMN ... DEFAULT X NOT NULLand then immediately dropped by a follow-upALTER COLUMN ... DROP DEFAULT— verify with./manage.py sqlmigrate.
Before merging, grep for external writers of the table:
rg -n "INSERT INTO <table>|insertRow\(.*'<table>'" nodejs rust products services
If any match, add both default= and db_default= to the model field. db_default= lands a real Postgres DEFAULT; default= keeps the Python-side value for ORM creates:
class User(models.Model):
hide_mcp_hints = models.BooleanField(default=False, db_default=False, null=False)
makemigrations will emit a plain AddField(..., db_default=False, default=False, ...), and sqlmigrate shows just ADD COLUMN ... DEFAULT false NOT NULL — no DROP DEFAULT follow-up.
db_default= is also load-bearing for the nodejs / rust test suites. posthog/management/commands/setup_test_environment.py calls disable_migrations() and builds the test schema directly from model definitions, skipping the migration entirely. Plain default= is invisible to that path; db_default= is what Django bakes into the generated CREATE TABLE. Without it, the postgres-parity and Jest jobs in .github/workflows/ci-nodejs.yml will fail on raw INSERTs even though ./manage.py migrate looks correct in isolation.
For modifying the default on an existing column (no ADD COLUMN), use a plain RunSQL instead:
migrations.RunSQL(
sql="ALTER TABLE <table> ALTER COLUMN <col> SET DEFAULT '[]'::jsonb;",
reverse_sql="ALTER TABLE <table> ALTER COLUMN <col> DROP DEFAULT;",
)
Always verify with ./manage.py sqlmigrate <app> <number> that no stray DROP DEFAULT slipped through, and confirm ./manage.py makemigrations --dry-run reports no state drift.
Frequently asked questions about Django Migrations
Similar skills
ClickHouse Logs Queries
Efficiently manage Supabase logs with ClickHouse SQL.
EF Core D2 Database Diagram Generator
Visualize your EF Core models as D2 diagrams effortlessly.
Safe SQL Execution
Ensure secure SQL execution in Supabase applications.
Oracle to PostgreSQL Migration
Identify migration risks between Oracle and PostgreSQL.
SSMA Console
Streamline Oracle to SQL Server migrations with ease.
SQL Performance Optimization
Enhance SQL query efficiency across all databases.
