All projects

Drape

AI product photoshoots for small fashion brands — designed, built, shipped, then rebuilt

2026 Personal
Drape
React NativeExpoSupabaseSkiaReact QueryTypeScript

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 useEffect fetching, no hand-rolled polling, no context-as-cache. Every read is a useQuery; every write a useMutation that 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 in src/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:

PlanCostPriceMargin
Lite$2.57₹1,99989%
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.