Drape
AI product photoshoots for small fashion brands — designed, built, shipped, then rebuilt
A small clothing or jewellery brand has a phone, a flat-lay photo, and no budget for a photoshoot. Drape takes that photo and returns on-model mockups, catalog sets, single hero shots, video, and ad creative. I designed it and built it — v1 shipped to the App Store at 1.5.1.
Designing for people who don’t want to make decisions
I wrote a UX design guide for the app before redesigning the flows, built on six behavioural principles. The one that changed the most screens: never hand the user a blank form.
Every empty field is a decision, and stacking decisions produces no decision at all. So every selector in the create wizard ships pre-selected — a default reads as a recommendation, and 70–90% of people never change one. Primary buttons carry the outcome rather than the verb: not “Search” but “See 12 results”. The home screen asks “What are we shooting today?” and offers five named jobs — Catalog, Photoshoot, On-Model, Video, Ads — instead of a generic upload box.
The rebuild
v1 worked and had real problems: a 180-line context acting as a cache, refetch storms that hit 6,500 requests a minute, credit-deduction maths duplicated across five client branches, and 1,875 hardcoded hex values with no token layer underneath them.
v2 is the same backend and a new client, built against eight rules written down before any code:
- Server state is React Query, always — no
useEffectfetching, no hand-rolled polling, no context-as-cache. Every read is auseQuery; every write auseMutationthat invalidates keys. - Credits are server-owned — the edge function deducts inside the job and refunds on failure. The client renders a balance and never does credit maths.
- No raw hex outside
src/theme— tokens for colour, spacing, type, radius; base components insrc/components/ui. - The navigator never unmounts — auth gating is
Stack.Protected, which kills the whole class of navigation-destruction bugs v1 had during phone-OTP login. - Thin client — the app talks only to Supabase. No AI keys, endpoints, or prompts exist in the codebase by design.
Rails on from day one: strict TypeScript with noUncheckedIndexedAccess, exhaustive-deps at
error level, fingerprint runtime versioning, and a typed analytics event union where an
unknown event name simply doesn’t compile.
Pricing that survives contact with the invoice
Drape had three credit systems running in parallel — a legacy client-side image wallet in AsyncStorage, subscription quotas, and a separate video wallet. I built a standalone wallet service to replace all three with one currency priced from real provider cost.
Pricing the existing tiers at actual rates was the argument that ended the discussion:
| Plan | Cost | Price | Margin |
|---|---|---|---|
| Lite | $2.57 | ₹1,999 | 89% |
| Pro | $58.56 | ₹3,999 | −22% |
A 4K image was consuming the same “one image” of quota as a 1K one. Quotas had to go.