Decision
Meet transcripts and AI notes are kept for 365 days (owner-approved). The purge runs as the default-off Cron Control jobmeet-transcript-retention and
stays a no-op until the database fence is opened. The consent notice keeps its
current wording until deletion is enabled in production.
Scope
M1-B explicitly replaces
private.purge_meet_transcripts_batch to use
purge_after <= now() in dry-run and delete. NULL means forever. Existing
sessions retain their previous deadlines through a backfill from
coalesce(ended_at, created_at) + 365 days; new sessions default to 365 days
from creation. The purge clears edited notes alongside the original notes.
Workspace overrides must be applied by the trusted server integration.
The schema migration closes the stop fence and withdraws delete approval.
Opening the fence and enabling deletion require fresh reviewed dry-run counts.
Rollback restores the original 365-day predicate with a hardened search path
and service-only grants, and closes the fence again.
Never touched: meetings, participants, session rows and their usage/cost fields,
recording sessions, audio_chunks and audio storage objects, consent evidence,
Meet AI memories (a separately consented store) and Durable Object state.
Consent rows restrict session deletion. A separate service-role erasure flow must
delete consent rows explicitly before deleting a session or meeting. The transcript
purge keeps both and requires no erasure-order change. Account deletion clears the
consent user FK and retains a random session-scoped participant pseudonym.
Design
private.meet_transcript_purge_control is a singleton fence row, readable and
writable only by the database owner:
fence_open = false(default): every run records askippedaudit row and touches nothing.fence_open = true,delete_enabled = false: a dry run records totals and a per-workspace breakdown (top 100) on the run row, and deletes nothing.delete_enabled = true(requiresfence_open): deletion. Closing the fence must also cleardelete_enabled, so reopening it starts with dry runs again.
service_role can only execute three SECURITY DEFINER functions; it cannot read
or change the fence.
The functions declare a 20-second statement timeout, but a function-level
statement_timeout does not apply to calls over the API: the API role’s own
timeout applies. Measure batch timing in the production dry run before
approving deletion.
Progress is the remaining data, so a retry or the next run after a crash
continues safely. private.meet_transcript_purge_runs keeps mode, terminal status,
batch count and counts per table. It never stores content.
The Worker side lives in apps/cron-control/src/processors/meet-transcript-retention.ts:
an injectable loop with no Worker imports, plus a thin adapter. Before every batch
it checks the Cron Control stop fence and a 25-second time reserve. It retries
5xx and network failures at most three times per run with 1/2/4-second
backoff, never retries after a deadline, and rejects summaries over 1 KiB or
out-of-bounds counts as unknown.
Cost bounds
The job shares the daily17 3 * * * trigger (10:17 GMT+7) with a 300-second
processor deadline.
The ledger adds its usual claim/fence/complete Durable Object requests. Five
failed or unknown days stop the job durably.
Enablement
- Apply the migration through the normal production migration path. The fence is closed.
- Deploy Cron Control through the Release Desk with
meet-transcript-retentionadded toCRON_CONTROL_ENABLED_JOBS. Each run records askippedrow. - Open the fence for a dry run (database owner, SQL editor):
After the next run, report counts to the owner:
- Complete Before the first real delete, then
enable deletion with
delete_enabled = true.
Before the first real delete
Deletion is irreversible. Every item needs a recorded owner sign-off beforedelete_enabled is set:
- Legal hold: the owner confirms no legal hold or retention obligation covers in-scope workspaces. A hold needs an exclusion mechanism first; none exists today.
- Backups: record the backup/PITR retention window. Deleted rows persist in backups until that window passes, so the notice and data-protection inventory must not promise sooner erasure.
- Cost and usage history: deleting chunk rows removes their per-chunk
cost_usd,usageand attempt counts, so meeting cost statistics for purged sessions shrink. Either add a counts-only roll-up before deleting, or record that the owner accepts the loss. - Dry-run sign-off: the owner signs off the latest dry-run counts and its measured batch timing on the same function version that will delete.
- Approver log: record who flipped
fence_openanddelete_enabled, and when, inupdated_byand the release record. - Consent notice: ship the notice wording that states the 365-day retention in the same release window as enabling deletion.
- Watched first run: watch the first deleting run live, with the operator stop and the fence-close SQL ready.
Stop and rollback
- Fastest stop:
POST /control/jobs/meet-transcript-retention/stop(operator token). An in-flight batch can still commit its at most 500 rows. - Durable stop:
The next batch returns
fencedand the run endsstopped. - Remove the job id from
CRON_CONTROL_ENABLED_JOBSto stop claims entirely. apps/database/rollbacks/20261029170000_meet_transcript_retention_purge.rollback.sqldrops the purge functions and the fence. It keeps theprivate.meet_transcript_purge_runsaudit, its rows and thetranscript_purged_atmarkers.
Regression evidence
apps/database/supabase/tests/meet-transcript-retention-purge.sql: grants, closed fence, dry run, bounded batches, mid-run fence close, daily cap, newer data kept, out-of-scope data kept, idempotent re-run, abandonment, and the explicit M1-B custom deadline, NULL-as-forever, and edited-note erasure.apps/cron-control/src/meet-transcript-retention.test.ts: default off, both fences mid-run, batch and run bounds, time reserve, lost-Worker resumption, bounded retries, terminal states and five-day durable stop.