Skip to Content
AIChat

Chat

The Chat tab is a streaming, multi-turn conversation with a model you pick. It is backed by the chat edge function.

Change the chat system prompt in this project. Edit the chat edge function, and tell me what else reads that prompt.
A chat thread showing a markdown-formatted assistant reply, with the thread's model shown as a badge in the header

Change the prompt or reply length

To changeEditThen
The system promptsupabase/functions/chat/index.tsRedeploy
Reply lengthMAX_OUTPUT_TOKENS in the same file (currently 2048)Redeploy
Which models appearconfig.js, and the function’s allow-listSee Models
The UIsrc/components/chat/Nothing

No system prompt is set by default, so the function forwards the conversation as-is. Add one for your product by passing system to streamText:

supabase/functions/chat/index.ts
const result = streamText({ model: openrouter.chat(model), system: 'You are a helpful assistant for …', messages: modelMessages, maxOutputTokens: MAX_OUTPUT_TOKENS, });
supabase functions deploy chat

Raising MAX_OUTPUT_TOKENS costs proportionally more per message.

Gotchas

Keep expo/fetch if you replace the transport, because React Native’s global fetch buffers the whole response body before resolving, so every token arrives at once.

The model is fixed once a thread exists, so a thread’s history stays consistent with the model that produced it. ModelPicker is disabled on existing threads. To switch, start a new chat.

Attachments are resized to a 1024px long edge and re-encoded as JPEG at quality 0.7, because vision models bill by image size. The picker’s own quality option compresses without capping dimensions, so it is not enough on its own.

Error codes

The function answers with a JSON body containing a code:

CodeHTTPMeaning
invalid_model400Slug not in the function’s allow-list. See Models.
model_not_vision400An image was attached to a model that can’t see.
invalid_file_url400A file part whose URL isn’t a chat-media signed URL.
pro_required403An imageOutput model requested by a non-Pro user.
not_found404The threadId doesn’t belong to the caller.
quota_exceeded402Free allowance used up. See Quota & Paywall.
provider_not_configured500OPENROUTER_API_KEY isn’t set. See Secrets.

Files

RouteFile
Thread listsrc/app/(protected)/(tabs)/chats.tsx
A conversationsrc/app/(protected)/chat/[threadId].tsx
Transportsrc/lib/ai/chatTransport.ts
Attachmentssrc/lib/storage/chatMedia.ts
Serversupabase/functions/chat/index.ts

How it works

Threads. The client never sends its own thread id. The function creates one on the first message and returns it in an x-thread-id header, titled from the first 40 characters of the message, or New chat if that message was image-only.

Attachments. An upload goes to chat-media/{userId}/<uuid>.jpg as a signed URL, which the server rewrites to the bare storage path before persisting, because signed URLs expire. resolveChatMediaUrl() re-signs the path on demand when history renders. The function only accepts a file part whose URL is a chat-media signed URL, so it cannot be tricked into persisting an arbitrary one.

Server-side inlining. Model providers reject Supabase Storage signed URLs outright (Only HTTPS URLs are supported., Cannot fetch from private/localhost URLs). Before calling the model, the function downloads each image with the service-role client and inlines it as a base64 data: URL, which works with any provider and keeps the bucket private. See inlinePartsForModel() in supabase/functions/_utils/ai.ts.

Image replies. A model flagged imageOutput can answer with images. These models are Pro-only, and their replies upload separately to chat-media/{userId}/gen/ so they survive in history.

Last updated on