Studio for an organization and a Coach for a member.
Mount: /v1/internal, router internalRouter in src/modules/internal/internal.routes.ts.
Lane: internal. requireServiceAuth only. The caller signs the request with SERVICE_AUTH_SECRET and sends x-service-id, x-timestamp, x-nonce and x-signature. There is no user context, no req.auth and no role check. The signing steps are on Authentication.
/v1 limiter, keyed by the calling server’s IP.
Endpoints
POST /v1/internal/studios
Creates or updates the studio for a better-auth organization. The call is idempotent on externalOrgId.
Auth: service signature.
string
required
The better-auth organization id.
string
required
The studio name.
string
required
The studio slug. Unique across studios.
string
At least 2 characters, for example
he. The model default is he.string
An IANA zone. The model default is
Asia/Jerusalem.provisionStudio does, through studios.upsertByExternalOrgId:
- If a studio with this
externalOrgIdexists, itsnameis updated, pluslocaleandtimezonewhen sent. The slug is not changed. - Otherwise, if a studio with this
slugexists and has noexternalOrgIdyet, it is linked to the organization and its name updated. - Otherwise, if a studio with this
slugexists and already belongs to another organization, that studio is returned unchanged. The caller receives a studio whoseexternalOrgIdis not the one it sent. - Otherwise a new studio is created with the default content categories for its locale.
externalOrgId before the call, the default form templates are copied into the studio (copyDefaultFormsToStudio). That covers a new studio and a studio that was just linked by slug. It also runs in case 3, where the returned studio belongs to another organization, so that studio receives a second copy of the default forms. A failure to copy the forms is logged and does not fail the request. Existing studios are never re-seeded, so later changes to the defaults do not disturb them.
Response: 200. The Studio row.
Studios are also created without this endpoint. On the web gateway lane,
webUserContext creates or links the studio on the first signed request that carries a new x-studio-id, using the x-studio-name and x-studio-slug headers. No caller of POST /v1/internal/studios was found in the three repos, so this endpoint may only be used by scripts or be a leftover of the earlier split deployment.POST /v1/internal/coaches
Creates the coach row for a studio member, or updates its role.
Auth: service signature.
string
required
The internal
Studio.id the coach belongs to.string
required
The better-auth user id.
string
required
Display name. Used only when the row is created.
string
required
A valid email. Used only when the row is created.
string
default:"SUB_COACH"
HEAD_COACH or SUB_COACH.studioId and externalUserId. A new row gets all five values. An existing row only has its role updated. Name and email are left as they are.
The caller is provisionCoach in backend/packages/auth/src/lib/provision-coach.ts. better-auth hooks call it after an invitation is accepted and after a member’s role changes, mapping owner and admin to HEAD_COACH and everything else to SUB_COACH. It logs a failure and never blocks the auth flow.
Response: 200. The Coach row.
Coach columns such as phone, permissions and lastActiveAt. They are omitted here.
Errors:
Admin mounts on the same lane
Two more routers are mounted withrequireServiceAuth only. They hold platform-wide defaults, not studio data, and are called by the web app’s admin area.
Because there is no user context on this lane, the API cannot tell which admin made a change. The web app must check that the signed-in user is a platform admin before it calls these.