Skip to content

Content & Feeds

Posts, comments, reactions, feeds, impressions, and media upload. All endpoints require authentication.

Feeds

MethodPathPurpose
GET/feedThe authenticated user's home feed
GET/user/:userId/feedOne user's posts

Query parameters

ParameterTypeNotes
limitnumberClamped to 1–50, default 20
cursor{ id, createdAt }Page forward
refreshSince{ id, createdAt }Fetch only posts newer than this — pull-to-refresh
excludePostIdsstring[]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|noskip

Invalidated 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

MethodPathPurpose
GET/posts/:idOne post
POST/postsCreate a post (multipart/form-data)
POST/posts/:postId/impressionRecord a view

POST /posts

Accepts multipart/form-data with files plus these fields:

ts
{
  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

MethodPathPurpose
GET/posts/:id/commentsList comments
POST/posts/:id/commentsAdd a comment ({ body }, min length 1)
DELETE/posts/:id/comments/:commentIdDelete a comment

Comments trigger a COMMENT notification to the post author, and MENTION notifications for any @user in the body.

Reactions

MethodPathPurpose
POST/reactionsAdd a reaction
DELETE/reactions/:idRemove 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

http
POST /posts/:postId/impression
{ "viewDurationMs": 3400, "videoWatchMs": 2100 }
204 No Content

PostImpression 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

http
POST /media/upload/url
{ "mimeType": "image/jpeg" }
json
{
  "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

http
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

Internal documentation — PlayPalz platform