Send a message to the native AI assistant (v1, read-only)
/api/ai/chatRuns 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 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.