Files
HealthLog/prisma/migrations/0035_coach_conversations_v1420/migration.sql
T
Marc-André BombeckandClaude Opus 4.7 5527001a01 feat(coach): add CoachConversation, CoachMessage, CoachUsage models
Persist Coach chat threads with AES-256-GCM-encrypted message bodies
and a per-user, per-day token-spend ledger. Encryption reuses the
active ENCRYPTION_KEYS entry so rotation continues to work without a
backfill. metric_source_json carries provenance labels only (window +
metric names, never raw values), keeping it queryable for analytics.

Cascade-on-user-delete is wired through the FK chain so GDPR delete
removes Coach data with the rest of the account. The integration-suite
truncation list picks up the new tables.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:54:28 +02:00

80 lines
2.8 KiB
SQL

-- v1.4.20 — AI Coach persistence + per-day token-spend ledger.
--
-- Three new tables:
-- coach_conversations — one row per chat thread, owned by a user.
-- coach_messages — encrypted message text + provenance metadata.
-- coach_usage — per-(user, day) token ledger for budget gating.
--
-- Encryption: `coach_messages.encrypted_content` is AES-256-GCM
-- ciphertext written via `src/lib/crypto.ts` under the active
-- `ENCRYPTION_KEYS` entry. Key rotation works the same way as for the
-- existing encrypted columns (Withings tokens, Telegram bot tokens,
-- moodLog API keys). `metric_source_json` is intentionally plain text
-- — it stores window + metric labels only ("last30days", "n=12"),
-- never raw values, so analytics queries can slice it without a key.
--
-- GDPR: every table cascades on user delete via FK chains, covered
-- end-to-end in tests/integration/cascade-delete.test.ts.
CREATE TABLE "coach_conversations" (
"id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"title" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "coach_conversations_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "coach_conversations_user_id_updated_at_idx"
ON "coach_conversations" ("user_id", "updated_at");
ALTER TABLE "coach_conversations"
ADD CONSTRAINT "coach_conversations_user_id_fkey"
FOREIGN KEY ("user_id") REFERENCES "users"("id")
ON DELETE CASCADE ON UPDATE CASCADE;
CREATE TABLE "coach_messages" (
"id" TEXT NOT NULL,
"conversation_id" TEXT NOT NULL,
"role" TEXT NOT NULL,
"encrypted_content" BYTEA NOT NULL,
"metric_source_json" TEXT,
"provider_type" TEXT,
"prompt_version" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "coach_messages_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "coach_messages_conversation_id_created_at_idx"
ON "coach_messages" ("conversation_id", "created_at");
ALTER TABLE "coach_messages"
ADD CONSTRAINT "coach_messages_conversation_id_fkey"
FOREIGN KEY ("conversation_id") REFERENCES "coach_conversations"("id")
ON DELETE CASCADE ON UPDATE CASCADE;
CREATE TABLE "coach_usage" (
"id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"date_key" TEXT NOT NULL,
"total_tokens" INTEGER NOT NULL DEFAULT 0,
"message_count" INTEGER NOT NULL DEFAULT 0,
"updated_at" TIMESTAMP(3) NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "coach_usage_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "coach_usage_user_id_date_key_key"
ON "coach_usage" ("user_id", "date_key");
CREATE INDEX "coach_usage_user_id_date_key_idx"
ON "coach_usage" ("user_id", "date_key");
ALTER TABLE "coach_usage"
ADD CONSTRAINT "coach_usage_user_id_fkey"
FOREIGN KEY ("user_id") REFERENCES "users"("id")
ON DELETE CASCADE ON UPDATE CASCADE;