Delete a staff member (admin, or manage_staff)
/api/providers/{id}Hard delete. As with services, the appointment foreign key has no cascade, so a provider with bookings cannot be removed. Gated to admin, or a staff login granted manage_staff (migration 0082).
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
Path Parameters
Provider id.
Response Body
application/json
application/json
application/json
application/json
application/json
application/json
curl -X DELETE "https://example.com/api/providers/string"{ "ok": true}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`.
Archive a staff member (admin, or manage_staff) POST
The third state alongside active and deleted, and the one to reach for when somebody leaves: unlike DELETE it works on a staff member who has bookings, and unlike the free `active` toggle it stops the seat being billed. Deliberately reversible. Only the photo is thrown away; bio, phone, service links and working hours all survive untouched, and a linked login is suspended rather than unlinked, so POST /api/providers/{id}/reactivate has nothing to re-enter or re-link. All of it happens inside one `book_archive_provider()` call (migration 0086), so a partial failure cannot leave a login suspended with the card still reading as active, or the reverse. The person leaves the seat count (`seatsUsed()` excludes them) and joins a separate archived count, free on every plan up to the plan's own limit (`SeatPlan.archivedLimit` in billing/config.ts: Free 3, Solo 5, Growth 10, Pro 20, Custom unlimited). At the limit this answers 402, and nothing already archived is ever removed to make room. The tier is re-read from the database here rather than trusted from the caller.