Remove a teammate (admin)
/api/members/{id}Order matters and is deliberate: the company_id claim is nulled FIRST, then the roster row is deleted. The reverse could leave a claim-holding non-member with live RLS access to everything. If the row delete fails the user is locked out but still listed; retrying fixes it, and the dangerous state cannot occur.
You cannot remove yourself, and you cannot remove the last admin.
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
The book_org_members row id; NOT the user id.
Response Body
application/json
application/json
application/json
application/json
application/json
application/json
curl -X DELETE "https://example.com/api/members/string"{ "ok": true}Set a teammate's granular permissions (admin) PATCH
Grants a staff login one or more of the manage_services, manage_staff and manage_org_settings permissions (migration 0082). Admin-gated with no manage_org_settings-style escape hatch, deliberately: this is the route that hands out permissions, so letting it be reached by anything short of a full admin would let a grant-holder mint grants, the same reasoning that keeps depositStaffEditable and unlinkedStaffFullAccess out of PATCH /api/companies' staff-writable set. An admin target is refused outright: an admin already has every permission unconditionally (hasPermission short-circuits on role), so storing any here would be a value nothing reads, and a UI showing ticked boxes on an admin would wrongly imply they could be unticked to take access away.
Resend a pending invite (admin) POST
Admin-triggered resend from the teammate roster; distinct from `POST /api/invites/resend`, which is public and token-keyed for someone acting on their own expired invite link. This one is id-keyed and admin-gated, for "they still haven't accepted, send it again." RLS's tenant-scoped select already proves the id belongs to the caller's own company, so an id from a different company is simply not found, the same reasoning `DELETE /api/members/{id}` relies on. An invite that has already been accepted or already revoked is likewise not found: this route only ever acts on a still-pending one. Issues a fresh token and a fresh expiry (the old link stops working the moment this succeeds) and re-sends the invite email.