See how a specific garment, in a specific size and colour, would actually look on your body — including when it does not fit.
Most virtual try-on tools quietly render a flattering result whatever size you pick. Wearly does the opposite: it computes the fit numerically first, then requires the render to show the consequences. Ask for an XS on a body that needs XXL and you get a garment that will not close.
Try-ons are generated with your OpenAI API key, so you only ever pay for your own images.
- Create a key at platform.openai.com/api-keys.
- Open AI Profile and paste it into Your OpenAI API Key.
- Upload front, side and back photos, fill in your measurements, then try something on from the catalog.
The key is stored in your browser's localStorage, sent only with generate
requests, and never written to the server. Remove it from the same panel at any
time. Because localStorage is readable by any script on the origin, use a
restricted key you can revoke.
Your reference photos never leave your device except as part of a generate request, and are stored locally in IndexedDB.
Renders always show the subject in ordinary underwear — plain, opaque and full-coverage, plus a bra where the reference photos show a woman — so an ill-fitting garment can be drawn honestly without the image becoming revealing. Bare midriff, arms and legs are expected wherever a garment genuinely does not cover; the private areas never are.
npm install
npm run devCopy .env.example to .env.local. For local development you can skip the
in-app key entry and use one from the environment instead:
OPENAI_API_KEY=sk-...
ALLOW_SHARED_KEY_FALLBACK=trueLeave OPENAI_API_KEY and ALLOW_SHARED_KEY_FALLBACK unset. The key
resolution fails closed: without ALLOW_SHARED_KEY_FALLBACK=true a visitor who
has not supplied their own key gets a clear "add your key" message rather than
silently spending yours.
| Variable | Default | Purpose |
|---|---|---|
OPENAI_API_KEY |
unset | Only used when ALLOW_SHARED_KEY_FALLBACK=true. |
ALLOW_SHARED_KEY_FALLBACK |
false |
Lets keyless visitors render on your key. Bills you. |
TRY_ON_QUALITY |
medium |
low / medium / high. high roughly doubles the wait. |
TRY_ON_STREAMING |
true |
Progressive previews while rendering. Falls back automatically if a model rejects it. |
TRY_ON_RATE_LIMIT |
12 |
Generates allowed per window per client. 0 disables. |
TRY_ON_RATE_WINDOW_SECONDS |
600 |
Length of that window. |
Deploys to Vercel with no configuration — it is a standard Next.js App Router project. Set the variables above in Project Settings → Environment Variables.
Rate limiting is in-memory and therefore per serverless instance — it stops one
person hammering the endpoint, not coordinated abuse. Back lib/rateLimit.ts with
Vercel KV or Upstash if you need more.
The try-on route declares maxDuration = 60, the Hobby ceiling. A render takes
roughly a minute at medium, so it fits but with little headroom; on Pro you can
raise it to 300 and use TRY_ON_QUALITY=high.
Renders run in a registry (lib/tryOnJobs.ts) keyed by variant — product, size,
colour and cut — rather than being owned by the page. Start one, browse to another
product while it works, and it keeps going; the panel simply shows whichever job
matches what is currently selected, and a strip lists anything rendering elsewhere.
Cancelling targets one render, not all of them.
Finished images are written to IndexedDB (try-on-results, 24 most recent). Coming
back to a variant you rendered before — this session or a previous one — restores it
instantly with no regeneration and no API call. IndexedDB rather than localStorage
because these are 1–2 MB binary blobs; localStorage caps out around 5 MB and stores
strings only.
lib/fitEngine.ts is dependency-free arithmetic that turns a body and a garment
into a verdict:
- Body estimation — chest, waist, hip and shoulder from height, weight and body type, plus vertical landmarks from standing-anatomy ratios.
- Graded size chart — the body measurements each size is drafted for, the wearing ease each cut adds, and per-garment pattern specs (length, sleeve, neckline, armhole, rise, inseam, leg opening).
- Fabric behaviour — usable stretch per material, so "too small" is judged against what the cloth can absorb. Rigid leather fails where jersey stretches.
- Consequences — ride-up, exposed midriff, closure gaps, dropped shoulder seams, hem and cuff placement as a percentage of figure height.
lib/tryOnPrompt.ts renders that into the brief, leading and closing with a
directive that names the required reading of the image. Severity is derived from
how far the wearer sits from the body the size was drafted for, independently of
the chosen cut — so deliberately picking Oversized in your correct size is not
reported as a bad fit.
The same report drives the How This Will Fit You panel, so the numbers shown to you are the numbers the renderer was given.
npm test # everything
npm run test:engine # fit engine, prompt, responsive CSS, BYO-key securitytests/fit-engine.test.mjs— sizing, fabric limits, landmark ordering, prompt structure and cross-references.tests/byo-key.test.mjs— key never logged or echoed, upstream errors scrubbed, fail-closed key resolution, catalog-derived inputs.tests/responsive.test.mjs— parses the stylesheet:dvhfallbacks, safe-area insets, breakpoint cascade, grid arithmetic.tests/try-on-jobs.test.mjs— concurrent renders, blob-URL lifetimes, cache restore.tests/rendered-html.test.mjs— server-render smoke tests.
- Product shots are 1024×1024 JPEGs, sent to the model untouched so weave and trim
survive. Replacing them with real studio photography at the same size or larger is
a drop-in improvement — see
productImagePathinlib/catalog.ts. - Body measurements are estimated from height, weight and body type, not measured. The brief tells the model to trust the photograph over the estimate for body shape while keeping garment dimensions fixed.
- A generation takes roughly a minute at
medium, against a 60s function limit on Vercel Hobby.highroughly doubles that and needs Pro with a raisedmaxDuration. Streaming previews keep bytes flowing throughout.