Chat
The Chat tab lets you choose a model and have a conversation with streaming replies. It uses the chat edge function.

Change the prompt or reply length
| To change | Edit | Then |
|---|---|---|
| The system prompt | supabase/functions/chat/index.ts | Redeploy |
| Reply length | MAX_OUTPUT_TOKENS in the same file (currently 2048) | Redeploy |
| Which models appear | supabase/functions/_utils/ai.config.json, shared by the app and server | See Models |
| The UI | src/features/chat/components/ | Reload the app during development |
No system prompt is set by default. Add system to the existing streamText call:
const result = streamText({
model: openrouter.chat(model),
system: 'You are a helpful assistant for a meal-planning app.',
messages: await convertToModelMessages(await inlineFileParts(history, admin)),
maxOutputTokens: MAX_OUTPUT_TOKENS,
...(caps.imageOutput ? { providerOptions: IMAGE_MODALITY_OPTIONS } : {}),
onFinish: (turn) => persistAssistantTurn(admin, user.id, threadId, turn),
});supabase functions deploy chatSend a new message to check that the reply follows your prompt.
Raising MAX_OUTPUT_TOKENS allows the model to generate longer replies.
Gotchas
Keep expo/fetch if you replace the transport. React Native’s global fetch
buffers the response, so the reply appears all at once instead of streaming.
The app disables ModelPicker once a thread exists, so a thread’s history stays
consistent with the model that produced it. To switch models, start a new chat.
Attachments are resized to a maximum long edge of 1024px and re-encoded as JPEG at
quality 0.7. Smaller images keep their dimensions. The picker’s own quality option
compresses images without limiting their dimensions.
Error codes
Requests rejected before streaming starts return a JSON body containing a code:
| Code | HTTP | Meaning |
|---|---|---|
invalid_request | 400 | The request body does not match the expected shape. |
invalid_model | 400 | Slug not in the function’s allow-list. See Models. |
model_not_vision | 400 | An image was attached to a model that does not accept image input. |
invalid_file_url | 400 | A file part’s url is not a valid storage path under the caller’s user ID. |
unauthorized | 401 | The Supabase access token is missing or invalid. |
pro_required | 403 | An imageOutput model requested by a non-Pro user. |
not_found | 404 | The threadId does not exist or belongs to another user. |
quota_exceeded | 402 | Free allowance used up. See Quota & Paywall. |
provider_not_configured | 500 | OPENROUTER_API_KEY isn’t set. See Secrets. |
internal_error | 500 | An unexpected server error. Check the function logs for details. |
Files
- chats.tsx - Thread list route
- [threadId].tsx - Conversation route
- chatTransport.ts - Streaming transport
- ChatsScreen.tsx - Thread list screen
- ChatScreen.tsx - Conversation screen
- repository.ts - Thread and message queries
- chatMedia.ts - Attachments
- index.ts - Chat edge function
How it works
Threads. The client sends the newest message and, for an existing conversation,
its threadId. The function reads earlier messages from the database. For a new
conversation, it creates the thread and returns its ID in the x-thread-id header.
The title uses the first 40 characters of the message, or New chat for an image-only
message.
Attachments. Images are uploaded to the private chat-media bucket under
{userId}/<uuid>.jpg. The client sends that storage path in the file part’s url
field, and the function checks that it matches the caller’s user ID before storing
the message. resolveChatMediaUrl() signs the path on demand to display the image.
Server-side inlining. OpenRouter cannot fetch images from a local Supabase
instance. Before calling the model, inlineFileParts() in
supabase/functions/_utils/media.ts downloads images with the service-role client
and converts them to base64 data: URLs. The bucket stays private. The OpenRouter
provider is configured in supabase/functions/_utils/provider.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.