-- ============================================================================ -- 0005_surface_metrics.sql -- ---------------------------------------------------------------------------- -- Per-surface raw metrics snapshots. Insert-only on each ingestion (no -- UPDATE); deduplicate on read via DISTINCT ON (window). -- -- Adapters write rows; analytics-dashboard panels read via DISTINCT ON to -- pick the most-recently-fetched snapshot per (user, surface, kind, window). -- -- Spec source: _engineering-surface-metrics.md §3. -- ============================================================================ CREATE TYPE surface_metric_kind AS ENUM ( -- Discovery funnel 'profile_view', 'search_impression', 'search_rank', 'click_through', -- Engagement funnel 'dm_inbound', 'dm_outbound', 'reply_rate', -- Conversion funnel 'subscription_new', 'subscription_total', 'tip_amount', 'tip_count', 'booking_inquiry', 'booking_confirmed', -- Revenue 'gross_revenue', 'net_revenue' ); CREATE TYPE surface_metric_source AS ENUM ( 'native_api', -- surface exposed an API endpoint 'native_scrape', -- adapter scraped the surface's own analytics page 'derived', -- computed from other rows (e.g. reply_rate from dm_in/dm_out) 'manual' -- Quinn entered manually ); CREATE TABLE surface_metrics ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, org_id UUID NULL REFERENCES orgs(id) ON DELETE CASCADE, surface TEXT NOT NULL, -- surface_kind value metric_kind surface_metric_kind NOT NULL, window_start TIMESTAMPTZ NOT NULL, window_end TIMESTAMPTZ NOT NULL, value_numeric NUMERIC(18,4), -- counts, sums, rates value_text TEXT, -- rank labels ("Boosted"), categorical currency TEXT, -- ISO 4217 when monetary source surface_metric_source NOT NULL, fetched_at TIMESTAMPTZ NOT NULL DEFAULT now(), payload_jsonb JSONB, -- raw surface-side response for audit created_at TIMESTAMPTZ NOT NULL DEFAULT now(), CONSTRAINT surface_metrics_window_chk CHECK (window_end > window_start), CONSTRAINT surface_metrics_value_chk CHECK (value_numeric IS NOT NULL OR value_text IS NOT NULL), CONSTRAINT surface_metrics_currency_chk CHECK ( currency IS NULL OR (currency ~ '^[A-Z]{3}$' AND value_numeric IS NOT NULL) ) ); CREATE INDEX idx_surface_metrics_user_surface_kind_window ON surface_metrics (user_id, surface, metric_kind, window_end DESC); CREATE INDEX idx_surface_metrics_org_surface_kind_window ON surface_metrics (org_id, surface, metric_kind, window_end DESC) WHERE org_id IS NOT NULL; -- Dedup read: DISTINCT ON (surface, metric_kind, window_start, window_end) -- ordered by fetched_at DESC picks the latest snapshot per window. CREATE INDEX idx_surface_metrics_dedup ON surface_metrics (user_id, surface, metric_kind, window_start, window_end, fetched_at DESC); ALTER TABLE surface_metrics ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation_surface_metrics ON surface_metrics USING ( user_id = current_setting('app.user_id', true)::uuid OR (org_id IS NOT NULL AND org_id = current_setting('app.org_id', true)::uuid) ) WITH CHECK ( user_id = current_setting('app.user_id', true)::uuid OR (org_id IS NOT NULL AND org_id = current_setting('app.org_id', true)::uuid) ); COMMENT ON TABLE surface_metrics IS 'Per-surface metric snapshots. Insert-only; reads use DISTINCT ON to dedup overlapping windows. Tier-gated surfaces (e.g. Tryst Basic) simply contribute no rows — UI shows "—" not "0".';