Image Generation
The Create tab is a single-shot image studio: type a prompt, pick a style and an aspect ratio, get an image. It is backed by the generate edge function.

Change presets, ratios or the model
| To change | Edit | Then |
|---|---|---|
| Style presets | stylePresets in ai.config.json | Redeploy generate |
| Aspect ratios | aspectRatios in ai.config.json | Redeploy generate |
| The model | generateModel in ai.config.json | Redeploy generate |
All three are defined in one file, supabase/functions/_utils/ai.config.json, which
config.js also reads. The client sends only an id. The function looks up what that id
means in the same file, so it never trusts wording the device sends. An id the function
doesn’t recognise contributes an empty suffix rather than failing, which is why a missed
redeploy shows up as unstyled images rather than an error.
Adding a style preset
Add it to ai.config.json
{ "id": "noir", "label": "Noir", "promptSuffix": ", black and white film noir, hard shadows, high contrast" }Redeploy
supabase functions deploy generateVerify
The chip appears in the Create tab and its images look styled. A chip that appears but produces unstyled images means the redeploy was missed.
Changing the model
Pick a slug from openrouter.ai/models whose output modalities include image, then redeploy:
"generateModel": "google/gemini-3.1-flash-image"Gotchas
With no REVENUECAT_SECRET_KEY, generation is refused for everyone. The
entitlement check fails closed, so an unset key reads as “nobody is Pro”. See
Secrets.
Aspect ratios are approximate. The image model exposes no width or height
parameter through OpenRouter, so the ratio is written into the prompt as text
(, wide 16:9 cinematic landscape composition, much wider than tall).
Generations are never metered, because they are Pro-only. See Quota & Paywall.
Error codes
| Code | HTTP | Meaning |
|---|---|---|
invalid_prompt | 400 | Empty prompt. |
pro_required | 403 | Not a Pro user, or REVENUECAT_SECRET_KEY is unset. |
no_image | 502 | The model replied without an image. Usually a prompt the provider refused. |
provider_not_configured | 500 | OPENROUTER_API_KEY isn’t set. |
Files
| Piece | Where |
|---|---|
| Screen | src/app/(protected)/(tabs)/create.tsx |
| Components | src/components/create/ |
| Client call | src/lib/ai/generateImage.ts |
| Gallery hook | src/hooks/generations.ts |
| Function | supabase/functions/generate/index.ts |
| Table | generations |
How it works
Generation is not streamed. Unlike chat, it’s a plain fetch that returns the finished
image in one response.
The gallery is owner-scoped by row-level security. Images are stored as paths rather than signed URLs, because signed URLs expire, and get signed again on demand when the gallery renders.