Each studio has a content library: recipes, videos, articles, PDFs and links that the coach publishes for trainees. The trainee app reads it through three endpoints. Mount: /v1/trainee/content, router traineeContentRouter in src/modules/content/content.routes.ts. Handlers are in createTraineeContentController in content.controller.ts. Lane: trainee. The router mounts authenticateTrainee only, without the app access guard. All three routes are GET, so preview tokens work. See Authentication.

Visibility rules

A trainee sees a content item when all of these hold:
  • it belongs to the trainee’s studio,
  • isPublished is true,
  • it is not restricted, or it is restricted and a ContentItemUnlock row exists for this trainee.
Restricted items (isRestricted) are hidden from every trainee until they are unlocked for that trainee. An unlock is written either by a coach from the web (source: "manual") or by an automation flow’s unlock step (source: "automation"), through unlockContentForClient in content-unlocks.ts. The unlock time is returned as unlockedAt so the app can show a “new” badge. One exception: the item linked from the studio’s live home banner can be opened by id even when it is not published. It still has to pass the restricted check.

Endpoints

GET /v1/trainee/content

Lists the content items the trainee can see. Auth: trainee token.
string
recipe, video, knowledge, pdf, other or link.
Case-insensitive match on the title.
number
default:"1"
Positive integer.
number
default:"100"
Positive integer, at most 500.
Ordered by sortOrder ascending, then createdAt descending. There is no filter by category, tag or featured flag on the server. The app filters the returned list. Response: 200.
string
recipe, video, knowledge, pdf, other or link. For other, otherLabel holds the coach’s own label.
string | null
image, video, youtube, pdf, link or none.
array | null
Recipe steps. Each is { type, text } with type step or tip.
number | null
Recipe macros as flat columns, for the whole recipe and per serving. The coach types the grams. All are null when not set.
object | null
The item’s category. null means the general bucket.
array
Studio-wide tags attached to the item, ordered by name.
Drives the featured strip in the app.
string | null
When this trainee was granted a restricted item. null for unrestricted items.
total is the count of all matching items, not just the page. The response does not echo page or pageSize. Errors: VALIDATION (422) when type is not one of the six values or the paging values are out of range.

GET /v1/trainee/content/taxonomy

Returns every content category and tag of the studio, so the app can show folders that have no items yet. Auth: trainee token. No parameters. Both lists are ordered by sortOrder, then creation time. Response: 200.
The rows are the raw ContentCategory and ContentTag records. Default categories are created with the studio and carry a systemKey. The exact column list beyond id, name and sortOrder follows the Prisma models. Errors: none beyond the lane errors.

GET /v1/trainee/content/:id

Returns one content item. Auth: trainee token.
string
required
The content item id.
The lookup applies the visibility rules above. If the item is not found as a published item, the service checks whether it is the content item of the studio’s published home banner and, if so, returns it even though it is unpublished. The per-trainee restricted check applies in both cases. Response: 200. One item in the same shape as a list entry. Errors: