The 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 of durationMin minutes 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 is CANCELLED. The rule is enforced twice:
  1. The service calls repo.findOverlapping(studioId, coachId, startsAt, endsAt, excludeId?), which looks for an event of the same coach with startsAt before the new end and endsAt after the new start, and status other than CANCELLED.
  2. Postgres has an exclusion constraint, calendar_events_no_overlap, added in migration 20260622200000_calendar_overlap_exclusion. It uses btree_gist on coachId and tsrange("startsAt", "endsAt") for rows where status is not CANCELLED. The service catches that violation and turns it into the same error, so two concurrent bookings cannot both pass.
Both paths answer CONFLICT (409) with message time slot already booked.

The event object

Every event endpoint returns the Prisma row plus clientName. 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.
Response:
Errors: 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.
Behaviour:
  1. Rejects endsAt at or before startsAt.
  2. Unless status is CANCELLED, runs the overlap check for the coach.
  3. Inserts the row.
  4. If the event has a clientId, writes a ClientActivity row with type MEETING, entityType MEETING, action ASSIGNED, actorType TRAINER, entityId set to the event id and entityName set to the title. actorId is req.auth.userId and actorName is the name of the Coach row with that externalUserId, or null. This row feeds the trainee card’s activity tab.
No push notification, job or task is created by this module. Response: the created event object with 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.
Response: the event object with 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.
Behaviour: the service loads the stored event, merges the body over it for 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.
Errors: 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.
Response:

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.
Example body:
Response: data.items is the array of created window rows. Errors:
  • VALIDATION (422) from Zod, including weekdays is required for recurring availability, dates is required for one-time availability and endMinute must be greater than startMinute.
  • VALIDATION (422) durationMin does not fit in any range.
The service does not check new windows against existing ones, so overlapping windows for the same coach can be stored.

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.
Errors: NOT_FOUND (404) availability window not found. 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.