-- Migration 010: login_events records THAT a user was active (returned and read) -- on a given day — the Stage-0 signal summary_actions misses. summary_actions -- captures *acts* (watch/skip/save); a reader who logs in weekly and clicks -- nothing is otherwise invisible, yet for a reading product that return IS the -- signal the gate ("usage in >=2 distinct weeks", VISION/ADR-016) is defined on. -- -- Append-only: one row per user per active day (the request-path throttle in the -- web layer enforces that cadence), never updated. Per-user isolation like every -- user-owned table. -- -- NO foreign key to users (mirrors summary_actions, migration 002): user_id is -- carried for RLS/scoping but the table is decoupled so a stamp never blocks on a -- users row. The cost of that decoupling: the users-row cascade does NOT reach -- login_events, so DeleteUser must delete it explicitly (see account.go) — the -- exact footgun the summary_actions delete work caught. CREATE TABLE login_events ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL, seen_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- (user_id, seen_at) serves both the per-user-per-day throttle lookup -- (seen_at >= start-of-today) and the gate query's per-user week bucketing. CREATE INDEX idx_login_events_user_seen ON login_events(user_id, seen_at); -- RLS: identical GUC-keyed policy/pattern to migration 003. FORCE so the table -- owner (tapir, non-superuser in prod) is subject to it; an unset GUC yields NULL -- → no rows match → deny-all. ALTER TABLE login_events ENABLE ROW LEVEL SECURITY; ALTER TABLE login_events FORCE ROW LEVEL SECURITY; CREATE POLICY login_events_isolation ON login_events FOR ALL USING (user_id = current_setting('tapir.current_user_id', true)::uuid);