Attach a CSV/TSV/XLSX file to an AI assistant conversation
/api/ai/chat/attachmentsPhase 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.
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
multipart/form-data
The file and which conversation it belongs to. multipart/form-data.
TypeScript Definitions
Use the request body type in TypeScript.
Response Body
application/json
application/json
application/json
application/json
application/json
application/json
application/json
curl -X POST "https://example.com/api/ai/chat/attachments" \ -F file="string" \ -F chatId="f255124e-3419-4f6e-b7ee-17a6577db94d"{ "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "filename": "string", "rowCount": 0}Send a message to the native AI assistant (v1, read-only) POST
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.
Get a short-lived download link for an attached file GET
Re-derives book_ai_attachments' own tenancy boundary by hand (company_id AND user_id), since this route holds a service-role client and RLS never applies to it. Returns 404, not the raw storage error, whether the attachment never existed for this caller or the 90-day sweep (GET /api/cron/ai-attachments-sweep) already removed the raw file: the caller cannot tell those apart and does not need to. The signed URL is a 15-minute bearer credential, same TTL reasoning as the data-exports download link.