The studios module exposes the caller’s own studio. There is no list or create endpoint: a Studio row is created by the web gateway middleware on first contact (ensureStudio in middleware/web-context.ts) or by the internal provisioning route, both keyed on the better-auth organization id in Studio.externalOrgId. API keys for the studio are managed by a separate router mounted under the same prefix. See Studio API keys.

Mount points and auth

studiosRouter is mounted twice in modules/index.ts. Source: apps/core-api/src/modules/studios/. Role rules come from requireRole in the routes file: On the web lane the role is whatever the gateway sends in x-user-role (unknown values fall back to SUB_COACH). On the bearer lane session-verifier.ts maps the better-auth member role: owner to OWNER, admin to HEAD_COACH, anything else to SUB_COACH.
The two lanes do not put the same kind of id in req.auth.studioId. The web lane resolves the x-studio-id header (an organization id) to the backend Studio.id. The bearer lane’s principalFromSession sets studioId to the session’s activeOrganizationId directly. The service then calls studio.findUnique by id. Whether the bearer mount resolves a studio in practice was not confirmed from the code. Treat /v1/web/studios as the supported path.
/v1/web/studios/current/api-keys is registered before /v1/web/studios, so those paths are handled by the API keys router.

The studio object

All three endpoints return the Studio Prisma row.
string
cuid.
string | null
The better-auth organization id. Unique.
string
Studio name.
string
Unique slug. Not editable through this module.
string
Default he.
string
Default Asia/Jerusalem.
object | null
Free-form JSON. Two keys carry rules, see below.
object | null
Free-form JSON. Known keys are listed below.
string
ISO timestamp.
string
ISO timestamp.
string | null
Set when the studio is archived.

Endpoints

Paths below use the web lane prefix. The bearer lane serves the same handlers under /v1/studios.

GET /v1/web/studios/current

Returns the caller’s studio. Auth: web lane or bearer lane, any role. Response:
Errors: NOT_FOUND (404) studio not found, UNAUTHORIZED (401).

PATCH /v1/web/studios/current

Updates top-level studio fields. branding and settings are replaced whole when sent, with the two exceptions described under Behaviour. Auth: web lane or bearer lane, roles OWNER or HEAD_COACH.
string
At least 1 character.
string
At least 2 characters. Not checked against a list of supported locales.
string
At least 1 character. Not checked as a valid IANA zone here.
object
Any JSON object. Replaces the stored blob.
object
Any JSON object. Replaces the stored blob, except that mbp* keys in the body are ignored.
Behaviour:
  • Branding. normalizeBrandingInput in branding.ts validates two keys when they are present and passes everything else through untouched.
    • instagramHandle: trimmed, leading @ removed, lowercased. Must match ^[a-z0-9._]{1,30}$. Blank or null clears it. A handle stored before this rule existed is kept as stored if it comes back unchanged, so it never blocks saving other fields.
    • storyProfileImage: must be an https: URL of at most 2000 characters. Blank or null clears it.
  • Settings. withStoredMbpSettings drops every key starting with mbp from the body and carries over the stored mbp* keys. Those keys change only through PATCH /current/settings, because changing them re-prices the food library.
This endpoint writes with a plain studio.update and takes no row lock. Use the settings endpoint below for single-key changes to avoid overwriting a concurrent save. Response: the updated studio object. Errors:
  • VALIDATION (422) invalid instagram handle... with details [{ "path": ["branding", "instagramHandle"], "message": "INSTAGRAM_HANDLE_INVALID" }].
  • VALIDATION (422) invalid story profile image url with details message STORY_PROFILE_IMAGE_INVALID.
  • VALIDATION (422) from Zod.
  • FORBIDDEN (403) insufficient role.
  • NOT_FOUND (404).

PATCH /v1/web/studios/current/settings

Merges the given keys into Studio.settings under a row lock, and re-prices the food library when an MBP anchor changes. Auth: web lane or bearer lane, roles OWNER or HEAD_COACH.
boolean
Turns trainee meeting booking on or off. Readers treat any value other than false as enabled. See Calendar.
boolean
Turns the MBP portion system on. Readers require exactly true.
number
Kcal per protein portion, 20 to 2000. Default when unset is 180.
number
Kcal per carb portion, 20 to 2000. Default when unset is 160.
number
Kcal per fat portion, 20 to 2000. Default when unset is 180.
string | null
The portion unit tag shown to coaches and trainees, at most 20 characters after trimming. Blank or null stores null, which means the per-locale default.
string | null
The studio’s contact number for the trainee app, at most 32 characters. Stored trimmed as typed. It must contain 8 to 15 digits. Blank or null clears it.
boolean
Whether the trainee app shows the contact number.
string
ASSIGNMENT, INTAKE_FORM, FIRST_PLAN or FIRST_WORKOUT. Under any mode other than ASSIGNMENT, a subscription assigned without a date waits for the named event. An absent or unknown value reads as ASSIGNMENT.
The schema is a plain z.object, so keys outside this list are stripped and cannot be written here. Behaviour: everything runs in one transaction from foods.runInTransaction.
  1. lockStudioSettings runs SELECT id FROM studios WHERE id = ... FOR UPDATE and reads the current settings.
  2. The patch is merged over the current blob ({ ...current, ...patch }) and saved. Keys not in the patch are kept, so concurrent saves of different keys do not lose each other.
  3. readMbpAnchors is read from the old and the new blob. If the three anchors are the same, the response carries foodRepricing: null.
  4. If an anchor changed, repriceStudioFoods runs in the same transaction with mode change. It loads every food the studio sees (system foods, the studio’s overrides and its own foods), plans the new portion values, applies them, and writes an AuditLog row with action mbp.anchors.reprice, resourceType STUDIO_SETTINGS, resourceId set to the studio id, actorId set to req.auth.userId and a diff holding mode, from, to and the report.
Response: the updated studio object plus foodRepricing.
object | null
null when no anchor changed. Otherwise the RepriceReport from modules/foods/reprice.ts.
The example omits some studio columns for length. Errors:
  • VALIDATION (422) invalid whatsapp number.
  • VALIDATION (422) from Zod, for example an anchor outside 20 to 2000.
  • FORBIDDEN (403) insufficient role.
  • NOT_FOUND (404) studio not found.

Other keys in settings

Studio.settings is shared by several modules. Keys that this module does not validate but that other code reads include: Because PATCH /current replaces the whole blob, a client that sends settings must send back every non-mbp key it wants to keep. Dropping onboarding or agent this way changes how billing limits and the agent behave.
GET /current and both PATCH endpoints return settings exactly as stored, with no field filtering. That includes values other modules keep there, such as the agent’s key. Do not log these responses or pass the blob to the browser beyond what a screen needs.

Branding for the trainee app

projectBranding in branding.ts shapes the blob the trainee app receives (called from modules/trainee/trainee.service.ts). Everything passes through as stored, except that instagramHandle is shown in canonical form: no @, lowercase, or null when blank.