Skip to content

Email templates: migration for key, managed_by, source and content hash columns #486

Closed

Description

@Silthus

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; sqlmigrate output for both migrations in the PR.
  • Local: migrate a local stack forward and back.

Acceptance criteria

How to build (map #461)

  • /tdd red-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's master is 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

  1. changed the title [-]Email templates: migration for key, managed_by and source columns[/-] [+]Email templates: migration for key, managed_by, source and content hash columns[/+] on Oct 16, 2026
  2. self-assigned this
    on Oct 16, 2026
  3. Silthus commented on Oct 16, 2026

    @Silthus
    OwnerAuthor

    Runtime: T3 child task
    Task: node:delegated-task:command%3Amcp%3A955e36b3-3986-469c-b816-09cf656780af%3Adelegate-task%3Awf-email-impl-486-1
    Cast: Opus 5.5, high. Cap 120 minutes.

  4. Silthus commented on Oct 16, 2026

    @Silthus
    OwnerAuthor

    Resolution

    PR: PostHog/posthog#118769 (draft), branch Silthus:spec/code-managed-templates-migration, based on upstream/master f0d00ac7cba.
    Reviewed head: a5dbb10460bcc9f8cebdaab46aef793d3ed0866a. fleet proof gate passes 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: SeparateDatabaseAndState with a UniqueConstraint in state, and CreateIndexConcurrently in the database, atomic = False. Predicate: NOT "deleted" AND "key" IS NOT NULL.
    • The model and max_migration.txt. No serializer, view or frontend change.

    Proof

    • Red: all three cases in products/messaging/backend/tests/test_message_template_key.py fail with MessageTemplate() got unexpected keyword arguments: 'key'.
    • Green: the same three cases pass. The messaging backend suite passes (315 tests).
    • Mutation: a test index without the deleted clause fails test_the_key_is_free_1_after_the_holder_is_deleted.
    • sqlmigrate: 0007 is seven ADD COLUMN ... NULL. 0008 is CREATE 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.
    • Risk analyzer: analyze_migration_risk --fail-on-blocked reports 2 Safe, 0 blocked.
    • Local stack: migrate messaging 0008 and back to 0005 on the dev database. The index and columns appear, then go. The dev database is back at its earlier state.
    • Review: GPT-6.1 Sol, high, through T3 delegate_task. No findings.

    Calls made

    • The index is named unique_message_template_key_per_team. The workflow branch's unique_key_for_team would collide, because Postgres index names share one namespace.
    • help_text is on the model now, so the serializer slice can expose the fields without another no-SQL AlterField migration.
    • managed_by uses max_length=400 and the gui/code choices of the workflow column.
    • Only Live run is owed. Prod shape is not owed, because 0007 leaves 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.
    • The ticket asked for one review, so no qa-swarm ran, even though migrations are a merge-risk danger area. Grade: low, two-way door.

    Follow-ups

    • #118664 also adds messaging 0007. Whichever PR lands second renumbers. The PR body says so.
    • gh pr create --assignee Silthus failed because the account lacks permission to assign on PostHog/posthog. The PR has no assignee.
    • Local test runs needed a private Postgres test DB (PGDATABASE=..., cloned from a healthy test DB) and a private ClickHouse test DB (PYTEST_XDIST_WORKER=...). The shared test_posthog and posthog_test were stale. A fresh --create-db also failed on this devbox (extension "ltree" already exists).
    • CI on the draft has only run the scanner checks so far.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions