MilWebWhatsApp
Back to projects

White-label e-commerce · WhatsApp checkout

Loja de iPhone

1 white-label base → many stores

Problem

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

Solution

Full store with catalog, admin panel for products and stock, and orders straight to WhatsApp. It's a white-label base that becomes many stores by swapping color, logo and contact.

How it was built

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.

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.

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.

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.

White-label by config

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

Per-store inventory

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.

100% WhatsApp checkout

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

RLS on every table

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

More screens

Store home with animated hero and featured catalogCatalog section with per-product stock, condition and installmentsPhysical stores section with address, hours and WhatsApp per unit

Stack

  • Next.js
  • TypeScript
  • Tailwind
  • Supabase
  • Zustand

Ready to turn your idea into a digital product?

Tell me what you need. I reply fast and send a free quote, no strings attached.

  • Service contract
  • Post-launch support
  • You own the code
  • Free quote