Create a staff member
/api/providersCreates 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 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`.