MainSDK
Search⌘K
How-to

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.

Building for 0.4

Status Status is where the work is: Planned (in a milestone, not started), Building (tickets in progress), Preview (usable, may change), then the stability it ships at.

Stability The promise an interface ships with.

  • Stable Follows semantic versioning; breaking changes need a new major, 12 months of overlap and a codemod.
  • Beta Usable in production; may change before it becomes stable.
  • Experimental May change or go away; needs an explicit opt-in in main.yaml.
Stability levels
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.

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

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

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

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.

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.

Was this page helpful?

↑↓ moveEnter openEsc close

Not decided yet