# Route Spec

## Route ID
`users-feed-list`

## Endpoint
`GET /api/v1/users/{userId}/feed`

## Human Description
Returns profile feed: original posts and reposts published by the user, plus repeatable feed and hub share events.

## Authentication
- Required: `no`
- Optional bearer token enriches viewer-specific fields such as `viewerVote`.

## Request
### Path Parameters
- `userId` (`string`, required)

### Query Parameters
- `cursor` (`string`, optional)
- `limit` (`number`, optional, default `20`, max `50`)

## Responses
### Success: `200 OK`
```json
{"success": true, "message": "User feed loaded", "data": {"items": [], "nextCursor": null}}
```

### Error: `401 Unauthorized`
When returned:
- Invalid access token supplied in the `Authorization` header.

Body:
```json
{
  "success": false,
  "error": {"code": "UNAUTHORIZED", "message": "Authentication required.", "details": {}}
}
```

## Data & Caching Dependencies
- **Spanner Tables:** `user_follows, post_share_events, post_share_targets, posts, post_main_contents, users, post_vote_counters, post_repost_counters, post_vote_receipts, post_vote_scope_receipts (Read)`
- **Redis Cache:** `active_users_day (HLL update)`
- **GCS Storage:** `None`
- **Edge Cache (CDN):** `No`

## Side Effects
- None (read-only endpoint).

## Visibility Notes
- `private_locked_hub` posts are omitted from profile feeds.
- Hub-locked content is accessed from the selected hub feed/member feed, not from the creator's public profile feed.
- The app's "share to feed" action appears here as a share event, not as a repost.
- Feed-share points to the exact post/repost frame supplied to `POST /api/v1/posts/{postId}/share/feed`; repost frames are not collapsed to their source original.
- Feed items include `viewerVote` with whether the viewer voted and when, but never the viewer's selected vote position.
- Slider feed items include `voteDistribution` only when the authenticated viewer already voted, so clients can open the detailed breakdown without a second request.
- Repost cards keep top-level `article` null and expose original article content as `sourcePost.article`.
- Repost source fallback order is `sourcePost.article`, then `sourcePost.thoughtText`, then `sourcePost.sliderText`.
- Only the current frame's top-level `sliderText`/`slider` make a repost votable; `sourcePost.sliderText`/`sourcePost.slider` are source display metadata.
