See the best posts from any Bluesky account
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Capture quote engagement data in v1

Quote post tracking moves from "schema-only reservation" to "captured in
the data layer but not surfaced in the UI." snapshot_quotes is now
populated from getPosts.quoteCount (free — already in the API response),
and the worker inspects post embeds to detect quotes targeting tracked
authors and append kind='quote' rows to engagement_events. This avoids
the irrecoverable loss of per-event quote history that would otherwise
occur between v1 and v2, since Jetstream cannot be replayed past its
~few-day retention window.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+29 -7
+29 -7
docs/superpowers/specs/2026-04-11-skystar-bluesky-design.md
··· 28 28 ### Explicitly out of scope for v1 29 29 30 30 - Authentication, accounts, or any login flow. 31 - - Quote-post tracking. The schema reserves space for it; no code touches it. 31 + - Quote-post tracking in the UI. (Quote data **is** captured in the data 32 + layer from v1 onward — both `snapshot_quotes` from `getPosts` and live 33 + `kind='quote'` events from Jetstream — but no v1 route, query, or UI 34 + element exposes it.) 32 35 - Global leaderboards ("today's best", "this week's best"). 33 36 - Per-user trophies, awards, or notifications. 34 37 - Email, RSS, or any push surface. ··· 223 226 post_created_at DateTime64(6), 224 227 snapshot_likes UInt32, 225 228 snapshot_reposts UInt32, 226 - snapshot_quotes UInt32, -- always 0 in v1, reserved for v2 229 + snapshot_quotes UInt32, -- populated from getPosts.quoteCount 227 230 snapshot_taken_at DateTime64(6), -- per-post watermark 228 231 is_deleted UInt8 DEFAULT 0 229 232 ) ENGINE = ReplacingMergeTree(snapshot_taken_at) ··· 247 250 post_author_did String, -- denormalized for fast author filter 248 251 actor_did String, 249 252 rkey String, -- the engagement record's rkey 250 - kind LowCardinality(String), -- 'like' | 'repost' (| 'quote' v2) 253 + kind LowCardinality(String), -- 'like' | 'repost' | 'quote' 251 254 event_created_at DateTime64(6), -- when the actor created the engagement 252 255 ingested_at DateTime64(6) DEFAULT now64(6) 253 256 ) ENGINE = ReplacingMergeTree(ingested_at) ··· 375 378 376 379 For `app.bsky.feed.post` events: if the post's author is tracked, insert a 377 380 row into `post_snapshots` with `snapshot_likes=0, snapshot_reposts=0, 378 - snapshot_taken_at=now()`, so future engagement on it has somewhere to live. 381 + snapshot_quotes=0, snapshot_taken_at=now()`, so future engagement on it has 382 + somewhere to live. 383 + 384 + Additionally, for every `app.bsky.feed.post` event (regardless of whether 385 + the post's author is tracked), inspect `record.embed`. If it is an 386 + `app.bsky.embed.record` or `app.bsky.embed.recordWithMedia`, parse the 387 + embedded post's URI and extract its author DID. If that author is in the 388 + tracked set, insert a row into `engagement_events` with `kind='quote'`, 389 + `post_uri = embedded post URI`, `actor_did = quoter's DID`. Quote events 390 + are stored from v1 forward but not surfaced in the v1 UI — the data 391 + accumulates silently until quote support ships in a future version. 392 + This avoids the irrecoverable loss of per-event quote history that would 393 + otherwise happen between v1 and v2 (Jetstream cannot be replayed beyond 394 + its ~few-day retention window). 379 395 380 396 For `app.bsky.feed.post` *delete* events on tracked authors: write a 381 397 tombstone row to `post_snapshots` with `is_deleted=1` and a fresh ··· 429 445 chronological order, ~100 posts per page. 430 446 b. For each batch of 25 URIs, `getPosts(uris)` returns aggregate counts. 431 447 c. Insert one row per post into `post_snapshots` with 432 - `snapshot_taken_at = now()` recorded *at the moment that batch's 433 - response landed*. 448 + `snapshot_likes`, `snapshot_reposts`, and `snapshot_quotes` copied 449 + directly from the `getPosts` response, and `snapshot_taken_at = 450 + now()` recorded *at the moment that batch's response landed*. 434 451 d. Update `backfill_jobs.fetched_posts` for the loading page. 435 452 e. Continue until cursor exhausted or 10,000 posts reached 436 453 (`BACKFILL_MAX_POSTS`). ··· 626 643 custom domain detection, invalid handle rejection. 627 644 - The worker's filter logic — given an event and a tracked-DID set, does 628 645 it correctly keep or drop. 646 + - The worker's quote-detection logic — given a post event, correctly 647 + identifies `app.bsky.embed.record` and `app.bsky.embed.recordWithMedia` 648 + embeds, parses the embedded post's author DID, ignores other embed 649 + types, and only emits `kind='quote'` engagement rows for tracked 650 + embedded authors. 629 651 - Date parsing for `?after=`. 630 652 631 653 ### Integration tests (Japa with real services) ··· 777 799 To preserve focus, the following are explicitly **not** built in v1: 778 800 779 801 - Authentication, accounts, sessions. 780 - - Quote-post tracking (schema reserves space, no code). 802 + - Quote-post tracking in the UI (data is captured in v1 but not surfaced). 781 803 - Global leaderboards, daily/weekly "best of" pages. 782 804 - Trophies, awards, notifications. 783 805 - Email, RSS, push.