GLOWUP: The app holds no secrets
A consumer photo-analysis app where the phone never sees a model key, every result passes a schema and hard bounds before it is saved, and minors never get a generated image.
Solo — Expo app, Supabase schema, Edge Functions, analysis worker · 2026
In development. The screens in the recorded demo run on local prototype results; the backend described here is built and tested but not yet wired to the app.
The problem
An app that looks at a selfie and says what to change is two products stacked on top of each other. The visible one is a coaching flow. The invisible one decides what a model is allowed to say about a stranger's face, and what happens when it says something else.
The easy build puts a model key in the app and renders whatever comes back. That ships a credential to every phone, and it makes the model's output the product with nothing in between.
So the design question was where the trust boundary sits, and what has to be true of an answer before it is allowed to cross it.
Architecture
The phone holds a user token and nothing else. Everything that talks to a model runs behind the queue.
- Expo app: public URL, anon key, a short-lived user token
- Edge Function issues a single-use signed upload into a path under the user's UUID
- Private bucket with size and MIME limits; row-level security on every user table
- analyze-photo checks ownership and queues a row by storage path, never a public URL
- Worker claims one job with FOR UPDATE SKIP LOCKED
- Input moderation, then structured output against glowup-analysis-v1
- Runtime bounds check, then an optional styling preview for adults only, moderated again
- App polls status and gets a signed image URL that expires in 15 minutes
Key decisions
A strict schema is necessary, not sufficient
The model is asked for JSON against a schema with no extra properties, and the result is checked again in code. The schema can say a score is an integer between 35 and 90. It cannot say the potential score must be at least the current one, or that the gap between them must stay under 30. Those rules live in validateAnalysis, and an answer that breaks them fails the job instead of reaching the screen.
The score describes presentation, never attractiveness
The categories only allow things a person can change: hair, skin routine, grooming, style, photos. No diagnoses, no guesses about protected traits, no advice about the structure of a face or body. The number on screen is labelled as presentation readiness, and its ceiling is deliberately below 100.
Moderation on the way in and on the way out
A photo is moderated before any model sees it. A generated styling preview is moderated again before it is stored. Either check failing ends that job rather than retrying it, because retrying a flagged image only produces the same flag at a higher cost.
Minors never get a generated image
The worker reads the age band the user picked, and the 13 to 17 band skips image generation entirely. Image generation is also off by default behind a flag until an analysis-only safety evaluation passes. The written guidance still works; the picture does not exist for that group.
A queue that survives a crash without double work
Each worker call claims at most one row with FOR UPDATE SKIP LOCKED, so several can run at once without two analysing the same photo. A job stuck in processing for ten minutes is requeued by a scheduled function, and after three attempts it stops. Only the service role can call that function.
Numbers
- 0 — model keys in the app (public URL and anon key only)
- 2 — moderation passes (the photo in, the generated preview out)
- 35–90 — allowed current score (potential capped at 95, gap at 30)
- 3 — attempts before a job stops (stale jobs requeue after 10 minutes)
- 15 min — lifetime of a result image URL (signed, from a private bucket)
There are no accuracy, retention or user numbers here because there are no users yet. The safety checks have three automated tests, not a reviewed evaluation set; building that set is the stated gate before image generation is switched on. The prototype still shows a stock photo as the "potential" look, which is why that screen is skipped in the recorded demo.