Appointments engine

Create a staff member

post/api/providers

Creates the provider row, then its service links and working hours. If the second step fails the provider is deleted again (its cascade takes any partial links with it), so a failure leaves nothing behind. Returns the id so a follow-up avatar upload has something to hang off.

Authorization

sessionCookie
sb-qxrvgfkjyvbngipqvslu-auth-token<token>

The dashboard's Supabase Auth session cookie, set at sign-in. Large sessions are split across numbered chunks (…auth-token.0, .1), so treat this as a cookie family rather than one name.

Every request re-validates it against the Auth server (getUser()), never by decoding the cookie locally: a JWT nothing has checked is not a credential. Tenancy is then read from the verified app_metadata.company_id claim and enforced by row-level security; it is never read from request input, on any route, ever.

role (admin / staff) is deliberately not in RLS. It gates specific actions in route code, the operations marked admin below, so hiding a button in the UI is cosmetic only, and a route's own check is the enforcement.

In: cookie

Request Body

application/json

TypeScript Definitions

Use the request body type in TypeScript.

On PATCH, serviceIds and the working-hours rows are replaced wholesale, not merged. Omitting hours entirely is the same as sending []: the staff member works no days and is offered no slots; UNLESS the caller is a Standard-tier staff member editing their own row, in which case the whole schedule (hours, cycleWeeks, rotationAnchor, rotationWeeks) is ignored outright (see availabilityDelegation).

Response Body

application/json

application/json

application/json

application/json

application/json

application/json

application/json

curl -X POST "https://example.com/api/providers" \  -H "Content-Type: application/json" \  -d '{    "name": "string"  }'
{  "ok": true,  "provider": {    "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08"  }}

Delete a service group (admin, or manage_services) DELETE

Never blocks and never takes services with it: the composite FK's column-scoped `on delete set null (group_id)` (0054) un-groups them in the same statement. That column list is load-bearing; a bare composite `set null` would null `company_id` too and fail the delete outright (the 0039 lesson). Gated to admin, or a staff login granted `manage_services` (migration 0082), same as every other service-catalog write.

Update a staff member PATCH

Replaces the provider's fields, their service links and their working hours; all three, in a single transaction. The whole replacement is one `book_replace_provider()` call (migration 0025, SECURITY DEFINER as of 0055) so RLS's tenant_all is never the real ownership boundary here: the function re-checks role and ownership itself, from the database. It used to be four PostgREST calls with nothing transactional over them, so a failure after the deletes left the staff member with no services and no hours, which means no slots, so the public widget silently stopped offering them until somebody re-saved. That is why there is no longer a 500 here: a partial save is not a state this route can reach. Links and hours are replaced wholesale, not diffed, because the editor posts the complete set every time. **Ownership (migration 0052)**: an admin may edit any provider in the org; any other role may only edit the provider row linked to their own login; a check this route was missing entirely before 0052, and the whole reason it exists: any staff-role login could open and submit any colleague's edit modal, `hours` included. A Standard-tier caller editing their own row additionally has the WHOLE schedule (hours, cycleWeeks, rotationAnchor, rotationWeeks) silently ignored (the stored template is kept) rather than the request being rejected. **Rotation (migrations 0150/0151)**: `cycleWeeks`/`rotationAnchor`/`rotationWeeks` layer an optional multi-week roster on top of `hours` (which stays week 0 either way): "week A this week, week B next week, week A again the week after". Wholesale-replace and the Standard-tier boundary both extend to all four fields as one schedule template, not a second permission to check. **Conflicts**, added after this route shipped with none at all: replacing the schedule used to write straight through even when a future appointment already sat outside the new pattern, silently orphaning it. Only checked when the schedule genuinely differs from what is currently stored (hours, cycleWeeks or rotationAnchor; changing only the anchor can orphan a booking exactly as surely as changing the hours), and applied per (week, day of week) pair against every non-cancelled future appointment resolved through its own date (an appointment on a date with an approved override is excluded; that date answers to the override, not the recurring week). No staff-vs-admin split on the acknowledgment, unlike the override route: a Standard-tier caller can never reach a real conflict here at all (their whole schedule is silently ignored), so anyone who CAN trigger one is already fully authorized to resend with `conflictAcknowledged: true`.