Wishlist — Software Engineering Case Study | Avanie Baptiste
contents

Software Engineering Case Study

your closet,
before it’s your closet.

Wishlist is a multi-surface fashion-planning product with a React web app, a Chrome extension, a relational backend, client-side image segmentation, and server-side generative visualization.

01 save 02 organize 03 style 04 visualize

The interesting engineering challenge was keeping several very different systems feeling like one simple product.

The Wishlist grid and Look Studio side by side

role

Frontend · Full Stack · Product Engineering

surfaces

React Web App · Chrome Extension

core stack

React · Vite · Supabase/PostgreSQL · Chrome MV3 · ONNX/WASM · Gemini

02system

two surfaces. one system.

The extension and web application share the same backend and data model. They do not depend on direct extension-to-app messaging.

retailer page
chrome extensioncontent / background scripts
↓                 ↑
supabaseauth · postgresql · storage · edge functions
↑                 ↓
react web appreact 19 + vite
client-side cv
react
slimsam
onnx runtime wasm
server-side generation
edge function
gemini

web app

Product interaction and wardrobe/outfit management.

extension

Captures product information from retailer pages.

database

Canonical user/item/collection/outfit state.

storage

Images and other file assets where applicable.

edge functions

Trusted server-side operations and external AI requests.

local ml

Interactive segmentation in the browser.

client

interaction · rendering · local inference

extension

extraction · normalization

database

persisted state · authorization

edge functions

secrets · trusted server operations

each boundary exists for a reason: responsiveness, trust, or separation of concerns.

frontend architecture

Product features, browser-specific logic, data access, styling, and expensive dependencies don’t collapse into one monolithic application — each has its own place.

web/src/
├── components/ — shared UI: Header, Sidebar, Footer, modal shell
├── features/
│    ├── wishlist/   screens + wishlist.js + segmentation.js
│    ├── collections/ screens + collections.js + looks.js
│    ├── profile/    screens + profile.js
│    └── auth/       screens + auth.js
└── lib/ — hooks, auth context, shared utilities
extension/ — separate codebase, Manifest V3, vanilla JS

componentization

Screens split into focused components rather than one large page — ProductCard/ProductGrid, WishlistHero/WishlistToolbar, and each modal (Create/Edit/Delete Collection, Prepare Piece, Illustrate Look) own a single file.

feature modularity

Wishlist, Collections, Look Studio, cutout, and Visualize each live inside their own features/ directory with their own screens and service module.

shared vs. feature-specific

components/ holds cross-feature UI; everything else stays owned by the feature that uses it.

web vs. extension

The React app and the Chrome extension are separate codebases and separate build targets, sharing only the Supabase backend — never a component tree.

data / service boundary

Each feature’s Supabase calls live in a plain .js service module (wishlist.js, collections.js, looks.js…), separate from its .jsx components — presentation never calls Supabase directly.

styling

Plain, co-located CSS per component — no CSS-in-JS or utility framework. One shared token file (CSS custom properties) and one shared modal.css base every modal builds on.

load the expensive stuff only when it matters.

The segmentation workflow depends on a relatively heavy ML runtime, but most users don’t need that code during the initial application load. The cutout feature sits behind a dynamic import, so its transformers.js / ONNX-related code loads only when the user opens Prepare Piece.

initial app load
core react experience
user opens prepare piece
dynamic import
load cutout feature
initialize slimsam
reuse model instance

initial path

589.7 KB raw
165.1 KB gzipped

cutout feature chunk

521.4 KB raw
150.1 KB gzipped
loaded on demand

lazy loading keeps ml code off the critical path while preserving the feature when it’s actually needed.

The model initializes once per page session and is reused rather than reloaded for every cutout — route/feature code splitting runs through a plain dynamic import(), no custom bundler config.

03browser engineering

one save button. a web with no standard product page.

Retailer pages expose the same product information through inconsistent markup and data structures.

