Inspiration

Getting dressed for an important occasion is stressful. You have a mental image of how you want to look, but you can't actually see yourself in that outfit or know how your skin will cooperate by the day of the event. Professional styling and skincare advice are expensive and inaccessible. We wanted to democratize that experience — give anyone a personal stylist and dermatologist in their pocket, driven by AI, that adapts to the specific occasion they're dressing for.

The "meet the moment" philosophy drove every decision: it's not about looking perfect, it's about showing up as the best version of yourself for that specific event. A job interview demands different styling than a wedding, and your skin's concerns on a given day should inform both skincare prep and outfit color choices.

We were also inspired by the rapid advancement of AI vision APIs — particularly YouCam's suite of skin analysis, simulation, and virtual try-on tools — which made it possible to build something that would have required a team of ML engineers just a few years ago.


What it does

Mirror is an occasion-aware self-presentation app. You type what you're dressing for ("job interview Friday", "wedding next Saturday"), upload a selfie and a full-body photo, and Mirror returns:

Skin Report

  • 6 concern scores — redness, acne, texture, oiliness, dark circles, hydration — each scored 0-100 with color-coded progress bars (green/amber/red)
  • Actionable tips per concern — e.g., "A calming, fragrance-free mask the night before can bring redness down fast" or "A gentle exfoliant 48 hours out, not the night before, evens texture without irritation"
  • Mask overlays — YouCam returns region-specific masks showing exactly where each concern appears on the face

Before/After Simulation

  • Each concern row has a "See the difference" toggle
  • Lazily triggers a YouCam skin-simulation task (max intensity for that concern only)
  • Shows the original selfie vs. the simulated "after" image side-by-side
  • Results are cached — toggling back and forth is instant after the first load

Outfit Try-On (Two Modes)

Catalog Mode — Real garment photos (blazers, trousers, shirts, etc.) from a seed catalog, rendered on the user's body via YouCam cloth-v4 VTO. The stylist agent picks up to 3 garments based on occasion formality and skin concerns.

Fabric Mode — AI-generated fabric styles (silks, prints, textures) from YouCam's predefined template library (~30-60 styles), rendered on the user's body via YouCam fabric VTO. Auto-generates when the user switches to the "Fabric" tab.

Both modes show a horizontal carousel of outfit cards with the rendered image, garment/template name, and the stylist's reasoning for each pick.

Styling Narrative

  • After outfits are ready, the user can generate a 2-3 sentence narrative
  • Written in second person ("you"), warm and specific
  • References actual skin concerns and garment choices with their reasoning
  • Persisted on the look for future reference

Look History

  • Every look is saved and browsable from the dashboard
  • Each look shows occasion text, creation date, skin task status, outfit status, and concern scores
  • Click through to view full skin report, outfit carousel, or regenerate narrative

How we built it

Frontend

  • Next.js 16 (App Router, Turbopack) with TypeScript
  • Tailwind CSS 4 + shadcn/ui for component primitives (Card, Progress, Badge, Button, Skeleton, Sidebar, Breadcrumb)
  • Remix Icon for iconography
  • Client components for interactive flows (polling, toggles, carousels)
  • Server components for data fetching where possible

Backend

  • Next.js API routes handle the full pipeline:
    • POST /api/looks — occasion parsing + look creation
    • POST /api/skin-analysis — selfie upload + task creation
    • GET /api/skin-analysis/[taskId] — polling + result persistence
    • POST /api/cloth-vto — body upload + stylist agent + garment VTO tasks
    • GET /api/cloth-vto/[taskId] — polling + render persistence
    • POST /api/fabric-vto — stylist agent + fabric VTO tasks (reuses stored body file ID)
    • GET /api/fabric-vto/[taskId] — polling + render persistence
    • POST /api/skin-simulation — per-concern simulation task creation
    • GET /api/skin-simulation/[taskId] — polling + simulation persistence
    • GET /api/looks/[lookId] — look detail aggregation
    • GET/PATCH /api/looks/[lookId]/outfit — catalog renders + selection
    • GET /api/looks/[lookId]/fabric — fabric renders + status
    • GET /api/looks/[lookId]/skin — skin scan results

AI Agents (LangChain / LangGraph)

  • Planner agent (lib/agents/planner.ts) — parses raw occasion text into structured data: occasionType (interview, wedding, date, etc.), formality (casual, smart_casual, business, formal), and timeframeDays (days until the occasion)
  • Stylist agent (lib/agents/stylist-agent.ts) — an LLM call (OpenAI) that receives the parsed occasion, a catalog (garments or fabric templates with id/name/category/color), and skin concern scores. Returns up to 3 selections with garment IDs and human-readable reasons. Uses Zod schema validation and filters out hallucinated IDs.
  • Synthesis agent (lib/agents/synthesis-agent.ts) — a LangGraph StateGraph with a single node that generates a 2-3 sentence narrative combining occasion, skin findings, and outfit choices.
  • Orchestration (lib/agents/graph.ts) — LangGraph StateGraph wrapping the stylist agent, designed to be extended with additional nodes (skin-agent, etc.)

