Repository navigation
Email templates: migration for key, managed_by, source and content hash columns #486
Description
Map: Map: send email from code with one PostHog call
Spec: Spec: code-managed email templates, sections "Template metadata", "Content hash" and "Schema and data"
Cast: Opus 5.5, high
Repository: PostHog/posthog
Question
Can a message template carry a per-project key, an owner, a source pointer and a hash of its published content, with no change to any existing behavior?
Delivers end to end. Migration-only PR at the bottom of the stack. Five nullable columns on the message template: key (400), managed_by (choices gui, code), source_repository, source_path, source_ref (400 each), content_hash (80), superseded_content_hashes (array of strings of 80, used as a capped list). No default, no backfill. A partial unique constraint on (team, key) where key is set and the template is not deleted, built with a concurrent index inside SeparateDatabaseAndState, atomic = False, in its own migration file. Nothing reads or writes the columns yet.
Prior art to port by hand (not cherry-pick): feat/workflows-key-migrations on the fork (c56aa4b984, migrations 0030_hogflow_key and 0031_hogflow_unique_key_for_team) and migration 0032 on feat/workflows-code-managed-read-only for the ownership columns.
Test seams. The migration checks the repo already runs (/django-migrations: sqlmigrate shows CREATE UNIQUE INDEX CONCURRENTLY, the migration risk analyzer passes). One model-level test that the constraint rejects a second live template with the same key in one team, and accepts the same key in another team and next to a deleted template. Skip it if the repo's migration tests already prove partial constraints and /writing-tests says it adds nothing.
Write scope. products/messaging/backend/models/message_template.py, two new migrations in products/messaging/backend/migrations/, max_migration.txt.
Proof.
- Red: the constraint test fails (no column).
- Green: it passes;
sqlmigrateoutput for both migrations in the PR. - Local: migrate a local stack forward and back.
Acceptance criteria
- Only the model and migrations change. No serializer, view or frontend change.
- The index migration is concurrent and non-atomic, separate from the column migration.
- A deleted template does not hold its key.
- The PR notes the
0007collision with feat(workflows): draft and publish edits to transactional email templates PostHog/posthog#118664: whichever lands second renumbers.
How to build (map #461)
/tddred-green-refactor, outside-in, at the seams named above. Real Postgres, Valkey and ClickHouse; mock only what the seam section allows.- Repo skills apply where the diff touches their area:
/writing-tests,/improving-drf-endpoints,/django-migrations,/adding-api-scopes,/implementing-mcp-tools,/writing-ui-components,/writing-kea-logics,/writing-user-facing-copy,/writing-code-comments. - Read code from
upstream/master(PostHog/posthog). The fork'smasteris stale. - Public repo: no usage numbers, customer names, internal metrics, Slack content or security reasoning in code, commits or the PR. Fixture addresses use
example.com. The private note the map points at is background only; never quote it. - Shipped means a draft PR handed off for review (see the map's Notes for remotes and branches). Never mark ready, enqueue or merge.
Activity
[-]Email templates: migration for key, managed_by and source columns[/-][+]Email templates: migration for key, managed_by, source and content hash columns[/+]Silthus commentedon Oct 16, 2026
Runtime: T3 child task
Task:
node:delegated-task:command%3Amcp%3A955e36b3-3986-469c-b816-09cf656780af%3Adelegate-task%3Awf-email-impl-486-1Cast: Opus 5.5, high. Cap 120 minutes.
Silthus commentedon Oct 16, 2026
Resolution
PR: PostHog/posthog#118769 (draft), branch
Silthus:spec/code-managed-templates-migration, based onupstream/masterf0d00ac7cba.Reviewed head:
a5dbb10460bcc9f8cebdaab46aef793d3ed0866a.fleet proof gatepasses on it.What landed
0007_messagetemplate_code_managed_fields: seven nullable columns (key,managed_by,source_repository,source_path,source_ref,content_hash,superseded_content_hashes). No default, no backfill.0008_messagetemplate_unique_key_per_team:SeparateDatabaseAndStatewith aUniqueConstraintin state, andCreateIndexConcurrentlyin the database,atomic = False. Predicate:NOT "deleted" AND "key" IS NOT NULL.max_migration.txt. No serializer, view or frontend change.Proof
products/messaging/backend/tests/test_message_template_key.pyfail withMessageTemplate() got unexpected keyword arguments: 'key'.deletedclause failstest_the_key_is_free_1_after_the_holder_is_deleted.sqlmigrate: 0007 is sevenADD COLUMN ... NULL. 0008 isCREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS "unique_message_template_key_per_team" ... WHERE (NOT "deleted" AND "key" IS NOT NULL). Full output is in the PR.analyze_migration_risk --fail-on-blockedreports 2 Safe, 0 blocked.migrate messaging 0008and back to0005on the dev database. The index and columns appear, then go. The dev database is back at its earlier state.delegate_task. No findings.Calls made
unique_message_template_key_per_team. The workflow branch'sunique_key_for_teamwould collide, because Postgres index names share one namespace.help_textis on the model now, so the serializer slice can expose the fields without another no-SQLAlterFieldmigration.managed_byusesmax_length=400and thegui/codechoices of the workflow column.0007leaves every key null, so the index covers no existing row. Cross-version is not owed: the columns are nullable, and no Node or Rust code reads the table.qa-swarmran, even though migrations are a merge-risk danger area. Grade: low, two-way door.Follow-ups
0007. Whichever PR lands second renumbers. The PR body says so.gh pr create --assignee Silthusfailed because the account lacks permission to assign on PostHog/posthog. The PR has no assignee.PGDATABASE=..., cloned from a healthy test DB) and a private ClickHouse test DB (PYTEST_XDIST_WORKER=...). The sharedtest_posthogandposthog_testwere stale. A fresh--create-dbalso failed on this devbox (extension "ltree" already exists).