---
title: "Build a feed with an end"
description: "Let people follow, block and mute, read a chronological home timeline that fans out in the background and ends with a caught-up marker, and show trending tags per region."
section: "guides"
status: "Building for 0.4"
spec: "feeds"
canonical: "https://mainsdk.dev/docs/guides/build-a-feed-with-an-end"
updated: "2026-10-28"
---

# Build a feed with an end

> Let people follow, block and mute, read a chronological home timeline that fans out in the background and ends with a caught-up marker, and show trending tags per region.

Status: Building for 0.4 · Canonical: https://mainsdk.dev/docs/guides/build-a-feed-with-an-end · Updated: 2026-10-28

You are building a small social app. People follow each other, post, and read a home timeline that stays fast whether an author has ten followers or ten million. The timeline ends: it shows where the person last stopped, pauses after a session, and never inserts items while they read. It takes about 30 minutes.

> **Note:** `feeds` is in 0.4 (spec `feeds`) and runs from the repository; it is not published to npm. Status: experimental.

## You need

- A MainSDK app with a `db` section and a model for your posts (here `posts`).
- Users that have an id. A subject is `{ id }`, and may carry `tenant` and `region`.

## 1. Declare the feed

```yaml title="main.yaml"
feeds:
  driver: db                # local keeps everything in process, for tests
  items: posts
  page: { end: true, pause_after: 200 }
  trending: { window: 1h, k: 20, min-count: 25 }
```

## 2. Follow, block and mute

```ts title="app/people.ts"
import { feeds } from "mainsdk";

await feeds.follow({ id: "ana" }, { id: "ben" });          // { state: "following" }
await feeds.mute({ id: "ana" }, { id: "ben" }, { until: "2026-12-01T00:00:00Z" });
await feeds.block({ id: "ben" }, { id: "cy" });            // deletes follows both ways
const counts = await feeds.counts({ id: "ben" });          // { followers, following }
```

## 3. Publish and read

```ts title="app/posts.ts"
import { feeds } from "mainsdk";

declare const post: { id: string };

await feeds.publish({ id: "ben" }, { id: post.id, visibility: "public" });
const home = await feeds.timeline({ id: "ana" }, { limit: 20 });
```

`publish` returns once the reference and the fan-out job are accepted. No follower's timeline is written in the request, and the author sees their own post at the top on the next read. The page ends with a `caught-up` or `pause` entry; send `continue: true` only when the person asks for more.

## 4. Show a count, not a push

Subscribe the client to `feeds.new` on `user:<id>` when you have a realtime port, and show "3 new posts". Never add the items to the list the person is reading.

## 5. Add trending

```ts
import { feeds } from "mainsdk";

feeds.count("tag:launch", { by: { id: "ana" } });
const top = await feeds.trending({ k: 10 });                // { region, window, keys }
```

Keys counted for fewer than `min-count` people are hidden. Count tags, not people.

## Check it worked

- `pnpm mainsdk check` passes with no `MAIN-FEED-0010` to `0013` findings.
- The home timeline of a follower shows the new post, and a blocked person's posts are gone from it.
- Every page you read ends with a `caught-up` or `pause` entry.

## If it fails

- `MAIN-FEED-0010`: the feed has no end, or a client requests the next page with no person acting. Keep `page.end` on and wait for a click.
- `MAIN-FEED-0011`: ranking in an app with a children's audience. Serve chronological only.
- `MAIN-FEED-0001`: one side blocks the other. That is the block working.

## Prompt for your coding agent

> Add follows, blocks, mutes and a home timeline to my app with MainSDK's `feeds` interface. Keep the timeline chronological with a caught-up end and a pause, show new posts as a count and never push them in, and count trending tags per region with a minimum group size.