AI Vision (YouCam / Perfect Corp)

  • Skin Analysis (/s2s/v2.1/task/skin-analysis) — 6 concerns with mask overlays, JSON output
  • Skin Simulation (/s2s/v2.0/task/skin-simulation) — per-concern before/after at 0.0-1.0 intensity
  • Cloth VTO (/s2s/v2.0/task/cloth-v4) — real garments on body
  • Fabric VTO (/s2s/v2.0/task/fabric) — AI fabric styles on body
  • Template listing (/s2s/v2.0/task/template/fabric) — paginated fabric template catalog
  • File upload (/s2s/v2.0/file) — two-step presigned URL upload

Database

  • PostgreSQL via Neon, managed with Drizzle ORM
  • 7 tables: looks, tasks, skinScans, outfitRenders, garments, fabricTemplates, skinSimulations
  • All async task state tracked in tasks table for audit and polling
  • Schema pushed via drizzle-kit push

Storage

  • Vercel Blob (private access) for all persisted images
  • persistYouCamResult() downloads from YouCam's 2-hour URL and re-uploads to blob
  • signedBlobUrl() generates short-lived signed URLs for client-side rendering
  • All image persistence happens immediately upon task completion

Authentication

  • Clerk for user auth (sign-in, session management)
  • All API routes check auth() and filter by userId

Challenges we ran into

1. YouCam API Async Lifecycle

Every YouCam task follows the same pattern: create → poll → result. Results expire after 2 hours. We had to build:

  • A polling system with 2.5s intervals and 60s timeout
  • Error parsing that handles both short codes (error_lighting_dark) and human-readable sentences
  • Immediate persistence of results to Vercel Blob before expiry
  • Database tracking of task state for audit and retry

The skin-analysis poll route alone is ~180 lines of error handling and persistence logic.

2. Skin Simulation src Reuse

The skin simulation API needs the original selfie as input. Initially we re-uploaded bytes for each simulation, wasting quota and adding latency. We solved this by:

  • Persisting the src_file_id from the analysis upload on the skinScans row
  • Storing the selfie as a blob URL during the POST (not during polling)
  • Having the simulation POST route read srcFileId from the existing scan row
  • Creating a placeholder skinScans row during POST so the data is available immediately, then updating it when polling completes

3. Catalog vs. Fabric Render Separation

Both catalog garments and fabric templates write to the same outfitRenders table. Without filtering, the fabric tab showed catalog renders (with "upper_body" as the name). We fixed this by:

  • Adding a nullable fabricTemplateId FK to outfitRenders
  • Filtering catalog route by garmentId IS NOT NULL
  • Filtering fabric route by fabricTemplateId IS NOT NULL
  • Filtering the look detail summary by garmentId IS NOT NULL

4. Stylist Agent Hallucination

The LLM occasionally invented garment IDs that didn't exist in the catalog. We added:

  • Zod schema validation on the structured output
  • A defensive filter that drops any selection whose ID doesn't match a real catalog entry
  • Prompt instructions emphasizing "only use garment ids that appear in the catalog"

5. Before/After Toggle UX

Getting per-concern toggles to:

  • Lazily trigger simulation (avoiding 6 concurrent API calls on page load)
  • Show loading state with a spinner
  • Cache results for instant toggling
  • Handle errors gracefully

...required careful state management in a client component with useState and useRef for polling intervals.

6. Fabric Tab Auto-Generation

The fabric renders don't exist until the user switches to the "Fabric" tab. This created a chicken-and-egg problem: the toggle only showed when both modes had renders, but fabric renders couldn't be generated without the toggle. We solved this by:

  • Always showing the toggle when catalog renders exist
  • Auto-triggering fabric generation on first switch to fabric mode
  • Polling fabric tasks every 3s while running
  • Showing "Preparing your outfits…" during generation

7. Next.js App Router Nuances

  • Route groups (dashboard) for shared layout without URL segments
  • Dynamic segments [lookId] with async params (Next.js 16 requirement)
  • Server/client component boundaries for data fetching
  • Middleware-free auth via Clerk's auth() in API routes

8. YouCam API Documentation Gaps

The docs pages render client-side (JavaScript), making them hard to scrape. We had to:

  • Fetch the OpenAPI YAML bundles directly (/_bundle/reference/ai_fabric.yaml)
  • Reverse-engineer exact request/response shapes from the specs
  • Test against the live API to confirm field names and nesting

