Erase a client's personal data on request (admin)
/api/clients/{id}/eraseCloses the gap the privacy policy already names: a business is responsible for honouring its own customers' access/deletion requests, but no tool existed to actually fulfil one for a single guest; only DELETE /api/account (the whole workspace) did.
Anonymises in place; never deletes the row. book_appointments.customer_id/book_reservations.customer_id are both NOT NULL with no ON DELETE clause, so a client with any booking history cannot be hard-deleted without also deleting that booking, which an erasure request never asked for.
Clears name (replaced with "Erased guest"), email, phone, notes, tags and the birthday fields, and sets marketingStatus to 'unsubscribed' permanently. Booking history, id, company_id and created_at are untouched. Never refuses on upcoming bookings: the count is reported instead, since refusing would let a venue stall a legitimate request indefinitely just by leaving one on the calendar.
Writes an append-only book_compliance_events row (customer_erased) recording who did it and when. Idempotent: re-running this on an already-erased record just rewrites the same values and logs another event.
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
Client 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 POST "https://example.com/api/clients/string/erase" \ -H "Content-Type: application/json" \ -d '{ "confirm": "ERASE" }'{ "ok": true, "upcomingBookingsAffected": 0}Update a client PATCH
Partial: only present keys are written. **Email is deliberately not editable.** It is the client identity (the unique index on `company_id, email`), so changing it is a merge or a split of two records and their histories, not a field edit. Sending `email` is silently ignored. `tags` and `notes` are both the venue's PRIVATE data about the client. Neither is ever rendered on a guest-facing surface or included in any email or SMS; the notify payload builders name their columns explicitly so they cannot start doing so by accident. **`marketingStatus` (migration 0143) is one-directional.** The only accepted value is `'unsubscribed'`; any other value, including `'subscribed'`, is a 400. Consent is the client's to give, never staff's to restore on their behalf, so this route has no way to resubscribe someone.
Add a form to the catalog (admin) POST
Same permission bar as PUT /api/intake and the same `intake_forms` PlanFeature: this is the other half of the one capability a business buys, not a new pricing decision. Not service-role: book_forms takes a plain `tenant_all` RLS policy, same posture book_intake_forms/book_products/book_packages all take for a catalog item a business edits directly. Shared by both engines: a form is not tied to book_services at all, unlike Packages, so there is no vertical split.