Engineering manualDatabase declarations
supabase-brand-studio-retention
Exact checked-in database declaration: docs/supabase-brand-studio-retention.sql
Source: docs/supabase-brand-studio-retention.sql. Declared objects: function:set_brand_studio_retention_settings_updated_at, policy:brand_studio_retention_settings_delete_own, policy:brand_studio_retention_settings_insert_own, policy:brand_studio_retention_settings_select_own, policy:brand_studio_retention_settings_update_own, table:brand_studio_retention_settings, trigger:set_brand_studio_retention_settings_updated_at. This is repository DDL, not a claim about live deployment.
All database declarations · Data structures and lineage
-- Brand Studio — per-account retention settings.
--
-- ADDITIVE. Run AFTER docs/supabase-brand-studio-pulls.sql (needs
-- brand_studio_pulls / brand_studio_brands to exist for the feature to mean
-- anything, though this table has no FK into either — see below). Nothing
-- here alters an existing table.
--
-- The gap this closes: src/local/retention.mjs's resolveRetention() has
-- always accepted a per-account override, but no column ever stored one, so
-- every account silently ran DEFAULT_RETENTION forever. An owner decision
-- already settled that retention is CUSTOMER-CONTROLLED — this table is the
-- missing storage half of that decision. The live server reads it in
-- retentionPreviewForUser/retentionEnforceForUser (scripts/brand-studio-live-server.mjs).
--
-- One row per user, upserted by the account's own retention-settings
-- endpoint. ABSENCE of a row is a meaningful, distinct state from a row that
-- happens to hold default-shaped values: "this account never set a policy" —
-- the server falls back to DEFAULT_RETENTION from src/local/retention.mjs
-- and reports that honestly via policy_source, rather than a migration
-- inventing a row (and therefore a policy_source lie: "stored" when nobody
-- ever stored anything) for every existing account.
--
-- `null` on scores_months/content_months means "keep indefinitely" — the
-- same convention retention.mjs already uses — so both must stay nullable.
-- content_months defaults to 12 (matching DEFAULT_RETENTION) so a fresh row
-- inserted with only e.g. scores_months set does not silently start the
-- customer at "keep content forever" by column-default accident.
create extension if not exists pgcrypto;
create table if not exists public.brand_studio_retention_settings (
user_id uuid primary key references auth.users(id) on delete cascade,
-- Months until scores/diffs/metadata (the trend) are purged along with the
-- whole pull. Null = kept indefinitely, matching DEFAULT_RETENTION.
scores_months integer,
-- Months until captured third-party content (screenshots, page text) is
-- stripped while the pull's scores survive. Null = kept indefinitely.
content_months integer default 12,
-- A pinned anchor baseline is exempt from both clocks above while true —
-- see retention.mjs's retentionActionFor(). Defaults on: an account has to
-- explicitly opt OUT of protecting its own comparison baseline.
anchor_exempt boolean not null default true,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint brand_studio_retention_settings_scores_months_check
check (scores_months is null or scores_months >= 0),
constraint brand_studio_retention_settings_content_months_check
check (content_months is null or content_months >= 0)
);
-- ------------------------------------------------------------------ RLS ----
alter table public.brand_studio_retention_settings enable row level security;
drop policy if exists brand_studio_retention_settings_select_own on public.brand_studio_retention_settings;
create policy brand_studio_retention_settings_select_own on public.brand_studio_retention_settings
for select using (user_id = auth.uid());
drop policy if exists brand_studio_retention_settings_insert_own on public.brand_studio_retention_settings;
create policy brand_studio_retention_settings_insert_own on public.brand_studio_retention_settings
for insert with check (user_id = auth.uid());
drop policy if exists brand_studio_retention_settings_update_own on public.brand_studio_retention_settings;
create policy brand_studio_retention_settings_update_own on public.brand_studio_retention_settings
for update using (user_id = auth.uid()) with check (user_id = auth.uid());
drop policy if exists brand_studio_retention_settings_delete_own on public.brand_studio_retention_settings;
create policy brand_studio_retention_settings_delete_own on public.brand_studio_retention_settings
for delete using (user_id = auth.uid());
-- ------------------------------------------------------------ updated_at ----
create or replace function public.set_brand_studio_retention_settings_updated_at()
returns trigger
language plpgsql
as $$
begin
new.updated_at = now();
return new;
end;
$$;
drop trigger if exists set_brand_studio_retention_settings_updated_at on public.brand_studio_retention_settings;
create trigger set_brand_studio_retention_settings_updated_at
before update on public.brand_studio_retention_settings
for each row
execute function public.set_brand_studio_retention_settings_updated_at();
-- ------------------------------------------------------------ reference ----
-- Deliberately NOT a foreign key into brand_studio_brands or
-- brand_studio_pulls: a retention policy is a property of the ACCOUNT
-- (auth.users), not of any one brand or pull, and it must remain settable
-- (and readable, so a customer can be told what happens to their data) even
-- for an account that has not created a brand yet. auth.users(id) on delete
-- cascade is the only ownership tie this table needs.