Accomplishments that we're proud of

End-to-End AI Pipeline

From raw text input ("job interview Friday") to skin analysis to outfit rendering to narrative generation — all orchestrated through real API calls to YouCam and OpenAI. No mock data, no placeholder images.

Hybrid Outfit System

Both real catalog garments and AI-generated fabric styles, switchable in a single carousel view. The user can compare "real blazer" vs. "AI silk print" on their own body.

Per-Concern Before/After Simulation

Each skin concern gets its own toggle that lazily generates a simulation showing what the user's skin would look like with that concern reduced. This makes the abstract concept of "improving your skin" concrete and visual.

Occasion-Aware Styling

The stylist agent doesn't pick random clothes — it reasons about:

  • Formality level (casual vs. business vs. formal)
  • Occasion type (interview, wedding, date, etc.)
  • Skin concerns (e.g., recommending colors that complement redness)
  • Timeframe (aggressive skin prep for 3-day vs. 14-day timelines)

Resilient Infrastructure

  • Every YouCam result persisted to blob before 2-hour expiry
  • Task state tracked in database for audit, retry, and UI polling
  • Errors parsed into user-friendly messages with actionable guidance
  • Placeholder rows inserted during POST so data is available immediately

Clean Architecture

  • Separation of concerns: YouCam client, upload, poll, task-specific modules
  • Reusable polling pattern across 4 task types
  • Agent pipeline designed for extensibility (LangGraph StateGraph)
  • Type-safe throughout (TypeScript + Zod + Drizzle)

What we learned

Async API Orchestration is Hard

Polling, persistence, error handling, and state management across multiple concurrent tasks requires careful architecture. The "persist before expiry" constraint added urgency to every code path. We learned to:

  • Insert placeholder DB rows during POST (don't wait for task completion)
  • Poll aggressively (2.5s) but with reasonable timeouts (60s)
  • Handle network glitches gracefully (keep polling on fetch errors)
  • Parse errors defensively (YouCam returns both codes and sentences)

LLM Agents Need Guardrails

Trusting model output blindly leads to broken UX. We learned to:

  • Use Zod schema validation on structured output
  • Filter hallucinated IDs against a known set
  • Include explicit constraints in system prompts ("only use garment ids that appear in the catalog")
  • Keep temperature low (0.3) for deterministic selection

Database Design Matters Early

Adding columns after the fact (srcFileId, storedSelfieUrl, bodyFileId, fabricTemplateId) required migrations and careful backfilling. We learned to:

  • Design for extensibility upfront (nullable FKs, flexible JSONB columns)
  • Use placeholder rows to avoid race conditions between POST and poll
  • Filter queries by type when multiple entity types share a table

YouCam's API Surface is Rich but Undocumented

The docs render client-side, making them hard to scrape. We learned to:

  • Fetch OpenAPI YAML bundles directly
  • Test against the live API to confirm field names
  • Build typed wrappers that match the actual response shapes
  • Handle both task_status and error fields (task can be "success" with an error)

Hybrid Approaches Beat Either/Or

Keeping both catalog and fabric VTO gave us the best of both worlds:

  • Catalog: real garments for realism and recognizability
  • Fabric: AI styles for creative exploration and variety
  • User can switch between modes and compare

Next.js App Router Has Nuances

  • Route groups for layout sharing
  • Async params in Next.js 16 (must await params)
  • Server vs. client component boundaries
  • useEffect dependency arrays and stale closures in polling

What's next for Mirror

Near-term

  • Skin plan over time — track skin concerns across multiple looks to show progress ("your redness score dropped from 65 to 42 over 3 weeks")
  • Auto-polling on dashboard — refresh look status without manual page reload
  • Error retry — allow users to retry failed tasks without re-uploading photos
  • Outfit comparison view — side-by-side catalog vs. fabric on the same body

Medium-term

  • Wardrobe integration — let users upload their own clothes instead of just the seed catalog
  • Social sharing — share a look (outfit + narrative) with friends for feedback
  • Calendar integration — auto-detect upcoming occasions from Google Calendar and pre-generate looks
  • Multi-outfit comparison — A/B test different outfit options

Long-term

  • Mobile app — native capture with the YouCam Camera Kit for better photo quality
  • Personalized recommendations — learn from user selections to improve future stylist agent picks
  • Accessibility — voice input for occasion text, screen reader support for skin reports
  • Multi-language support — occasion parsing and narratives in multiple languages
  • AR try-on — real-time fabric overlay using device camera

Built with Next.js, YouCam API, LangChain, Drizzle ORM, Clerk, and Vercel Blob.

Built With

  • neon-postgresql
  • nextjs
  • tailwindcss
  • typescript
  • vercel-blob
  • youcam-api
Share this project:

Updates

Submission history