-- Tapir initial schema (Stage 0). Per-user isolation: every user-owned table -- carries user_id even though Stage 0 has a single user (docs/data-model.md). -- Scoped to Stage 0 dedup + delivery; Future B/C concerns are out of scope. CREATE TABLE users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), display_name TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- One row per (user, video) — per-user isolation, not a global deduped table -- (DECISIONS.md "Rejected alternatives"). subscription_id has no FK at Stage 0: -- the subscriptions table is not part of the store-sink slice. CREATE TABLE videos ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, subscription_id UUID, provider TEXT NOT NULL, provider_video_id TEXT NOT NULL, title TEXT, duration_s INTEGER, published_at TIMESTAMPTZ, url TEXT, seen_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT videos_user_provider_video_unique UNIQUE (user_id, provider, provider_video_id) ); CREATE INDEX idx_videos_user_id ON videos(user_id); -- At most one transcript per video. source = 'none' records "checked, no usable -- transcript" so the watcher does not reprocess (ADR-007); content is NULL then. CREATE TABLE transcripts ( video_id UUID PRIMARY KEY REFERENCES videos(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, source TEXT NOT NULL, language TEXT, content TEXT, resolved_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_transcripts_user_id ON transcripts(user_id); -- At most one summary per video (UNIQUE on (user_id, video_id)). video_id is not -- FK-constrained to videos at Stage 0: the store sink receives only a Summary -- (ports.Sink.Deliver), so the durable dedup key (user_id, video_id) stands on -- its own; video-row persistence is the engine/source's concern, deferred. CREATE TABLE summaries ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, video_id UUID NOT NULL, summary TEXT NOT NULL, highlights JSONB NOT NULL DEFAULT '[]'::jsonb, takeaways JSONB NOT NULL DEFAULT '[]'::jsonb, ai_provider TEXT, ai_model TEXT, fallback_used BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT summaries_user_video_unique UNIQUE (user_id, video_id) ); CREATE INDEX idx_summaries_user_id ON summaries(user_id); -- One row per (summary, sink) attempt. "Also sent to brain" lives here as a -- delivery row with sink = 'brain'; no brain-specific tables (data-model.md). CREATE TABLE sink_deliveries ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), summary_id UUID NOT NULL REFERENCES summaries(id) ON DELETE CASCADE, sink TEXT NOT NULL, status TEXT NOT NULL, detail TEXT, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT sink_deliveries_summary_sink_unique UNIQUE (summary_id, sink) );