-- Reusable message templates (email/SMS/in-app). DB is runtime truth; platform defaults seeded -- from repo files on boot. Versions are immutable (a change = a new higher version). CREATE TYPE "IiosTemplateChannel" AS ENUM ('EMAIL', 'SMS', 'INTERNAL'); CREATE TABLE "IiosMessageTemplate" ( "id" TEXT NOT NULL, "scopeId" TEXT, "key" TEXT NOT NULL, "channel" "IiosTemplateChannel" NOT NULL, "locale" TEXT NOT NULL DEFAULT 'en', "version" INTEGER NOT NULL DEFAULT 1, "subject" TEXT, "bodyHtml" TEXT, "bodyText" TEXT, "variables" JSONB, "active" BOOLEAN NOT NULL DEFAULT true, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL, CONSTRAINT "IiosMessageTemplate_pkey" PRIMARY KEY ("id") ); ALTER TABLE "IiosMessageTemplate" ADD CONSTRAINT "IiosMessageTemplate_scopeId_fkey" FOREIGN KEY ("scopeId") REFERENCES "IiosScope"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- Uniqueness for SCOPED rows (scopeId NOT NULL). CREATE UNIQUE INDEX "IiosMessageTemplate_scopeId_key_channel_locale_version_key" ON "IiosMessageTemplate" ("scopeId", "key", "channel", "locale", "version"); -- Uniqueness for GLOBAL rows (scopeId IS NULL). Postgres treats NULL as distinct under a normal -- UNIQUE, so the constraint above does NOT stop two platform defaults for the same key — this -- partial index does. (Same footgun handled in the inbox idempotency migration.) CREATE UNIQUE INDEX "IiosMessageTemplate_global_key" ON "IiosMessageTemplate" ("key", "channel", "locale", "version") WHERE "scopeId" IS NULL; -- Resolution lookup: by key/channel/locale among active rows. CREATE INDEX "IiosMessageTemplate_key_channel_locale_active_idx" ON "IiosMessageTemplate" ("key", "channel", "locale", "active");