MILWEB®
MILWEB®
MW/024LOJA DE IPHONESTUDIO

Loja de iPhone

Loja de iPhone — home screen
STUDIO
MILWEB
TYPE
DIGITAL PRODUCT
STATUS

02 / THE IDEA

White-label e-commerce · WhatsApp checkout

iPhone stores sell only on Instagram, without a proper professional storefront.

03 / EXPERIENCE
  1. 01 / 01

    One file (theme.config.ts) plus environment variables define brand, color and WhatsApp. Each client is the same codebase with different Vercel env vars.

    Loja de iPhone — 01
  2. 02 / 02

    A Postgres trigger sums each physical store's inventory and recalculates products.stock automatically. The app never needs to know if there's 1 store or 10.

    Loja de iPhone — 02
  3. 03 / 03

    No payment gateway: phone normalized (9th-digit rule), cart reconciled against real stock, and a store picker when 2+ units are attending.

    Loja de iPhone — 03
  4. 04 / 04

    No use of service_role anywhere; admin allow-list via a SECURITY DEFINER function, and login IPs hashed (SHA-256) for LGPD compliance.

    Loja de iPhone — 04
Store home with animated hero and featured catalog

04 / UNDER THE HOOD

  • 01Next.js 15 (App Router) with TypeScript, Tailwind v4 with a fully CSS-first theme (no tailwind.config: colors live in an @theme inline block…+

    Next.js 15 (App Router) with TypeScript, Tailwind v4 with a fully CSS-first theme (no tailwind.config: colors live in an @theme inline block in globals.css) and Supabase covering Auth, Postgres and Storage. Multi-client customization isn't done by forking. A single file (src/config/theme.config.ts) defines name, WhatsApp number, logo and palette, which <ThemeInjector> turns into CSS variables (--store-*) on :root, and components only ever consume utilities (bg-bg, text-primary). For reselling at scale, that same theme.config.ts reads NEXT_PUBLIC_STORE_* and NEXT_PUBLIC_COLOR_* env vars with fallbacks, so each new store is the same codebase with different Vercel Environment Variables. The only per-client artifact is the logo file in /public.

  • 02Inventory goes beyond a single field: products.stock is always the total, but two extra tables (units, inventory) enable a multi-physical-st…+

    Inventory goes beyond a single field: products.stock is always the total, but two extra tables (units, inventory) enable a multi-physical-store mode with per-unit stock. A Postgres trigger sums the inventory rows on every change and recalculates the total automatically, so the rest of the app (cart, stock badge, JSON-LD) never needs to know whether the store has one location or ten. Toggling that mode seeds or clears the inventory while preserving the total, with a race guard that checks the newly created unit is genuinely the only one, so two admins can't double the stock by creating the 'first' unit at the same time.

  • 03There's no payment-gateway checkout: the cart lives 100% client-side (Zustand with localStorage persistence) and always closes on WhatsApp. …+

    There's no payment-gateway checkout: the cart lives 100% client-side (Zustand with localStorage persistence) and always closes on WhatsApp. A syncWithServer method reconciles the saved cart with fresh database state on every visit. It drops items that went inactive or out of stock, clamps quantity to available stock, and flags lastSyncChanged so the UI can warn the customer. Every wa.me link goes through normalizeWhatsapp(), which applies Brazil's Anatel 9th-digit rule plus the 55 country code, and through waUrl(), never built by hand. The <WhatsappCta> component alone decides the right number: it goes straight through with 0 or 1 store unit, or opens a picker modal when 2 or more units are attending, each with its own WhatsApp.

  • 04The security posture takes seriously that this white-label becomes a third party's product. RLS is enabled on every table in the public sche…+

    The security posture takes seriously that this white-label becomes a third party's product. RLS is enabled on every table in the public schema, no exceptions, with an admin allow-list in a table with zero policies, reachable only through the SECURITY DEFINER function is_admin(). The service_role key is used nowhere: admin mutations run on the owner's session, with RLS as the final line of defense. Login rate limiting persists in Postgres, so it survives serverless cold starts, and never stores the raw IP: it saves a SHA-256(salt + IP) hash for LGPD compliance. Even the product JSON-LD escapes '<' as \u003c before serializing, so database text containing '</script>' can't inject HTML into the page.

BUILT WITHNext.js · TypeScript · Tailwind · Supabase · ZustandCODE

06 / RESULT

1 white-label base → many stores

[ I WANT SOMETHING LIKE THIS ]

07 / SELECTED SCREENS
Store home with animated hero and featured catalog
SCREEN 01Store home with animated hero and featured catalog
Catalog section with per-product stock, condition and installments
SCREEN 02Catalog section with per-product stock, condition and installments
Physical stores section with address, hours and WhatsApp per unit
SCREEN 03Physical stores section with address, hours and WhatsApp per unit
NEXT EXPERIENCE18 / LOJA DE IPHONE PREMIUM

Loja de iPhone Premium

Loja de iPhone Premium

ALL WORK

ALL WORK