···11+-- Newsletter mode: four new tables and drop the legacy subscribers_to_publications.
22+-- See specs/2026-04-20-newsletter-mode.md.
33+44+-- Legacy table has no live rows; its writers retired with the feature branch.
55+drop table if exists "public"."subscribers_to_publications";
66+77+88+-- publication_newsletter_settings ---------------------------------------------
99+-- Per-publication newsletter config. Separate from publications.metadata because
1010+-- reply_to_email has a real verification lifecycle and we want cheap queries for
1111+-- "newsletter-enabled publications".
1212+create table "public"."publication_newsletter_settings" (
1313+ "publication" text not null,
1414+ "enabled" boolean not null default false,
1515+ "reply_to_email" text,
1616+ "reply_to_verified_at" timestamp with time zone,
1717+ "created_at" timestamp with time zone not null default now(),
1818+ "updated_at" timestamp with time zone not null default now()
1919+);
2020+2121+alter table "public"."publication_newsletter_settings" enable row level security;
2222+2323+CREATE UNIQUE INDEX publication_newsletter_settings_pkey ON public.publication_newsletter_settings USING btree (publication);
2424+CREATE INDEX publication_newsletter_settings_enabled_idx ON public.publication_newsletter_settings USING btree (publication) WHERE enabled;
2525+2626+alter table "public"."publication_newsletter_settings" add constraint "publication_newsletter_settings_pkey" PRIMARY KEY using index "publication_newsletter_settings_pkey";
2727+2828+alter table "public"."publication_newsletter_settings" add constraint "publication_newsletter_settings_publication_fkey" FOREIGN KEY (publication) REFERENCES publications(uri) ON DELETE CASCADE not valid;
2929+alter table "public"."publication_newsletter_settings" validate constraint "publication_newsletter_settings_publication_fkey";
3030+3131+grant delete on table "public"."publication_newsletter_settings" to "anon";
3232+grant insert on table "public"."publication_newsletter_settings" to "anon";
3333+grant references on table "public"."publication_newsletter_settings" to "anon";
3434+grant select on table "public"."publication_newsletter_settings" to "anon";
3535+grant trigger on table "public"."publication_newsletter_settings" to "anon";
3636+grant truncate on table "public"."publication_newsletter_settings" to "anon";
3737+grant update on table "public"."publication_newsletter_settings" to "anon";
3838+grant delete on table "public"."publication_newsletter_settings" to "authenticated";
3939+grant insert on table "public"."publication_newsletter_settings" to "authenticated";
4040+grant references on table "public"."publication_newsletter_settings" to "authenticated";
4141+grant select on table "public"."publication_newsletter_settings" to "authenticated";
4242+grant trigger on table "public"."publication_newsletter_settings" to "authenticated";
4343+grant truncate on table "public"."publication_newsletter_settings" to "authenticated";
4444+grant update on table "public"."publication_newsletter_settings" to "authenticated";
4545+grant delete on table "public"."publication_newsletter_settings" to "service_role";
4646+grant insert on table "public"."publication_newsletter_settings" to "service_role";
4747+grant references on table "public"."publication_newsletter_settings" to "service_role";
4848+grant select on table "public"."publication_newsletter_settings" to "service_role";
4949+grant trigger on table "public"."publication_newsletter_settings" to "service_role";
5050+grant truncate on table "public"."publication_newsletter_settings" to "service_role";
5151+grant update on table "public"."publication_newsletter_settings" to "service_role";
5252+5353+5454+-- publication_email_subscribers -----------------------------------------------
5555+-- Per-publication email subscriber list. Handle subscriptions continue to live
5656+-- in publication_subscriptions; cross-flow queries union the two.
5757+create table "public"."publication_email_subscribers" (
5858+ "id" uuid not null default gen_random_uuid(),
5959+ "publication" text not null,
6060+ "email" text not null,
6161+ "identity_id" uuid,
6262+ "state" text not null default 'pending',
6363+ "confirmation_code" text,
6464+ "unsubscribe_token" uuid not null default gen_random_uuid(),
6565+ "created_at" timestamp with time zone not null default now(),
6666+ "confirmed_at" timestamp with time zone,
6767+ "unsubscribed_at" timestamp with time zone
6868+);
6969+7070+alter table "public"."publication_email_subscribers" enable row level security;
7171+7272+CREATE UNIQUE INDEX publication_email_subscribers_pkey ON public.publication_email_subscribers USING btree (id);
7373+CREATE UNIQUE INDEX publication_email_subscribers_publication_email_key ON public.publication_email_subscribers USING btree (publication, email);
7474+CREATE UNIQUE INDEX publication_email_subscribers_unsubscribe_token_key ON public.publication_email_subscribers USING btree (unsubscribe_token);
7575+CREATE INDEX publication_email_subscribers_confirmed_idx ON public.publication_email_subscribers USING btree (publication) WHERE state = 'confirmed';
7676+7777+alter table "public"."publication_email_subscribers" add constraint "publication_email_subscribers_pkey" PRIMARY KEY using index "publication_email_subscribers_pkey";
7878+alter table "public"."publication_email_subscribers" add constraint "publication_email_subscribers_publication_email_key" UNIQUE using index "publication_email_subscribers_publication_email_key";
7979+alter table "public"."publication_email_subscribers" add constraint "publication_email_subscribers_unsubscribe_token_key" UNIQUE using index "publication_email_subscribers_unsubscribe_token_key";
8080+8181+alter table "public"."publication_email_subscribers" add constraint "publication_email_subscribers_publication_fkey" FOREIGN KEY (publication) REFERENCES publications(uri) ON DELETE CASCADE not valid;
8282+alter table "public"."publication_email_subscribers" validate constraint "publication_email_subscribers_publication_fkey";
8383+8484+alter table "public"."publication_email_subscribers" add constraint "publication_email_subscribers_identity_id_fkey" FOREIGN KEY (identity_id) REFERENCES identities(id) ON DELETE SET NULL not valid;
8585+alter table "public"."publication_email_subscribers" validate constraint "publication_email_subscribers_identity_id_fkey";
8686+8787+alter table "public"."publication_email_subscribers" add constraint "publication_email_subscribers_state_check" CHECK (state IN ('pending','confirmed','unsubscribed')) not valid;
8888+alter table "public"."publication_email_subscribers" validate constraint "publication_email_subscribers_state_check";
8989+9090+grant delete on table "public"."publication_email_subscribers" to "anon";
9191+grant insert on table "public"."publication_email_subscribers" to "anon";
9292+grant references on table "public"."publication_email_subscribers" to "anon";
9393+grant select on table "public"."publication_email_subscribers" to "anon";
9494+grant trigger on table "public"."publication_email_subscribers" to "anon";
9595+grant truncate on table "public"."publication_email_subscribers" to "anon";
9696+grant update on table "public"."publication_email_subscribers" to "anon";
9797+grant delete on table "public"."publication_email_subscribers" to "authenticated";
9898+grant insert on table "public"."publication_email_subscribers" to "authenticated";
9999+grant references on table "public"."publication_email_subscribers" to "authenticated";
100100+grant select on table "public"."publication_email_subscribers" to "authenticated";
101101+grant trigger on table "public"."publication_email_subscribers" to "authenticated";
102102+grant truncate on table "public"."publication_email_subscribers" to "authenticated";
103103+grant update on table "public"."publication_email_subscribers" to "authenticated";
104104+grant delete on table "public"."publication_email_subscribers" to "service_role";
105105+grant insert on table "public"."publication_email_subscribers" to "service_role";
106106+grant references on table "public"."publication_email_subscribers" to "service_role";
107107+grant select on table "public"."publication_email_subscribers" to "service_role";
108108+grant trigger on table "public"."publication_email_subscribers" to "service_role";
109109+grant truncate on table "public"."publication_email_subscribers" to "service_role";
110110+grant update on table "public"."publication_email_subscribers" to "service_role";
111111+112112+113113+-- publication_email_subscriber_events -----------------------------------------
114114+-- Append-only event log. Timestamp columns on publication_email_subscribers
115115+-- are convenience caches derivable from this.
116116+create table "public"."publication_email_subscriber_events" (
117117+ "id" uuid not null default gen_random_uuid(),
118118+ "subscriber" uuid not null,
119119+ "publication" text not null,
120120+ "event_type" text not null,
121121+ "occurred_at" timestamp with time zone not null default now(),
122122+ "metadata" jsonb
123123+);
124124+125125+alter table "public"."publication_email_subscriber_events" enable row level security;
126126+127127+CREATE UNIQUE INDEX publication_email_subscriber_events_pkey ON public.publication_email_subscriber_events USING btree (id);
128128+CREATE INDEX publication_email_subscriber_events_subscriber_idx ON public.publication_email_subscriber_events USING btree (subscriber, occurred_at DESC);
129129+CREATE INDEX publication_email_subscriber_events_publication_type_idx ON public.publication_email_subscriber_events USING btree (publication, event_type, occurred_at DESC);
130130+131131+alter table "public"."publication_email_subscriber_events" add constraint "publication_email_subscriber_events_pkey" PRIMARY KEY using index "publication_email_subscriber_events_pkey";
132132+133133+alter table "public"."publication_email_subscriber_events" add constraint "publication_email_subscriber_events_subscriber_fkey" FOREIGN KEY (subscriber) REFERENCES publication_email_subscribers(id) ON DELETE CASCADE not valid;
134134+alter table "public"."publication_email_subscriber_events" validate constraint "publication_email_subscriber_events_subscriber_fkey";
135135+136136+alter table "public"."publication_email_subscriber_events" add constraint "publication_email_subscriber_events_publication_fkey" FOREIGN KEY (publication) REFERENCES publications(uri) ON DELETE CASCADE not valid;
137137+alter table "public"."publication_email_subscriber_events" validate constraint "publication_email_subscriber_events_publication_fkey";
138138+139139+alter table "public"."publication_email_subscriber_events" add constraint "publication_email_subscriber_events_event_type_check" CHECK (event_type IN ('subscribe_requested','confirmation_sent','confirmed','unsubscribe_requested','resubscribed','post_sent','bounce','complaint')) not valid;
140140+alter table "public"."publication_email_subscriber_events" validate constraint "publication_email_subscriber_events_event_type_check";
141141+142142+grant delete on table "public"."publication_email_subscriber_events" to "anon";
143143+grant insert on table "public"."publication_email_subscriber_events" to "anon";
144144+grant references on table "public"."publication_email_subscriber_events" to "anon";
145145+grant select on table "public"."publication_email_subscriber_events" to "anon";
146146+grant trigger on table "public"."publication_email_subscriber_events" to "anon";
147147+grant truncate on table "public"."publication_email_subscriber_events" to "anon";
148148+grant update on table "public"."publication_email_subscriber_events" to "anon";
149149+grant delete on table "public"."publication_email_subscriber_events" to "authenticated";
150150+grant insert on table "public"."publication_email_subscriber_events" to "authenticated";
151151+grant references on table "public"."publication_email_subscriber_events" to "authenticated";
152152+grant select on table "public"."publication_email_subscriber_events" to "authenticated";
153153+grant trigger on table "public"."publication_email_subscriber_events" to "authenticated";
154154+grant truncate on table "public"."publication_email_subscriber_events" to "authenticated";
155155+grant update on table "public"."publication_email_subscriber_events" to "authenticated";
156156+grant delete on table "public"."publication_email_subscriber_events" to "service_role";
157157+grant insert on table "public"."publication_email_subscriber_events" to "service_role";
158158+grant references on table "public"."publication_email_subscriber_events" to "service_role";
159159+grant select on table "public"."publication_email_subscriber_events" to "service_role";
160160+grant trigger on table "public"."publication_email_subscriber_events" to "service_role";
161161+grant truncate on table "public"."publication_email_subscriber_events" to "service_role";
162162+grant update on table "public"."publication_email_subscriber_events" to "service_role";
163163+164164+165165+-- publication_post_sends ------------------------------------------------------
166166+-- One row per (publication, document). Primary job is idempotency: publish
167167+-- inserts with ON CONFLICT DO NOTHING before firing a send.
168168+create table "public"."publication_post_sends" (
169169+ "publication" text not null,
170170+ "document" text not null,
171171+ "status" text not null default 'pending',
172172+ "subscriber_count" integer,
173173+ "started_at" timestamp with time zone not null default now(),
174174+ "completed_at" timestamp with time zone,
175175+ "error" text
176176+);
177177+178178+alter table "public"."publication_post_sends" enable row level security;
179179+180180+CREATE UNIQUE INDEX publication_post_sends_pkey ON public.publication_post_sends USING btree (publication, document);
181181+CREATE INDEX publication_post_sends_publication_started_idx ON public.publication_post_sends USING btree (publication, started_at DESC);
182182+183183+alter table "public"."publication_post_sends" add constraint "publication_post_sends_pkey" PRIMARY KEY using index "publication_post_sends_pkey";
184184+185185+alter table "public"."publication_post_sends" add constraint "publication_post_sends_publication_fkey" FOREIGN KEY (publication) REFERENCES publications(uri) ON DELETE CASCADE not valid;
186186+alter table "public"."publication_post_sends" validate constraint "publication_post_sends_publication_fkey";
187187+188188+alter table "public"."publication_post_sends" add constraint "publication_post_sends_document_fkey" FOREIGN KEY (document) REFERENCES documents(uri) ON DELETE CASCADE not valid;
189189+alter table "public"."publication_post_sends" validate constraint "publication_post_sends_document_fkey";
190190+191191+alter table "public"."publication_post_sends" add constraint "publication_post_sends_status_check" CHECK (status IN ('pending','sending','sent','failed')) not valid;
192192+alter table "public"."publication_post_sends" validate constraint "publication_post_sends_status_check";
193193+194194+grant delete on table "public"."publication_post_sends" to "anon";
195195+grant insert on table "public"."publication_post_sends" to "anon";
196196+grant references on table "public"."publication_post_sends" to "anon";
197197+grant select on table "public"."publication_post_sends" to "anon";
198198+grant trigger on table "public"."publication_post_sends" to "anon";
199199+grant truncate on table "public"."publication_post_sends" to "anon";
200200+grant update on table "public"."publication_post_sends" to "anon";
201201+grant delete on table "public"."publication_post_sends" to "authenticated";
202202+grant insert on table "public"."publication_post_sends" to "authenticated";
203203+grant references on table "public"."publication_post_sends" to "authenticated";
204204+grant select on table "public"."publication_post_sends" to "authenticated";
205205+grant trigger on table "public"."publication_post_sends" to "authenticated";
206206+grant truncate on table "public"."publication_post_sends" to "authenticated";
207207+grant update on table "public"."publication_post_sends" to "authenticated";
208208+grant delete on table "public"."publication_post_sends" to "service_role";
209209+grant insert on table "public"."publication_post_sends" to "service_role";
210210+grant references on table "public"."publication_post_sends" to "service_role";
211211+grant select on table "public"."publication_post_sends" to "service_role";
212212+grant trigger on table "public"."publication_post_sends" to "service_role";
213213+grant truncate on table "public"."publication_post_sends" to "service_role";
214214+grant update on table "public"."publication_post_sends" to "service_role";