Scan
The Scan tab identifies the main subject of a photo and returns its name, category,
confidence, description and a few facts. It uses the identify edge function.

Change the prompt or model
The prompt is a local constant at the top of supabase/functions/identify/index.ts:
const INSTRUCTION =
'Identify the single main subject of this image. Give its common name, a broad ' +
'category (e.g. Animal, Plant, Food, Vehicle, Landmark, Everyday Object), your ' +
'confidence as a number between 0 and 1, a one or two sentence description, and ' +
'2 to 4 short fun facts.';IDENTIFY_MODEL is imported from _utils/registry.ts, which reads
supabase/functions/_utils/ai.config.json. Set identifyModel to a model that
accepts image input, then redeploy:
"identifyModel": "google/gemini-3-flash-preview"supabase functions deploy identifyRepurposing it
To turn Scan into something else, such as a plant-disease checker or a wine-label reader, change
INSTRUCTION and IdentifySchema together. Update the IdentifyResult type in
src/features/scan/repository.ts and the display in src/features/scan/components/ScanResultCard.tsx,
then redeploy. Existing rows keep their previous shape, so migrate those results or
handle both shapes when rendering.
The result shape
The function validates successful results against this Zod schema:
const IdentifySchema = z.object({
name: z.string(),
category: z.string(),
confidence: z.number().min(0).max(1),
description: z.string(),
funFacts: z.array(z.string()),
});It is stored as jsonb in scans.result.
The request takes imagePath, an uploaded object’s path within chat-media, such as
{userId}/scans/{uuid}.jpg. The useIdentifyImage() hook uploads the selected photo
and sends this path. The response contains:
| Field | Value |
|---|---|
id | The new scan row’s ID. |
path | The uploaded image’s storage path. |
result | The validated identification result. |
createdAt | The scan row’s creation timestamp. |
Gotchas
Categories are free text. The prompt suggests Animal, Plant, Food, Vehicle, Landmark, Everyday Object, but the model can return other labels. The
category chips filter on those labels. For a fixed set, update INSTRUCTION
and change category to a Zod enum.
Scan opens the system camera or photo library through expo-image-picker. The
viewfinder graphic in the modal is not a live camera preview.
Photos are resized to a maximum long edge of 1024px before upload. Smaller images keep their dimensions.
Scans count toward the free monthly allowance alongside chat messages. Reaching
the allowance returns 402 quota_exceeded and opens the upgrade paywall. See
Quota and paywall.
Error codes
| Code | HTTP | Meaning |
|---|---|---|
invalid_request | 400 | The request body is missing imagePath or has the wrong shape. |
invalid_image | 400 | The path is invalid or is not under the caller’s user ID. |
image_unavailable | 400 | The object couldn’t be downloaded server-side. |
unauthorized | 401 | The Supabase access token is missing or invalid. |
quota_exceeded | 402 | Free allowance used up. |
provider_not_configured | 500 | OPENROUTER_API_KEY isn’t set. |
internal_error | 500 | An unexpected server or provider error. Check the function logs. |
Files
- scan.tsx - Scan route
- ScanScreen.tsx - Scan screen
- repository.ts - Scan queries and the IdentifyResult type
- ScanResultCard.tsx - Result display
- ScanCameraModal.tsx - Photo selection
- scans.ts - Scan requests and history
- chatMedia.ts - Image uploads
- index.ts - Prompt, schema and handler
How it works
Structured output. The function tries generateObject first. If that request
fails, it asks for JSON through generateText and parses the response with the
same schema. The function logs when it uses this fallback.
History. Row-level security limits useScans() to the signed-in user’s results,
so the query needs no user_id filter.