The Wishlist extension confirming a product was saved from the retailer's product page
product page
json-ld
→ fallback →
open graph / meta
→ fallback →
dom
normalized product
{
  name: string,
  price: number | null,
  currency: string,
  imageUrl: string,
  color: string | null,
  productUrl: string,
  store: string  // hostname, not scraped brand text
}

Extraction isn’t all-or-nothing. Each field resolves independently from its most reliable available source. Product name can prefer structured data, while variant-sensitive values such as price or imagery can prefer the current rendered page.

different storefronts. one product model.

minimum permissions, explicit action

activeTab
storage
scripting

No broad tabs permission, and host permissions are scoped to the Supabase project URL only, not <all_urls> — the extension can read any page but can only network-talk to its own backend. Extraction is user-triggered: content scripts run on every page to make it possible, but they only act when the popup explicitly requests GET_PRODUCT. Persistence happens only after an explicit Save click.

no browsing history

Product data is only extracted while the popup is open.

duplicate saves

A unique constraint on product_url rejects re-saves at the database level; the popup surfaces “Item already saved.”

what if the page doesn’t cooperate?

structured data unavailable

Falls back to Open Graph / meta tags.

metadata incomplete

Falls back to reading the rendered DOM directly.

field still unavailable

Missing required fields (name, image, URL, store) block Save with a clear message, rather than persisting invented data.

Retailer markup changes are an unavoidable maintenance tradeoff of this approach — resilient extraction still needs upkeep as third-party pages evolve.

04data + backend

a closet is relational.

The same product can belong to multiple planning contexts without duplicating its core data.

wishitemsevery saved/manual item
collections
collection_itemsjoin table
looksoutfits
look_itemsitem ↔ outfit relationship + composition state
profiles1:1 with auth.users

context lives in relationships, not duplicate product records.

ownership + integrity

ownership

profiles.user_id references auth.users(id); every other table’s rows are scoped to the authenticated owner via RLS, not a foreign key to profiles.

composition state

Composition-specific state belongs to the outfit-item relationship rather than the underlying product — the product row itself is never duplicated to hold it.

cascades

Deleting a collection or a look cascades to its own join rows (collection_items / look_items) at the database level; the underlying wishitems row is untouched.

duplicates

A unique constraint on product_url prevents the same retailer product being saved twice.

auth / rls

The frontend is not the security boundary.

authentication

Supabase Auth establishes user identity and issues the session JWT.

authorization

PostgreSQL Row Level Security controls which records that identity can access — enforced at the database, not just hidden in the UI.

-- verified policy shape, profiles table
using (auth.uid() = user_id)

the ui decides what to display. postgresql decides what the caller is allowed to access.

05client ai + performance

where should inference happen?

The cutout interaction can require a new mask every time the user adjusts an include/exclude point. Sending each interaction through a remote inference service would introduce network latency and another service dependency, so segmentation runs locally in the browser.

image
points
slimsam
mask
transparent garment
The cutout tool with include/exclude points placed on a product photo

SlimSAM · ONNX Runtime WASM · transformers.js — runs entirely in the browser

model lifecycle

open cutout tool
load feature chunk
initialize model
cache / reuse instance
run segmentation

The model is a module-level singleton — loaded once per page session on first use, then reused for every subsequent Prepare Piece open. A quantized SlimSAM checkpoint keeps the download smaller than the full-precision model. The bundle-size tradeoff behind this is covered in Frontend Architecture, above.

06server ai + security

generation belongs behind a trust boundary.

the client sends intent.
the server retrieves trusted state.

outfit id + style
authenticate
retrieve owned outfit
retrieve required imagery
construct server-controlled request
gemini
result

The browser never constructs the authoritative generation context itself — it only requests one.

why not call gemini directly?

direct client call

  • Exposes API credentials to the browser.
  • Trusts client-provided state.
  • Harder to constrain the request being made.

