Shared

Send a message to the native AI assistant (v1, read-only)

post/api/ai/chat

Runs entirely as the signed-in user through requireMember()'s RLS-scoped client, never service-role, with one exception: book_usage_events has no RLS policy for authenticated at all (0064), so the atomic credit reservation opens its own serviceRoleClient() for that one read/write, never for anything the assistant itself can see or do (src/lib/usage.ts's reserveAiCredit).

requireMember() is called with no vertical argument on purpose: either engine's staff can reach this, and buildTools() (src/lib/ai/tools.ts) hands back a DIFFERENT tool set per ctx.businessType instead: e.g. a hospitality company's model sees getTodaysReservations, never getTodaysAppointments, and vice versa. No tool ever takes a company id as an argument, the same non-negotiable rule requireApiKey() follows for API keys, inherited here for a fourth trust model.

v1 is read-only: the assistant can look up this company's own bookings, clients and revenue, and search the help centre, but cannot create, change, or cancel anything; enforced by the system prompt, not by tool absence alone. Streams back an AI SDK v5+ UI-message stream (text/event-stream), not a JSON body, so the response schema below is a description rather than a checked shape (this file's own opening note: "prose, field-level schemas, and examples" are not machine-derived).

Calling OpenRouter's free-tier models (v1's only model tier) is a platform-wide shared cap on this one key, separate from the per-company credit gate below; see src/lib/ai/provider.ts. OpenRouter and the underlying free-tier model vendors are listed in SUBPROCESSORS (src/lib/marketing/legal.ts) since real customer/booking data reaches them as tool-call context.

Authorization

sessionCookie
sb-qxrvgfkjyvbngipqvslu-auth-token<token>

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.

Response Body

text/event-stream

application/json

application/json

application/json

application/json

application/json

curl -X POST "https://example.com/api/ai/chat" \  -H "Content-Type: application/json" \  -d '{    "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",    "messages": []  }'
"string"

Global search, backs the dashboard's Cmd/Ctrl+K palette GET

One round trip across every entity a dashboard member might be hunting for mid-shift, instead of the palette racing several fetches itself. Reuses lib/booking/customer-search.ts's searchCustomers() for the `customers` half, so its scope confinement (appointments-only, staff/manager scoped to clients they share a confirmed appointment with) and staff_client_visibility redaction are identical to /api/customers/search rather than a second, driftable copy. `services` and `tables` are vertical-exclusive: appointments gets `services` (from `book_services`) and an always-empty `tables`, hospitality gets `tables` (from `book_tables`) and an always-empty `services`. `staff` is populated in both, from `book_providers` (appointments) or `book_servers` (hospitality). `customers` and `staff` are always attempted regardless of vertical. Below 2 characters this returns every key empty rather than scanning: the palette's own default state (no query yet) shows static quick links instead, so there is nothing useful to fetch this early. A failed sub-query (any of the four) degrades to an empty array for that key alone rather than failing the whole response: there is no 400 this route can emit.

Attach a CSV/TSV/XLSX file to an AI assistant conversation POST

Phase 1 only: CSV, TSV and XLSX. PDF and images are later phases (PDF needs a text-extraction dependency this app does not have yet; images need a vision-capable model actually wired into the free-tier rotation). Validation IS successfully parsing the file (parseImport, src/lib/data/parse.ts), the same 'prove it's real by processing it' posture the avatar routes take for images: a file that fails to parse is rejected outright and never reaches Storage. Uploads through serviceRoleClient() end to end, same posture as every data-imports/data-exports route for an equally sensitive 'a venue's own file' surface; the private `ai-attachments` bucket carries no policies for `authenticated` at all. Only the extracted grid (capped rows and characters, attachmentContextText) is what the model ever reads (readAttachment, src/lib/ai/tools.ts); the raw file is never inlined into a chat message. The chat row is upserted if it does not exist yet, since a file can be attached before the first message is ever sent. No upload-specific rate limit or credit charge yet, same posture as `AI_RATE_LIMIT_PER_MINUTE` on POST /api/ai/chat itself: no real users yet, needs a real answer before production traffic.