Set a teammate's granular permissions (admin)
/api/members/{id}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.
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.
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Response Body
application/json
application/json
application/json
application/json
application/json
curl -X PATCH "https://example.com/api/members/string" \ -H "Content-Type: application/json" \ -d '{ "permissions": { "property1": true, "property2": true } }'{ "ok": true}Toggle a Recipe on/off, or edit its message (admin) PATCH
Org-wide marketing configuration for one Recipe (docs/competitive-gap-analysis.md:1260-1424): ADMIN-gated, unlike PATCH /api/notification-prefs' self-only shape, since this changes what every customer of the business receives, not one login's own alert preferences. Writes book_companies.recipe_prefs past its SELECT-only grant (migration 0156) via service_role. `state` accepts only `off`/`automatic` today. `ask_first` is a valid value already at rest in storage (recipe_prefs is free text) but has no landing surface yet (no "Today" page), so this route 400s it rather than silently accepting a setting nothing acts on. `subject`/`body` are optional and independent of `state`: sending either (or both) sets a per-org override for that recipe's message, read by recipeTemplate() (src/lib/marketing/recipes/definitions.ts) ahead of the shipped default; an explicit `null` on either resets it back to the default.
Remove a teammate (admin) DELETE
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.