Stage-0 usage measurement (VISION/ADR-016): summary_actions captures acts
(watch/skip/save) but not reads. A reader who logs in weekly and clicks
nothing is invisible — for a reading product that return is the signal the
gate ("usage in >=2 distinct weeks") is defined on. login_events records
THAT a user was active, append-only, one row per user per active day.
Per-user isolation via the same GUC-keyed FORCE RLS policy as migration 003.
No FK to users (mirrors summary_actions) — the cascade footgun is handled by
DeleteUser in a later commit. Adds an up/down reversibility test and registers
the table in both truncate helpers.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
34 lines
1.8 KiB
SQL
34 lines
1.8 KiB
SQL
-- 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);
|