feat: social_snapshots, posts engagement columns, apify_scraper_runs lineage (app#2018)
Database PR for recoupable/app#2018 (scrape persistence keystone). Contract: recoupable/docs#316. Absorbs the database row of app#2026.
Migrations
20260827120000_create_social_snapshots.sql
social_snapshots(id, social_id FK→socials CASCADE, captured_at, captured_on DATE, follower_count, following_count, post_count),UNIQUE (social_id, captured_on)so the api can upsert one point per social per UTC day (latest scrape that day wins), index(social_id, captured_at DESC)for the history read, RLS on with no policies.- Backfill: one row per
socialsrow with a non-nullfollowerCount, stamped with the row'supdated_at, sohistorystarts today rather than at the next scrape.ON CONFLICT DO NOTHING— re-runnable.
20260827120100_posts_engagement_columns.sql
posts.views / likes / comments / repostsas nullableBIGINTwith>= 0checks. Named columns rather than a JSONB so the table can be ordered and filtered on them.- Drops the
set_updated_atBEFORE UPDATE trigger onposts. Every handler writesupdated_atas the post's platform publish timestamp (andGET /api/artists/{id}/postsorders by it); the trigger would overwrite that withnow()on the first re-scrape that refreshes engagement.
20260827120200_apify_scraper_runs_lineage.sql
origin TEXT CHECK IN ('artist','fan'),parent_run_id TEXT,account_idnullable (a spawned run inherits its parent's account; a chain whose root predates this migration has none to inherit). Partial indexes onparent_run_idand(account_id, created_at DESC)for the lineage walk and the budget PR's hourly count.
Merge order
docs#316 → this → api keystone (writes snapshots + engagement, registers spawned runs) → api budget. The api PR reads social_snapshots and the new posts columns, so this lands first; pnpm update-types in api regenerates database.types.ts after apply.
Verification
grep -c '\$\$'on all three files: 0 (no DO blocks, nothing for the shell to expand).- Backfill cardinality to be recorded on apply:
SELECT count(*) FROM social_snapshotsshould equalSELECT count(*) FROM socials WHERE "followerCount" >= 0.
🤖 Generated with Claude Code
https://claude.ai/code/session_012PS8hmiwR1rGD6c41n6gD8
Summary by cubic
Adds the database layer for social snapshot history, post-level engagement, and scraper run lineage, so the API can persist scrape results instead of throwing them away after each run.
- Creates
social_snapshotskeyed on(social_id, captured_on)for one follower-count point per social per UTC day, with a backfill so history starts today.captured_onis derived fromcaptured_atby a trigger, so callers only supply the timestamp. - Adds
views,likes,comments, andrepostscolumns topostsand drops theset_updated_attrigger that would overwrite the platform publish timestamp on re-scrape. - Adds
origin,parent_run_id, and nullableaccount_idtoapify_scraper_runsso spawned runs can be traced and counted.
Migration
- Apply order: docs#316 → this PR → API keystone → API budget; run
pnpm update-typesin the API after applying. - Verification: the
social_snapshotsbackfill count should equal the count ofsocialsrows with"followerCount" >= 0.
Summary by CodeRabbit
- New Features
- Added historical daily snapshots for social follower counts, preserving values across future updates.
- Added engagement metrics for posts, including views, likes, comments, and reposts.
- Added scraper run lineage tracking to support related runs and origin details.
- Data Integrity
- Added validation to prevent negative follower, engagement, and activity counts.
- Added automatic UTC-based dating for social snapshots.