Search the complete documentation

Try idempotency, proposal lifecycle, authentication, or ordering.

Navigate Open
繁體中文
Browse documentation

Activity · Timelines

Ordering, fan-out, and rebuild

Activity stores canonical posts separately from viewer timelines. Timeline entries are derived projections. They may arrive after the post write, but retries and repair converge on one entry for each viewer and post.

Publish transaction
  ├─ canonical Post
  ├─ author self entry
  ├─ initial PostStats
  ├─ fan-out job
  └─ outbox event

Fan-out worker
  └─ follower TimelineEntry rows

Private posts skip the fan-out job. The author’s self entry still exists.

Timeline order

The home timeline is reverse chronological. Activity sorts entries by:

ORDER BY ranked_at DESC, id DESC

ranked_at is copied from the source post’s published_at. The timeline entry UUID is the stable tie-breaker when multiple entries share the same timestamp.

This is a materialized chronological order, not a recommendation score. The host owns ranking policy that has not been explicitly delegated to Activity.

Cursor pagination

Request the first page with an optional limit:

GET /api/v1/timeline/home?limit=50

The public contract accepts limits from 1 to 100 and uses 50 by default. When another page exists, page.next_cursor contains an opaque URL-safe value.

{
  "data": [],
  "page": {
    "next_cursor": "eyJhdCI6IjIwMjYtMDctMTRUMDQ6MTA6MDBaIiwiaWQiOiIuLi4ifQ"
  }
}

Pass that value unchanged in the next request:

GET /api/v1/timeline/home?limit=50&cursor=<next_cursor>

Sign the exact query string sent over HTTP. Do not decode the cursor, change its padding, or use it with a different pagination contract.

An invalid cursor returns 400 invalid_cursor. A final page returns "next_cursor": null.

Stable page boundary

The cursor records both the last entry’s ordering time and UUID. The next page selects entries below that pair:

ranked_at < cursor time
or
ranked_at = cursor time and id < cursor id

This tie-break prevents entries with the same ranked_at from being repeated or skipped while walking a stable result set.

Timeline reads are not snapshots. A new post or a policy change may alter the materialized set between requests. Consumers should treat each cursor chain as a forward pagination session, not as a permanent archive position.

Fan-out convergence

The publish transaction creates the author’s self entry and schedules follower fan-out. Followers may not see the post when the publish response first returns.

Each follower projection uses an upsert backed by this database identity:

(tenant_id, viewer_id, post_id)

Duplicate jobs may run, but they update the same timeline entry instead of creating another one. Job uniqueness is not the public guarantee. Projection uniqueness is.

Before fan-out, the worker reads the current post and skips work when the post is already:

  • soft deleted
  • in a moderation state other than visible
  • private

It also uses the current active follow graph and block state when choosing viewers.

What rebuild restores

The internal repair worker rebuilds one viewer at a time. In one database transaction it:

  1. Deletes that viewer’s following entries.
  2. Reads canonical posts from currently active follows.
  3. Excludes private, deleted, hidden, blocked, and non-visible posts.
  4. Recreates one following entry per eligible post.

Rebuild leaves the viewer’s self entries intact. Recreated entries keep the source post’s published_at as ranked_at, so the normal read order is preserved.

The current rebuild primitive is an internal Oban worker. It is not exposed through the public /api/v1 contract. A host cannot request a rebuild through the customer HTTP API today.

Guarantee matrix

SituationCurrent behavior
Publish succeedsCanonical post and author self entry are committed
Follower reads immediatelyThe new entry may still be pending fan-out
Fan-out job runs twiceOne viewer-post entry remains
Post is already deleted before fan-outWorker skips materialization
Cursor is retriedRead is safe when the request is signed again
Publish times outDuplicate-safe retry is not guaranteed
Viewer projection driftsInternal rebuild can reconstruct following entries

Proof boundary

Repository tests cover same-timestamp pagination, duplicate fan-out execution, stale fan-out after deletion or moderation, private-post exclusion, and viewer rebuild. The FeedCore TLA+ model checks tenant safety, projection uniqueness, visibility safety, idempotent job effects, and eventual convergence under its modeled fairness assumptions.

That proof does not establish production fan-out latency, queue capacity, recovery time objectives, or public rebuild operations. Those remain productization work.

Next: apply visibility and host-owned policy.