Posts Module
Location: apps/api/src/modules/posts/
Aggregate roots: Post, PostReply, PostReaction, PostBookmark, PostInterest, PostCoAuthor, PostSeries, PostBoost
Post Formats
| Format | Description |
|---|---|
note | Short-form text (Twitter-style), default |
essay | Long-form article with title |
photo | Image with optional caption |
video | Video with optional caption |
quote | Quote from another source with commentary |
thread | Multi-part connected note |
repost | Bare share of another post (empty body) |
Quotes and reposts reference another post via Post.quotedPostId.
Content Depth
Authors self-rate their post's technical depth on a 0–5 scale:
| Level | Label | Meaning |
|---|---|---|
| 0 | Surface | Casual, conversational |
| 1 | Intro | Introductory coverage |
| 2 | Deep | Substantive exploration |
| 3 | Expert | Expert-level analysis |
| 4 | Research | Research-grade content |
| 5 | Thesis | Original thesis / academic |
Readers can filter feed by minimum depth.
Visibility
| Value | Who Can See |
|---|---|
public | Everyone (including unauthenticated in future) |
circles | Users who are in at least one of the author's circles |
inner | Users in the author's inner circle specifically |
Multi-Interest Tagging
Posts have one primary interest (Post.interestId) plus up to 4 additional interests via PostInterest junction table.
PostInterest.isPrimary = truemirrors the primary tag- Feed and trending queries use
Post.interestIdfor performance - Cross-interest discovery uses
PostInterest
#Journalism Tag
Posts can be tagged as journalism (original reporting, factual news, investigative work).
Gate: Author must have reputation ≥ 20 in any single interest (canPostJournalism capability from AccessMatrixService). Admin override available.
Effects:
Post.isJournalism = true- Journalism badge shown on post card in mobile and admin
- Filterable via
GET /feed?journalism=true
Co-Authorship
A post's primary author can invite collaborators. The PostCoAuthor table manages the invitation lifecycle.
Flow
sequenceDiagram
Author->>API: POST /posts/:id/co-authors {userId, splitShare}
API-->>Collaborator: notification (co-author.invited)
Collaborator->>API: POST /posts/:id/co-authors/:inviteId/respond {accept: true}
Note over API: Validate total splitShare ≤ 90
API-->>Author: notification (co-author.accepted)Split Rules
splitShareis 0–100 (percentage)- Server enforces: sum of all accepted
splitSharevalues ≤ 90 - Primary author always receives at least 10 % of each EmojiPay payment
- Only accepted rows participate in mana splits
Replies
Each reply has a kind that enables richer threading:
| Kind | Purpose |
|---|---|
reply | Plain reply (default) |
reaction | Short emotional response |
criticism | Counter-argument (surfaced separately in UI) |
contribution | Substantive addition (accrues more reputation for replier) |
Series
Authors can group posts into named series (e.g. "Introduction to Stoicism — 5 parts").
PostSeries: title, description, authored by one userPostSeriesItem: join table with explicitposition(1-based)SeriesFollow: users subscribe to series and get notified on new parts- Max one position per series (unique constraint
(series_id, position))
Scheduled Publish
Set publishAt to a future timestamp. The scheduled-publish worker checks every minute and:
- Emits the post to the feed
- Sets
scheduledEmittedAtto prevent double-emission on worker restart
Counters
Materialised counters on Post are updated synchronously on each action:
| Counter | Incremented By |
|---|---|
reactions | PostReaction with kind = 'heart' |
replies | PostReply create |
shares | format = 'repost' create |
bookmarks | PostBookmark create |
views | Fire-and-forget view increment (non-author opens) |
Moderation
Post.hiddenAt(non-null) = moderator hide. Post is invisible in feed but kept for audit.Post.deletedAt(non-null) = author soft-delete. Also hidden from feed.- Both are independent — a moderated-hidden post can still be author-deleted.
API (key endpoints)
| Method | Path | Description |
|---|---|---|
| GET | /feed | Home feed |
| GET | /feed?tab=following | Following tab |
| GET | /feed?tab=trending | Trending tab |
| GET | /feed?tab=circles | Circles-only tab |
| GET | /feed?journalism=true | Journalism filter |
| POST | /posts | Create post |
| GET | /posts/:id | Post detail with replies + emojiBreakdown |
| PATCH | /posts/:id | Edit (author only) |
| DELETE | /posts/:id | Soft-delete |
| POST | /posts/:id/replies | Add reply |
| GET | /posts/:id/replies | Paginated replies |
| POST | /posts/:id/react | React |
| POST | /posts/:id/bookmark | Bookmark |
| POST | /posts/:id/repost | Repost or quote |
| POST | /posts/:id/pin | Pin to profile (max 3) |
| POST | /posts/:id/boost | Boost post |
| GET | /posts/by/:username | User's profile posts |
| GET | /posts/series | List my series |
| POST | /posts/series | Create series |
| POST | /posts/series/:id/items | Add post to series |
| GET | /posts/bookmarks | My bookmarked posts |