···9292| Timeline item | `u#<readerDid>#timeline` | `<publishedAt>#<rkey>` | — | — | Fan-in/fan-out items written when a subscription is created or a new post is published. Deleted (all items for that author) when the subscription is removed. Expires via `ttl` (30 days). Carries `description` and `textContent` (max 300 chars) from the source post so the following feed can render excerpts without a secondary lookup. |
9393| Subscriber count | `u#<did>` | `count` | — | — | `subscribers` attribute incremented on each new subscription. Counted per DID, not per publication — if a reader subscribes to two publications by the same author, the count increments twice. Used by the API to identify celebrity authors (threshold: 1000) who bypass fan-out in favour of live post queries at read time. |
9494| User info | `u#<did>` | `info` | — | — | Written on first sign-in via `POST /users`. Attributes: `entityType` (`user-info`), `acceptedTermsAt` (ISO timestamp, set once), `updatedAt`. |
9595+| Suggestion | `u#<did>#sugg` | `pub#<rkey>` | — | — | One item per suggested publication. `show` (boolean) controls visibility: `true` = active suggestion, `false` = dismissed tombstone. Attributes: `pubAtUri`, `authorDid`, `pubName`, `pubUrl`, `pubDescription`. Written by `POST /suggestions`; updated to `show=false` by `PATCH /suggestions`. Never deleted — tombstones prevent re-adding dismissed or subscribed-to publications on future "Find suggestions" runs. |
9596| iframely cache | `iframely#cache` | SHA-256 hex of the embed URL | — | — | Expires via `ttl` attribute (DynamoDB TTL enabled) |
96979798Global secondary index `FeedIndex` (GSI1PK=`blog-post#published`, GSI1SK=`<publishedAt>#<rkey>`) enables a time-ordered global feed across all users.
···155156### Unsubscribe
156157157158Clicking "Subscribed" on a card calls `DELETE /subscriptions/:rkey` on the API and then calls `removeSubscription(rkey)` on the context, which filters the record out of the shared `subscriptions` array. The card is also removed from the local `pubs` state. The `initializedRef` guard ensures the removed subscription does not re-trigger the initial fetch loop.
159159+160160+## Suggestions page
161161+162162+The `/suggestions` route (`app/routes/suggestions.tsx`) surfaces publications from people the signed-in user follows on Bluesky, helping them discover content to subscribe to.
163163+164164+### Data model
165165+166166+Suggestions are stored in DynamoDB under `PK=u#<did>#sugg`, `SK=pub#<rkey>` (where `rkey` is the rkey of the publication on the author's PDS). The `show` boolean controls whether an item appears in the list:
167167+168168+- `show=true` — active suggestion, returned by `GET /suggestions`
169169+- `show=false` — dismissed tombstone, excluded from query results but kept in the table to prevent the same publication from being re-added on future "Find suggestions" runs
170170+171171+Records are never deleted; the tombstone is the permanent record of a user's decision to dismiss or subscribe.
172172+173173+### "Find suggestions" flow
174174+175175+1. Fetch all Bluesky follows for the signed-in user via `app.bsky.graph.getFollows` on the public Bluesky API (paginated until exhausted).
176176+2. For each followed DID, call `GET /publications?did=<did>` to look up their publications in the Lemma DynamoDB index (5 concurrent requests at a time).
177177+3. Filter out the user's own publications and any pubs they are already subscribed to (checked client-side against `subscriptions` from `DataContext`).
178178+4. Bulk-upsert candidates in batches of 100 via `POST /suggestions`. Each item is written with `ConditionExpression: 'attribute_not_exists(PK)'` so already-present items (including `show=false` tombstones) are silently skipped — the `added` count in the response reflects only net-new items.
179179+5. Reload the suggestions list from the API and update `DataContext`.
180180+181181+### Filtering
182182+183183+Two layers of filtering keep the list clean:
184184+185185+- **Server-side** — `GET /suggestions` queries only items where `show=true` (via `FilterExpression`), paginating through all DynamoDB pages to avoid under-fetching.
186186+- **Client-side** — items whose `pubAtUri` matches any entry in the user's `subscriptions` list are hidden at render time, catching publications subscribed to outside the suggestions page without waiting for a server round-trip.
187187+188188+### Subscribe and dismiss
189189+190190+- **Subscribe** — calls `POST /subscriptions` (writes the subscription to the PDS and to DynamoDB), then immediately calls `PATCH /suggestions` to set `show=false` for that item. The card is removed from `DataContext` state at the same time so it disappears from the list without a reload. This prevents the subscribed publication from reappearing on the next "Find suggestions" run.
191191+- **Dismiss (X button)** — calls `PATCH /suggestions` directly to set `show=false` and removes the item from local state.
192192+193193+### Stale index entries
194194+195195+Suggestions are sourced from the Lemma DynamoDB index, not the live PDS. If a publication has been deleted from the PDS but the indexer missed the delete event (e.g. the indexer was down beyond the Jetstream 72-hour replay window), the stale DynamoDB record will still surface as a suggestion candidate. Such orphaned records must be removed manually by verifying the record against `com.atproto.repo.getRecord` on the author's PDS and then deleting the DynamoDB item.
196196+197197+### State caching
198198+199199+The loaded suggestions list is stored in `DataContext` (`suggestions: SuggestionItem[] | undefined`). The initial fetch fires once per session on first visit; subsequent navigation to `/suggestions` skips the fetch and renders the cached list instantly. All items are loaded in a single API call; infinite scroll reveals them 20 at a time client-side using an `IntersectionObserver` on a sentinel element.
158200159201## Jetstream indexer
160202