How the stylist selects clothes and builds outfit previews
These notes explain how garment IDs are validated, how uploaded photos form the basis for outfit previews, and how moderation and subscriptions fit around the stylist. The figures below describe implementation settings, rather than usage or performance results.
← Back to the case studyFrom question to worn preview, stage by stageView diagramHide diagram
The constraint that shaped everything
Auddr recommends outfits from the clothes saved in a person's wardrobe. Model suggestions therefore need to resolve to owned garments, and generated previews need the uploaded photos as their visual reference. So the design goal was fixed early. No suggestion that cannot be resolved to owned garments, and no preview that is not anchored to the user's own photos.
That splits into two problems. The first is containment: the model's output is treated as a proposal, and code decides whether each proposed item really exists in the wardrobe. The second concerns previews: a text description of a black wool coat does not supply the visual details of the person's actual coat. The answer to that one is the two-stage pipeline in section 3.0.
From question to outfit
Every styling request starts by shrinking the problem. The full wardrobe is fetched, then filtered by live weather through temperature-to-season bands: above 22°C the stylist sees summer and spring pieces, below 8°C winter and autumn, with the weather itself served from a cache invalidated after 120 minutes or a 5 km change in location, whichever comes first. Occasion filtering follows a defined fallback graph, work borrows from everyday, formal from evening, so a sparse wardrobe degrades to a sensible neighbour instead of an empty result. What survives is serialised into the prompt as a reduced projection, item ID, brand, category, colour, style tags, occasion and piece role, enough to style with and nothing more. The four agents themselves are explicit decision trees, not open-ended personas: ordered steps with hard reject rules such as at most one hero piece per outfit, no duplicated roles, and named conditions that force a regeneration.
The model streams its reply under a strict contract: prose for the user, a delimiter, then a JSON block naming the chosen item IDs. The two halves are split, and the JSON is put through the same compatibility rules used before the prompt: every ID is looked up in the user's wardrobe, and clashes such as two bottoms or two pairs of shoes are filtered out. Only the validated outfit is written to the conversation, and the chat renders from a live Firestore listener, so saved conversations can be restored on refresh or resume. Conversation context is truncated to the system prompt plus the most recent nineteen messages, which keeps long sessions from dragging the whole transcript into every call.
Dressing users in their own clothes
Composite first, generate second
The suggested garments' real photos are laid out on a hidden 400 by 600 canvas, positioned off-screen and layered by piece role: shoes under bottoms under tops under outerwear under accessories. Three fixed layout templates cover the common outfit shapes, and a dynamic layout divides the canvas at runtime when tops and outerwear have to share it.
Readiness is counted, not assumed
Capture fires only when the image loader reports every garment photo loaded and the layout counter confirms each one has actually been placed. Nothing about the capture is timed; it is gated on counts agreeing. Section 7.0 tells the story of the version that slept instead.
Anchored generation
The captured composite is uploaded and a Cloud Function runs FLUX.1-dev image-to-image over it: strength 0.65, 30 steps, 512 by 768 portrait output. Strength is the whole trade: high enough to render a person wearing the outfit, low enough that the garments stay recognisably the user's.
Cached by content
The cache key is built from the sorted item IDs, plus the face photo when one is set, so the same outfit requested in any order hits the same entry and a repeat visualisation avoids another image-generation call. Results and failures are both persisted, the failures to their own Firestore collection for diagnosis.
Safety and enforcement
Moderation is wired where the app cannot route around it: a Cloud Function triggered by the storage bucket itself runs Google Cloud Vision SafeSearch on every uploaded image. A likely adult or violent result, or a strongly racy one, moves the file into quarantine and marks the item; an in-app queue lets an admin approve or reject, and rejection deletes the file outright. The storage trigger keeps this check on the server rather than relying on each client to request it.
Commercial limits get the same treatment as safety ones. The free tier allows 10 AI generations and 100 wardrobe items, and the usage counter is cached on the device and checked against a connectivity probe, so going offline does not grant free generations; the count syncs when the connection returns. On privacy, the defaults do the work: Mixpanel starts opted out, Firebase Analytics receives a randomised ID rather than the account ID while Crashlytics keeps the real one for support, marketing email is a separate opt-in recorded with a timestamp, and account deletion removes the data and the credentials together.
The app around the stylist
Votes that feel instant and stay correct
Community voting mutates local state immediately, handling all three cases in the client: a new vote, a switched vote, a vote removed. A Firestore transaction then replays exactly the same three cases server-side. The UI never waits on the network, and concurrent voters cannot corrupt a count.
Sharing on real deep links
Wardrobe and wishlist sections are shareable by link, with gifting mode as a wishlist variant. The links are universal links, associated domains on iOS and verified intent filters on Android, so a shared wardrobe opens straight into the installed app rather than dead-ending in a browser.
Admin without a second system
Administration lives inside the app behind custom-claim role checks: granting and revoking admin runs through server-side functions, and the moderation queue is just another screen. There is no separate dashboard to build, host or secure.
Insights from data already there
The insights screen computes wardrobe value, wear frequency, never-worn counts and category, colour and brand breakdowns directly from the wardrobe items, with no extra tracking pipeline behind it.
The numbers, and where they come from
These are engineering constants read from the source, stated because each one records a decision: how much an image generation may transform the garments, where moderation sits, when cached weather is too stale to style with, and what the free tier includes.
Where other engineering notes in this collection quote latency and load figures, this one does not. No adoption or runtime benchmarks are reported here. The figures describe configuration and product limits.
Faults the process caught
The first pass at capturing the hidden canvas waited 500 milliseconds and hoped the garment photos had loaded, which meant the capture raced the image loader and lost whenever a device or connection was slow enough, producing half-composed outfits. The fix replaced hope with a count: every image reports when it has loaded and when it has laid out, and capture fires only when the counts agree.
A system review found the three stylist modes had each grown a private chat interface with its own state and its own model calls, and none of those conversations persisted to history. Caught by walking the flows end to end rather than testing screens in isolation. The refactor cut the mode screens back to input forms feeding one chat screen with an agent dispatcher, and every conversation became durable.
The plan specified caching of generated previews; a written gap analysis comparing the plan against the built system found it missing, so every repeat of the same outfit was paying for a fresh generation. Caught by the audit, not by a bill. The fix keys the cache on sorted item IDs, so the same garments in any order resolve to the same entry.
Deployment and operations
The app ships through EAS Build with five profiles, from simulator development builds to store production, and version numbers are managed remotely with auto-increment so a release cannot reuse a build number. Store compliance was treated as engineering rather than paperwork: the Android target SDK held at the level Play policy requires, Apple sign-in alongside Google, and permission strings and encryption declarations maintained in configuration instead of clicked through a console.
The serverless surface is deliberately small: eight Cloud Functions, capped at ten instances, with lint and compile run as a predeploy gate on every release. Failures write to dedicated Firestore collections for visualisation, moderation and email sync, so a support question starts from persisted evidence rather than log archaeology. Subscription state flows from the RevenueCat webhook into Firestore and on to Brevo, and the lifecycle email sequence hangs off that sync rather than off anything the app does.