Videos Module — #VideoVisit (+ AudioPost)
Location: apps/api/src/modules/videos/ and apps/api/src/modules/audio/
Aggregate roots: VideoVisit, VideoReaction, VideoRepost, VideoComment; AudioPost
Short video cards per interest (Wave 6 §3.5.3). The file itself goes through the media pipeline (R2) — this context only persists the URL + metadata. Cross-context refs (authorId, interestId) are UUID-only.
VideoVisit
| Field | Notes |
|---|---|
| mediaUrl / thumbnailUrl | R2 public URLs from the media presign flow |
| caption | ≤ 280 chars |
| durationSec | clamped to ≤ 60 s (MAX_VIDEO_SECONDS) |
| interestId | optional interest anchor (UUID, no FK) |
| removedAt | admin soft-remove flag |
Every card is enriched with author, interest, likeCount / repostCount / commentCount, and viewer flags (viewerHasLiked, viewerHasReposted) via batched groupBy aggregations.
HTTP Endpoints
All under /api/v1/videos, JwtAuthGuard.
| Method | Path | Description |
|---|---|---|
| GET | /videos?interestId=&limit=&cursor= | Feed, newest first (offset cursor) |
| POST | /videos | Create a clip (≤ 60 s, throttled 5/min, 30/h) |
| GET | /videos/by-author/:authorId | Author's clips (latest 30) |
| GET | /videos/:id | Single card |
| DELETE | /videos/:id | Author hard-deletes |
| POST | /videos/:id/like | Like (idempotent upsert) |
| DELETE | /videos/:id/like | Unlike |
| POST | /videos/:id/repost | Repost (idempotent upsert) |
| DELETE | /videos/:id/repost | Un-repost |
| GET | /videos/:id/comments | Comments, oldest first (≤ 200) |
| POST | /videos/:id/comments | Add a comment (≤ 2000 chars) |
| DELETE | /videos/comments/:commentId | Author soft-removes (removedAt) |
Likes/reposts are unique per (videoId, userId); engagement mutations return the refreshed card.
Discovery Integration
The unified discovery stream (/modules/discovery) merges videos into the feed and attaches an engagement block (engagement: { ... }) for type === 'video' items only — likes/reposts/comments counts ride along with the stream card.
AudioPost
Sibling context in apps/api/src/modules/audio/ — short voice notes per interest, same shape (mediaUrl, caption ≤ 280, durationSec clamped to ≤ 300 s, removedAt). Routes mirror the video CRUD: GET /audio, POST /audio, GET /audio/by-author/:authorId, GET /audio/:id, DELETE /audio/:id.
Note: AudioPost has no engagement layer yet — no likes, reposts, or comments. Adding parity with VideoVisit is a follow-up.
Events
Neither context emits or consumes domain events today — engagement is read-model only.
Admin
Both kinds are moderated through the unified content endpoint: GET /admin/content?kind=videos|audio, POST /admin/content/:kind/:id/remove|restore (sets/clears removedAt). See /modules/content-contexts for the full kind list.