How it works
Fan-out on write
Section titled “Fan-out on write”When a user posts an activity, it is written once to their own feed and fanned out along
the follow edges to each follower’s feed. Fan-out happens on write, not on read, so reads
stay cheap: a feed read is exactly three statements, constant in page size, no matter how
many activities exist. Pagination is keyset-based on (activity_time DESC, activity_id DESC) — there is no OFFSET anywhere on the read path. Fan-out workers are idempotent
and safe to replay.
Deletes follow the same “write path does the work” idea in reverse: activities are
soft-deleted, but the fan-out rows pointing to them are hard-deleted, so the read path
never has to filter out a deleted_at. Reaction counts are denormalized on the activity
and bumped only when a reaction write actually changes a row, so counting a reaction
never means a real-time COUNT(*).
Per-tenant isolation
Section titled “Per-tenant isolation”Each tenant gets its own database. A shared platform database holds the tenant registry — the mapping from a tenant to its own database — and the migration runner applies migrations to every tenant database individually. Because tenants live in physically separate databases, not separate rows in a shared one, reads and writes for one tenant can never cross into another’s data.
Two-service topology
Section titled “Two-service topology”- Gateway — holds all authentication, in exactly one place. It verifies the tenant’s HS256 user JWT, resolves the tenant, and never opens a tenant database.
- Feed service — owns the domain (activities, feeds, follows, reactions) and the BullMQ fan-out worker. It never sees a user JWT; it trusts the tenant and user identity the gateway forwards, authenticated by an internal shared secret between the two services.
Your own backend is the identity provider: it mints HS256 user tokens offline, signed
with its private api_secret, with no network call to dropin. dropin issues no tokens
of its own and stores no user credentials — there is no session store and no refresh
chain, because the tenant can always mint a fresh token for free.
See the guides for how these pieces show up in the SDKs, or the API reference for the underlying HTTP contract.