Content & Feeds
Posts, comments, reactions, feeds, impressions, and media upload. All endpoints require authentication.
Feeds
| Method | Path | Purpose |
|---|---|---|
GET | /feed | The authenticated user's home feed |
GET | /user/:userId/feed | One user's posts |
Query parameters
| Parameter | Type | Notes |
|---|---|---|
limit | number | Clamped to 1–50, default 20 |
cursor | { id, createdAt } | Page forward |
refreshSince | { id, createdAt } | Fetch only posts newer than this — pull-to-refresh |
excludePostIds | string[] | Posts already on screen, to avoid duplicates |
refreshSince and excludePostIds exist because the feed is an infinite list that also refreshes at the top. Without them, a refresh would either re-render posts already visible or lose position.
Caching
Responses are cached in Redis for 60 seconds, keyed by every parameter:
feed|u:<userId>|limit:20|cur:<id>:<createdAt>|nonew|noskipInvalidated by invalidateUserFeedCache(userId) when the user creates a post or their media finishes processing.
Cache invalidation is per-author only
When you post, only your feed cache is cleared. Your followers' caches expire naturally after 60 seconds, so a new post takes up to a minute to appear for them. That is a deliberate trade — fanning out invalidation to every follower would be far more expensive.
Separately, the invalidation uses redis.keys(), which blocks Redis while it scans the entire keyspace. Replace with SCAN before volume grows.
Posts
| Method | Path | Purpose |
|---|---|---|
GET | /posts/:id | One post |
POST | /posts | Create a post (multipart/form-data) |
POST | /posts/:postId/impression | Record a view |
POST /posts
Accepts multipart/form-data with files plus these fields:
{
body: string, // required, min length 1
type?: "text" | "clip" | "photos", // default "text"
visibility?: "public" | "private" | "followers",
commentable?: boolean, // default true
gameId?: string,
}Files stream to Spaces via multer-s3, a Media row is created per file with status: "pending", and a job is queued for each. The post is returned immediately; media becomes available as ogun finishes. See Media Pipeline.
type matters beyond display — ogun reads the parent post's type to decide which image variants to generate.
Comments
| Method | Path | Purpose |
|---|---|---|
GET | /posts/:id/comments | List comments |
POST | /posts/:id/comments | Add a comment ({ body }, min length 1) |
DELETE | /posts/:id/comments/:commentId | Delete a comment |
Comments trigger a COMMENT notification to the post author, and MENTION notifications for any @user in the body.
Reactions
| Method | Path | Purpose |
|---|---|---|
POST | /reactions | Add a reaction |
DELETE | /reactions/:id | Remove one |
Reactions are polymorphic — the payload targets either a post or a comment — and kind selects the type. The known kinds are like, love, haha, wow, sad, angry.
@@unique([userId, postId, commentId, kind]) prevents double-reacting.
Impressions
POST /posts/:postId/impression
{ "viewDurationMs": 3400, "videoWatchMs": 2100 }
→ 204 No ContentPostImpression is unique per (userId, postId), so the endpoint upserts — repeat calls accumulate engagement on one row rather than inflating a view count. videoWatchMs is only meaningful for clip posts.
Media upload
The preferred path, avoiding the API entirely for the bytes:
1. Request a presigned URL
POST /media/upload/url
{ "mimeType": "image/jpeg" }{
"key": "uploads/<userId>/images/2026/<uuid>.jpeg",
"url": "https://…presigned…",
"expiresIn": 300
}2. PUT the file to url
Directly to Spaces. Nothing goes through the API.
3. Notify completion
POST /media/upload/complete
{ "key": "uploads/…", "postId": "clx…", "mimeType": "image/jpeg", "width": 1080, "height": 1350 }
→ { "ok": true }This creates the Media row, sets the object ACL to public-read, enqueues processing, and invalidates the user's feed cache.
Presigned URLs expire after 5 minutes.
Reporting
Report rows exist for posts, comments, and users, and moderationStatus columns exist on Post and Comment. There is no moderation review pipeline or admin surface consuming them. Partial
