Most feeds have one notion of time: newest first, forever, until the scroll gives out. Snakken's product idea is that a neighbourhood has two kinds of time — the moment (someone has moving boxes to give away, the bar shows the match tonight) and the permanent (the café, the bookshop, the Tuesday run club). Both belong in one feed, but they have opposite lifecycles, and pretending otherwise is how feeds rot.
Two lifecycles, one stream
Under the hood these are different entity families with different contracts:
- Ephemeral content (posts, events) is born with an expiry. A post about spare moving boxes carries a TTL of hours; an event expires when it is over. Expiry is not a soft-delete flag the UI politely ignores — expired content is gone from the product, and eventually gone from storage.
- Permanent content (places, groups) has no expiry, but it has freshness: a place can attach a "today" note that itself is ephemeral. The café is forever; today's apple pie is not.
The feed shows everything visible in your neighbourhood cell, where "visible" means not expired. Ordering is built from local relevance and nothing else. Recency dominates — newer simply wins most of the time — mildly adjusted by how close a post is to expiry, a deliberately capped dose of neighbourhood engagement, and whether something comes from your own cell rather than the one next door. When there is little to rank, the feed is simply chronological.
What is never weighed is what keeps you scrolling: there is no engagement-maximisation target anywhere in the system, no personalisation profile feeding the order, nothing that learns what makes you linger. Our one-line summary in the design doc: geography is the algorithm.
The reaper
Expiry needs an enforcer. Ours is a periodic job — internally, unceremoniously, the reaper — that hard-deletes content past its TTL. Two design points earned their keep:
- Expiry is enforced at read time too. The reaper runs on an interval, so there is always a window where something is expired but not yet deleted. Every read path filters on expiry regardless. The reaper is garbage collection, not the source of truth.
- Deletion is the default, retention is the exception. Going in, we expected to find reasons to keep expired content "for analytics". We found the opposite: not having an archive of everyone's past notes is a privacy property we can state plainly — and one less dataset to defend.
- Reported content outlives its timer. The one named exception: a post under an open report is not reaped. It vanishes from the product like any expired post — the read-time filter does not care — but the record is kept until moderation has resolved the case, because deleting evidence on a timer would turn every abuse report into a race. The retention is scoped to the report; it is not a backdoor archive.
Groups are events in disguise
The most useful modelling decision came from the product side: Snakken groups have no group wall. A group is a membership plus its next event — the run club is "Tuesday, 7 am, the canal", not a second feed to scroll. Technically that means groups generate ephemeral content (their events) instead of accumulating permanent content. The recurring thing is the rhythm, not the archive.
That single decision deleted an entire class of problems we never had to solve: group feed moderation, group content retention, group archive privacy. The cheapest code is the code whose feature you talked out of existence.
// Published under CC BY 4.0 — take the patterns, cite the source. · ← All articles