server function

  • Secret stays server-side, in Edge Function environment.
  • Authenticated caller is verified before any work happens.
  • Outfit data is loaded from persistence, not trusted from the client.
The generated fashion illustration of a composed outfit

a generative failure mode I had to debug

Early generations reproduced physical properties of the style reference image — spiral binding, page edges, a signature — instead of only adopting its illustration style.

cause

The model treated unwanted reference artifacts as part of the requested visual language.

fix

Separated positive style instructions from explicit artifact exclusions in the prompt.

result

Cleaner generated output — not a guarantee of deterministic control.

fetching an external product image safely

The visualization pipeline may need to retrieve an external retailer image. Accepting arbitrary URLs creates a server-side request risk, so the proxy validates the destination before fetching.

url
http/https only
hostname + dns validation
reject private / loopback / metadata
reject redirects
8s timeout + 8MB cap
fetch

Both the hostname and its resolved address are checked, because a safe-looking hostname can still resolve to a private destination.

07reliability

failure is part of the interface.

product data incomplete

Prevent invalid persistence and explain what’s missing, rather than saving a broken item.

duplicate product

A database constraint rejects it; the UI communicates the existing item instead of failing silently.

segmentation inaccurate

Include/exclude prompts let the user correct the mask rather than treating one pass as final.

generation failure

The composed outfit stays intact, and retry is possible without losing the composition.

empty states

An empty collection or filtered result renders explanatory copy and a next action, not a blank grid.

session expired

Returns to authentication instead of rendering a broken authenticated view.

one codebase, two delivery targets.

web

Vite → web build → Vercel.

extension

Vite → extension bundle → packaged Chrome extension — the same React app the web build validates.

backend

Supabase — PostgreSQL, Auth, Storage. Edge Functions run on the Supabase runtime, deployed via the Supabase CLI.

configuration

Client-safe VITE_* variables in the build; server secrets (e.g. the Gemini key) live only in Edge Function environment, never bundled client-side.

where I’d take the system next

observability

Centralized production error reporting around extension extraction, Edge Function failures, and model initialization.

extraction health

Automated monitoring for retailer markup changes that begin reducing extraction success.

resilience

Bounded retry/backoff where temporary external-service failures can safely recover.

performance

Measure model initialization and segmentation behavior on lower-powered devices.

operations

Strengthen release/build validation across the web app, packaged extension, and Edge Functions.

08decisions + outcome

three engineering decisions.

01

normalize the web at the edge.

problemRetailer pages disagree about product structure.
decisionLayered extraction with per-field normalization into one internal product contract.
whyThe rest of the application shouldn’t care which storefront produced the data.
tradeoffThird-party markup remains a maintenance surface.

02

put interactive inference in the browser.

problemSegmentation requires repeated low-latency interaction.
decisionRun SlimSAM through ONNX/WASM locally.
whyEach point adjustment can update without another inference-service round trip.
tradeoffLarger client runtime and model initialization complexity.

03

treat external ai as a server trust boundary.

problemGeneration requires secrets, user-owned state, and external imagery.
decisionUse an authenticated Edge Function to retrieve trusted data, construct requests, and guard outbound fetching.
whyThe browser sends intent rather than authoritative state or credentials.
tradeoffAdds a server round trip and another runtime boundary.

three hats. one product.

frontend / interaction

React app, Look Studio, keyboard interactions, extension UI, segmentation experience.

full stack / systems

PostgreSQL model, Auth/RLS/Storage/Edge Functions, extraction, local ML, generation pipeline.

product / ux

Defined the workflow and interaction model the system supports.

the architecture exists to make the workflow feel simple.

what the user sees

save → organize → style → visualize

what the system does

extract → normalize → relate → infer locally → generate securely

Wishlist started with a fashion problem. Solving it meant making several very different systems behave like one product.

Next deep dive

Interested in the product and experience thinking behind Wishlist?

Product Design / UX Case Study

Research · IA · interaction design · experience iteration · product decisions