Web AppQ1 2023Solo build
Chitter
A mini social app that lets users sign up, log in and post whatever is on their mind.

The build
A compact study in feed modelling — post, follow, unfollow, like.
Problem
A timeline looks trivial until you write the query. "Show me posts from people I follow, newest first" is one sentence in English and the single hardest read in a small social app — it spans two collections, it has no natural bound, and it runs on every page load.
The point of Chitter was to build exactly that, small enough to see clearly.
Constraints
- MongoDB, no joins of consequence. The document model doesn't hand you a foreign key and an index the way a relational database does. Whatever the feed cost, it was going to be paid explicitly.
- A study, not a product. Built to understand the modelling problem. That constraint is what kept it to four features instead of forty.
Approach
Three collections: users, posts, and the follow edge. The feed reads the edge first, then the posts:
// Fan-out on read: who do I follow, then what did they post.
const following = await Follow
.find({ follower: userId })
.distinct("following");
const feed = await Post
.find({ author: { $in: following } })
.sort({ createdAt: -1 })
.limit(30)
.lean();Likes are their own small collection rather than an array on the post — an array mutated by every reader is a write-contention problem waiting to happen, and it makes "did I like this" a scan.
Tradeoffs
- Fan-out on read over fan-out on write. Reading is two queries and always current; writing is one insert. The alternative — pushing each post into every follower's precomputed timeline — makes reads instant and writes expensive, and goes wrong the moment someone with many followers posts. At this size, reading was obviously right; at scale it obviously isn't.
- Follows as their own collection over an array on the user. More documents, but unbounded growth doesn't live inside a single document, and both directions of the relationship stay queryable.
- Server-rendered EJS. Consistent with everything else I'd built, at the cost of a timeline that reloads instead of updating in place.
Outcome
Four features, working: post, follow, unfollow, like. The value wasn't the app — it was learning where a document database makes you pay for what a relational one gives away, and being able to say specifically which query it is.
Decisions
$inover an aggregation pipeline with$lookup. Chosen because two simple queries are easier to reason about and index than one clever one. Against it: two round trips, and a$inlist that grows with the follow count.- A separate likes collection over a counter on the post. Chosen so "has this user liked this" is an indexed lookup rather than an array scan. Against it: showing a like count now needs a count query or a denormalised field that has to be kept honest.
- Not building notifications. Chosen to keep the study on the feed. Against it: notifications are where the interesting write-amplification problems live, so the project stops just short of the harder half.
Stack
- Node.js
- Express.js
- MongoDB
- EJS
- JavaScript
- Fomantic UI
Related projects

Lecture Lense
Turns any recording into searchable, timestamp-cited notes.

Heckfree
Users get a public profile that showcases all of their links in one place.

Coscholars
An Ed-Tech platform built end to end during my internship at Coscholars.