Feeds
Every feed is a flat, reverse-chronological timeline of activities — no ranking, no
aggregation. A feed read is exactly three statements on the backend, constant in page
size, no matter how many activities exist (spec §6). Feeds are addressed by
(group, id), e.g. timeline:maya or user:maya.
There is no “create feed” call. A feed is just an address — naming one in a read, a
write or a follow is all it takes to exist. Reading a feed nobody has posted to returns
an empty page, not a 404. group is yours to choose; the three conventional ones are
user (someone’s own posts), timeline (the feed aggregating who they follow), and
flat (a shared feed like flat:explore).
There are no private feeds
Section titled “There are no private feeds”A feed has no read ACL, and posting is publishing. Anyone holding a valid token for your tenant can read any feed by name, and every follower of a feed sees everything that fans out into it. There is no visibility flag on an activity, no per-feed permission, and no way to un-publish something except deleting it — after it has already reached every timeline that follows.
That is deliberate (spec §1), and it has one consequence worth designing around:
The producer is the only gate. If something must not be seen, do not post it.
The trap is that the decision lives in your code, at a moment that doesn’t feel like a
publishing decision — a database trigger firing on a row write. A private group’s match,
a hidden listing, a draft: if your app already has a predicate for “this one isn’t
public”, it has to run before addActivity, not in the UI that renders the card. A
reader filter is not a gate; the data has already left.
The same applies to objects: an object is resolved into the sidecar for anyone who can read an activity that refs it, so a private session’s title and score are public the moment a public activity points at them.
If some content is genuinely private, keep it out of dropin and render it from your own database, alongside the feed rather than inside it.
One exception, and it is not addressable by feed name:
notifications are owner-scoped. A user token reads only its
own; a server token must name the owner it is acting for. Everything reachable as
group:id is tenant-open.
Read a feed
Section titled “Read a feed”import { DropInClient } from '@dropinnodex/client'
const client = new DropInClient({ url, apiKey, tokenProvider })
const page = await client.feed('user', 'maya').get({ limit: 20 })page.results // Activity[] — newest firstpage.next // opaque cursor, or null when there's no moreimport { useFeed } from '@dropinnodex/react'
function Feed() { const { activities, isLoadingInitial, error, hasNext, loadNext, refresh } = useFeed('user', 'maya') // activities: Activity[], hasNext: boolean, loadNext(): fetch the next page and append // Gate on isLoadingInitial, not isLoading — isLoading is also true during loadNext, // which would blank the list mid-scroll. See /guides/pagination/. if (isLoadingInitial) return <p>Loading…</p> return <ul>{activities.map((a) => <li key={a.id}>{a.verb}</li>)}</ul>}import { DropInServer } from '@dropinnodex/server'
const server = new DropInServer({ tenantId, apiKey, apiSecret, url })
const page = await server.feed('user', 'maya').get({ limit: 20 })Read a user feed
Section titled “Read a user feed”user:<id> is a single user’s own posts. The client and react hooks expose sugar for it;
DropInServer only has the generic feed(group, id) — there’s no userFeed shortcut
server-side.
// client.userFeed(id) is sugar for client.feed('user', id)const page = await client.userFeed('maya').get({ limit: 20 })import { useUserFeed } from '@dropinnodex/react'
const { activities, isLoading, hasNext, loadNext } = useUserFeed('maya')// No userFeed() sugar on DropInServer — use the generic feed() with group 'user'.const page = await server.feed('user', 'maya').get({ limit: 20 })Read the following timeline
Section titled “Read the following timeline”timeline:<id> aggregates activities from every feed <id> follows (spec §1) — the
“who I follow” feed. Same sugar pattern as userFeed.
// client.timeline(id) is sugar for client.feed('timeline', id)const page = await client.timeline('maya').get({ limit: 20 })import { useTimeline } from '@dropinnodex/react'
const { activities, isLoading, hasNext, loadNext } = useTimeline('maya')const page = await server.feed('timeline', 'maya').get({ limit: 20 })Read a flat/explore feed
Section titled “Read a flat/explore feed”There’s no special “explore” concept in the SDK — it’s an ordinary feed, conventionally
named e.g. flat:explore, that other feeds follow so their posts fan out into one global
timeline (this is exactly how the showcase app’s Explore tab works — see
apps/showcase/lib/seed.ts).
const page = await client.feed('flat', 'explore').get({ limit: 20 })import { useFeed } from '@dropinnodex/react'
const { activities, isLoading, hasNext, loadNext } = useFeed('flat', 'explore')const page = await server.feed('flat', 'explore').get({ limit: 20 })Related
Section titled “Related”- Activities — posting into a feed.
- Pagination — how
nextandloadNext()work across pages. - Follow — the edges that decide what a timeline aggregates.
- Notifications — the separate flat feed for follows and reactions aimed at a user.
- How it works — fan-out on write, and why a feed read is three statements regardless of page size.
- Promoted activities — content served alongside a feed’s first page, outside the follow graph and outside the cursor.