Software Engineering Case Study
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.
The interesting engineering challenge was keeping several very different systems feeling like one simple product.
The extension and web application share the same backend and data model. They do not depend on direct extension-to-app messaging.
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.
Product features, browser-specific logic, data access, styling, and expensive dependencies don’t collapse into one monolithic application — each has its own place.
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.
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 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.
Retailer pages expose the same product information through inconsistent markup and data structures.
{
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.
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.”
Falls back to Open Graph / meta tags.
Falls back to reading the rendered DOM directly.
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.
The same product can belong to multiple planning contexts without duplicating its core data.
context lives in relationships, not duplicate product records.
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.
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.
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.
SlimSAM · ONNX Runtime WASM · transformers.js — runs entirely in the browser
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.
the client sends intent.
the server retrieves trusted state.
The browser never constructs the authoritative generation context itself — it only requests one.
direct client call
server function
Early generations reproduced physical properties of the style reference image — spiral binding, page edges, a signature — instead of only adopting its illustration style.
The model treated unwanted reference artifacts as part of the requested visual language.
Separated positive style instructions from explicit artifact exclusions in the prompt.
Cleaner generated output — not a guarantee of deterministic control.
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.
Both the hostname and its resolved address are checked, because a safe-looking hostname can still resolve to a private destination.
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.
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.
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.
01
normalize the web at the edge.
02
put interactive inference in the browser.
03
treat external ai as a server trust boundary.
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.
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.