Project brief
Highlights
Domain Model
Production-grade TypeScript interfaces across the full commerce schema
Eight TypeScript interfaces define the commerce domain: SSAUser, Product, ProductImage, Subscription, Shipment, Address, Profile, and BeautyBoxCartItem. Field names, nullability, and relationship IDs match the conventions a Supabase-backed schema would expose — nullable recipient phones, optional address lines, is_primary and sort_order on product images, and enum-constrained status fields on subscriptions and shipments.
The product model is the most layered: a sorted image array (is_primary + sort_order), multi-valued skin_types and skin_concern string arrays, and nullable cost and retail fields that reflect real sourcing data quality. The structural match means the demo objects are a direct lift into production code, not a simplified placeholder that would require a rewrite.
Demo Infrastructure
Supabase interface stub, synchronous auth, and a deterministic fixture graph
Three production dependencies are replaced at the interface boundary. The Supabase client (supabase.ts) defines the full SSAAuthClient and generic SSAQueryBuilder<T> interfaces — with correct method signatures for select, eq, order, limit, single, and upsert — but getSupabaseClient() returns null. Component code that imports supabase compiles against the real interface without bundling the library or making any network call.
The auth hook (useRequireAuth) returns DEMO_USER synchronously, eliminating async auth resolution. The fixture graph in demo-data.ts covers every domain object with consistent cross-referenced IDs: DEMO_USER → DEMO_PROFILE → DEMO_ADDRESSES → DEMO_SUBSCRIPTIONS → DEMO_SHIPMENTS → DEMO_PRODUCTS. Relationship fields (shipping_address_id, subscription_id, user_id) resolve correctly across the my/ account pages without a backend.
Design System
CSS custom properties consumed through a typed runtime hook
The visual language is expressed in 20+ CSS custom properties scoped to the SSA context: --ssa-sage, --ssa-espresso, --ssa-cream, --ssa-forest, --ssa-forest-deep, --ssa-card-green, --ssa-sand, and more. The useTheme() hook reads these values at runtime and returns a typed ThemeTokens object used directly in inline style expressions across every component. No CSS class generation, no build-time processing, and no stylesheet interference with the host application.
Typography is centralized in keepBrandTerms() and keepBrandTermsDeep() utilities that protect brand strings from automatic capitalization or translation transforms. The i18n system loads server-side typed dictionary files for five locales (ko, en, ja, de, fr) in server components and passes them to client islands via props, requiring no client-side locale resolution at runtime.
State Machine
Skin profile from the quiz persists as a typed field into the cart item
The purchase flow is a typed hand-off chain: the skin quiz writes the result to sessionStorage; SubscribeClient reads it on mount and passes it to buildBeautyBoxCartItem(), which embeds it in BeautyBoxCartItem.skinType; the cart serializes the item to localStorage; CartClient deserializes it and forwards skinType to the checkout success URL. Each boundary is a typed transition.
Plan selection is driven by BEAUTY_BOX_PLANS — a typed config array with price, numericPrice, and showKit flags — and normalizeBeautyBoxPlanId() coerces arbitrary URL param input (including the legacy onetime variant) to the canonical plan ID. The success page reads from the URL param rather than from upstream component state, making it stateless with respect to the subscription flow.
This case study explains the implementation intent and technical structure. The demo lets you review product discovery, skin profile, cart, box composition, account, order, and shipment states without connecting production data or server writes.
See demo