calendar module manages two tables for the coach web app:
CalendarEvent(calendar_events): a meeting or session on a coach’s calendar, optionally linked to a trainee.AvailabilityWindow(availability_windows): the hours a coach offers for booking. The trainee booking module (modules/trainee-calendar) reads these windows and cuts them into slots ofdurationMinminutes in the studio’s timezone.
Mount point and auth
Source:
apps/core-api/src/modules/calendar/. The router has no role guard and does not apply coach access scoping, so any role forwarded by the gateway can read and write every event in the studio. All queries filter by req.auth.studioId.
The availability routes are registered before /:id, so availability is never read as an event id.
Enums
Overlap rule
A coach cannot hold two events that overlap in time unless one of them isCANCELLED. The rule is enforced twice:
- The service calls
repo.findOverlapping(studioId, coachId, startsAt, endsAt, excludeId?), which looks for an event of the same coach withstartsAtbefore the new end andendsAtafter the new start, and status other thanCANCELLED. - Postgres has an exclusion constraint,
calendar_events_no_overlap, added in migration20260622200000_calendar_overlap_exclusion. It usesbtree_gistoncoachIdandtsrange("startsAt", "endsAt")for rows wherestatusis notCANCELLED. The service catches that violation and turns it into the same error, so two concurrent bookings cannot both pass.
CONFLICT (409) with message time slot already booked.
The event object
Every event endpoint returns the Prisma row plusclientName. CalendarEvent.clientId has no Prisma relation, so the repository resolves names with one scoped client.findMany over the ids on the page.
string
cuid.
string
Owning studio.
string
The
Coach.id whose calendar holds the event.string | null
Linked trainee, if any.
string | null
The trainee’s name, or
null when there is no clientId or the client is not in this studio.string
Event title.
string
TRAINING, CONVERSATION or OTHER.string
CONFIRMED, CANCELLED or COMPLETED.string
ISO timestamp.
string
ISO timestamp.
string | null
Free text.
string | null
External calendar id. Only writable through
PATCH.string
ISO timestamp.
string
ISO timestamp.
Event endpoints
GET /v1/web/calendar
Lists the studio’s events ordered by startsAt ascending.
Auth: web lane, any role.
string
Only events of this coach.
string
Only events linked to this trainee.
date
Events with
startsAt at or after this value. Parsed with z.coerce.date(), so an ISO string works.date
Events with
startsAt at or before this value. The filter is on the start time only, so an event that began before from and is still running is not returned.integer
default:"1"
Positive integer.
integer
default:"20"
Positive integer, maximum 500.
VALIDATION (422), UNAUTHORIZED (401).
POST /v1/web/calendar
Creates an event. Responds with status 201.
Auth: web lane, any role.
string
required
The coach whose calendar gets the event. The service does not check that this coach belongs to the studio.
string
Trainee to link.
string
required
At least 1 character.
string
TRAINING, CONVERSATION or OTHER. The database default is OTHER.string
CONFIRMED, CANCELLED or COMPLETED. The database default is CONFIRMED.date
required
Coerced to a date.
date
required
Must be after
startsAt.string
Free text.
- Rejects
endsAtat or beforestartsAt. - Unless
statusisCANCELLED, runs the overlap check for the coach. - Inserts the row.
- If the event has a
clientId, writes aClientActivityrow withtypeMEETING,entityTypeMEETING,actionASSIGNED,actorTypeTRAINER,entityIdset to the event id andentityNameset to the title.actorIdisreq.auth.userIdandactorNameis the name of theCoachrow with thatexternalUserId, ornull. This row feeds the trainee card’s activity tab.
clientName.
Errors:
VALIDATION(422)endsAt must be after startsAt, or a Zod failure.CONFLICT(409)time slot already booked.UNAUTHORIZED(401).
GET /v1/web/calendar/:id
Returns one event of the studio.
Auth: web lane, any role.
string
required
Event id.
clientName.
Errors: NOT_FOUND (404) calendar event not found.
PATCH /v1/web/calendar/:id
Updates the fields present in the body.
Auth: web lane, any role.
string
required
Event id.
string
Move the event to another coach.
string
Link another trainee. The schema does not accept
null, so a link cannot be cleared through this endpoint.string
At least 1 character.
string
TRAINING, CONVERSATION or OTHER.string
CONFIRMED, CANCELLED or COMPLETED.date
New start.
date
New end.
string
Free text.
string
External calendar event id.
coachId, startsAt, endsAt and status, then validates the merged values. The overlap check excludes the event itself and is skipped when the merged status is CANCELLED. After the write, an event with a clientId gets a ClientActivity row with action REMOVED when the body set status to CANCELLED, otherwise EDITED.
Response: the updated event object with clientName.
Errors: NOT_FOUND (404), VALIDATION (422), CONFLICT (409).
DELETE /v1/web/calendar/:id
Deletes the event row. Responds with status 204 and no body. If the event had a clientId, a ClientActivity row with action REMOVED is written after the delete.
Auth: web lane, any role.
string
required
Event id.
NOT_FOUND (404).
Availability endpoints
An availability window is one continuous range on one weekday or one date.string
cuid.
string
Owning studio.
string
The coach offering the time.
string
RECURRING or ONE_TIME.integer | null
0 to 6, set on
RECURRING rows. The trainee booking service compares it with JavaScript getUTCDay(), so 0 is Sunday.string | null
Calendar date (
@db.Date), set on ONE_TIME rows.integer
Minutes from midnight, 0 to 1440. The trainee booking service reads it as wall-clock time in the studio’s timezone.
integer
Minutes from midnight, greater than
startMinute.string
TRAINING or CONVERSATION.integer
Length of one bookable slot in minutes.
boolean
Always
true on rows created here. The list returns active rows only.string
ISO timestamp.
string
ISO timestamp.
GET /v1/web/calendar/availability
Lists the studio’s active windows ordered by weekday, then date, then startMinute. Not paginated.
Auth: web lane, any role.
string
Only windows of this coach.
POST /v1/web/calendar/availability
Creates one row per combination of day and range. Three weekdays and two ranges produce six rows. All rows are created in one prisma.$transaction. Responds with status 201.
Auth: web lane, any role.
string
required
The coach offering the time.
string
required
RECURRING or ONE_TIME.integer[]
Each 0 to 6. Required with at least one entry when
kind is RECURRING.date[]
Required with at least one entry when
kind is ONE_TIME.object[]
required
At least one range. Each has
startMinute and endMinute, both integers from 0 to 1440, with endMinute greater than startMinute.string
required
TRAINING or CONVERSATION. OTHER is not accepted here.integer
required
5 to 1440. Must fit in the longest range, otherwise the request fails.
data.items is the array of created window rows.
Errors:
VALIDATION(422) from Zod, includingweekdays is required for recurring availability,dates is required for one-time availabilityandendMinute must be greater than startMinute.VALIDATION(422)durationMin does not fit in any range.
DELETE /v1/web/calendar/availability/:id
Deletes one window row after checking it belongs to the studio. Responds with status 204 and no body. Events already booked in that window are not touched.
Auth: web lane, any role.
string
required
Window id.
NOT_FOUND (404) availability window not found.
Related settings
Studio.settings.calendarEnabled turns trainee booking on or off. It is written through PATCH /v1/web/studios/current/settings (see Studios) and read by the trainee modules. A value other than false counts as enabled. The coach calendar endpoints on this page do not read it.