Threads is a community discussion application. A person signs in, publishes a post, replies to another person, joins a community, and finds conversations through feeds, profiles, or search.
After the first post is saved, it belongs to an author, may belong to a community, can receive replies, and appears on several screens. This walkthrough follows one conversation from creation to deletion, then covers the supporting identity, image, and search flows around it.
Creating a post and its first reply#
A new top-level post stores its text, author, creation time, and an optional community. The author and community are not copied into the post. The record stores their identifiers so the application can load the current name and image when it displays the conversation.
That stored identifier is a reference: one record points to another record that owns the related information. MongoDB holds these document-like records, and the server follows the required references when it prepares a feed or profile page.
A reply uses the same record shape as a post, with one addition: it stores the identifier of the post it answers. The parent also keeps the reply's identifier in its list of children. A top-level post has no parent; a reply does.
Using one shape means a reply can receive another reply without a separate comment model. The conversation becomes a tree: it begins at one root post and branches each time someone answers a post or an existing reply.
View diagram source
flowchart TB
A["Author publishes a top-level post"] --> P["Post with no parent"]
P --> R1["Reply with P as its parent"]
P --> R2["Another reply with P as its parent"]
R1 --> R3["Reply with R1 as its parent"]
P -. "also appears on" .-> F["Home feed"]
P -. "also appears on" .-> U["Author profile"]
P -. "also appears on" .-> C["Community page"]Showing the same conversation on several screens#
After a post is created, its identifier is added to the author's thread list and, when relevant, the community's thread list. Those reverse lists make common screens direct to load: a profile already knows which posts belong to that person, and a community already knows which posts belong inside it.
The full post text still lives in the post record. The profile and community keep identifiers, not separate copies of the conversation. When a screen is requested, the server loads the referenced posts and the author details needed to display them.
This is convenient for reading, but it creates more work when data changes. The same post identifier may now exist in the post tree, the author's list, and a community's list. All three must continue to describe the same reality.
What deletion must clean up#
Suppose an author deletes the root post in the diagram above. Removing only that one record would leave three replies whose parent no longer exists. The author's and community's thread lists would also continue pointing at a missing post.
The delete path first walks through every child below the selected post. It uses recursion, which means the same deletion logic runs for a child and then for that child's children until there is nothing further to visit. That matches the shape of the conversation: every reply can be treated as the root of a smaller reply tree.
After removing the descendants, the code removes the root identifier from the author and community records. Several database writes have to agree for the deletion to be complete.
This version performs those writes in application code without wrapping the entire operation in one database transaction. A failure halfway through can therefore leave stale identifiers or an incomplete tree. A production rebuild would either move the related writes into a transaction or store fewer reverse lists and derive more of them from one source.
View diagram source
flowchart TB
D["Delete the root post"] --> L["Load its direct replies"]
L --> W["Repeat the same walk for every reply"]
W --> X["Delete descendants"]
X --> R["Delete the root post"]
R --> A["Remove its ID from the author"]
A --> C["Remove its ID from the community"]Updating membership when nobody is on the site#
Threads used Clerk for sign-in and organizations. In the interface, a Clerk organization acted as a community. A person could join or leave one through Clerk, so the local MongoDB records needed to receive that change even if no Threads page was open at the time.
Clerk reported those events to a server route using a webhook—an HTTP request one system sends to another when something changes. Threads handled community creation, edits, deletion, and membership changes, then updated the matching local community and user records.
An internet request is not trustworthy merely because it reaches the correct route. Clerk's webhook requests include a cryptographic signature. The route verified that signature through Svix before changing any membership data.
This version does not record processed event identifiers or periodically compare Clerk with the local database. A stronger version would add both so a repeated webhook is harmless and a missed event can be repaired.
Images, search, and pagination#
Profiles and communities can include images. The browser uploaded those files through UploadThing, while MongoDB stored only the returned URL. This keeps large file bytes out of the records used for feeds and conversations. The upload route also required a signed-in user before accepting a file.
Search queried user names or community text with case-insensitive matching. Results were returned in pages rather than as one unbounded list. Pagination means requesting a limited slice—such as the next twenty results—so the response does not grow with the entire collection.
For this project size, that query kept search small and direct. With more data, I would add indexes based on measured query patterns before considering a separate search service.
Before adding more features#
The core conversation shape can stay: one record for posts and replies, parent-child links for nesting, and references for authors and communities. The work before adding more features would be at the boundaries.
Every create, edit, and delete route should verify permission on the server. Multi-record changes should be transactional or have a repair path. Webhook processing should tolerate duplicates and missed delivery. Deleting a user or community needs an explicit policy for its posts, replies, memberships, and uploaded files. Public discussion also needs reporting, moderation, and abuse controls.
The resulting lifecycle is concrete: creation adds links, reading follows them, outside identity events update them, and deletion has to remove them without leaving the database in disagreement.
