20 de 25 · PRODUCTO DIGITAL
Loja de iPhone
E-commerce white-label · checkout por WhatsApp
PROYECTO PROPIO

CONTEXTO
Las tiendas de iPhone venden solo por Instagram, sin una vitrina propia y profesional.
QUÉ SE HIZO
Tienda completa con catálogo, panel admin de productos e inventario, y pedido directo por WhatsApp. Es una base white-label que se convierte en varias tiendas cambiando solo color, logo y contacto.
1 BASE WHITE-LABEL → VARIAS TIENDAS
SOLUCIONES
White-label por config
Un archivo (theme.config.ts) y variables de entorno definen marca, color y WhatsApp. Cada cliente es el mismo código con env vars distintas en Vercel.
Inventario por sucursal
Un trigger en Postgres suma el inventory de cada tienda física y recalcula products.stock automáticamente. La app nunca sabe si hay 1 o 10 tiendas.
Checkout 100% WhatsApp
Sin pasarela: número normalizado (regla del 9.º dígito), carrito reconciliado con el inventario real y selector de tienda cuando hay 2+ sucursales atendiendo.
RLS en toda tabla
Ningún uso de service_role; allow-list de admins vía función SECURITY DEFINER e IP de login con hash (SHA-256) para la LGPD.
DETALLE TÉCNICO
01Next.js 15 (App Router) con TypeScript, Tailwind v4 con tema 100% CSS-first (sin tailwind.config: los colores viven en @theme inline en glob…
Next.js 15 (App Router) con TypeScript, Tailwind v4 con tema 100% CSS-first (sin tailwind.config: los colores viven en @theme inline en globals.css) y Supabase cubriendo Auth, Postgres y Storage. La personalización multicliente no se hace por fork. Un único archivo (src/config/theme.config.ts) define nombre, WhatsApp, logo y paleta, que <ThemeInjector> convierte en CSS variables (--store-*) en el :root, y los componentes solo consumen utilidades (bg-bg, text-primary). Para reventa en serie, el mismo theme.config.ts lee variables NEXT_PUBLIC_STORE_* y NEXT_PUBLIC_COLOR_* con fallback: cada tienda nueva es el mismo código con Environment Variables distintas en Vercel. El único artefacto por cliente es el archivo del logo en /public.
02El inventario va más allá de un campo único: products.stock es siempre el total, pero dos tablas extra (units, inventory) habilitan un modo …
El inventario va más allá de un campo único: products.stock es siempre el total, pero dos tablas extra (units, inventory) habilitan un modo multitienda física, con inventario por sucursal. Un trigger en Postgres suma el inventory en cada cambio y recalcula el total automáticamente, así que el resto de la aplicación (carrito, badge de inventario, JSON-LD) nunca necesita saber si la tienda tiene una o diez sucursales. Activar o desactivar ese modo siembra o limpia el inventario preservando el total, con una guarda contra condiciones de carrera que verifica que la sucursal recién creada sea de hecho la única, para que dos admins no dupliquen el inventario creando la 'primera' tienda al mismo tiempo.
03No hay checkout con pasarela: el carrito vive 100% en el cliente (Zustand con persist en localStorage) y el cierre es siempre por WhatsApp. …
No hay checkout con pasarela: el carrito vive 100% en el cliente (Zustand con persist en localStorage) y el cierre es siempre por WhatsApp. Un método syncWithServer reconcilia el carrito guardado con el estado fresco de la base de datos en cada visita. Elimina ítems que quedaron inactivos o sin inventario, limita la cantidad al inventario disponible y marca lastSyncChanged para que la UI avise al cliente. Todo link wa.me pasa por normalizeWhatsapp(), que aplica la regla del noveno dígito de Anatel y el código de país 55, y por waUrl(), nunca armado a mano. El componente <WhatsappCta> decide solo el número correcto: va directo si la tienda tiene 0 o 1 sucursal, o abre un selector modal cuando hay 2 o más tiendas atendiendo, cada una con su propio WhatsApp.
04La postura de seguridad toma en serio que este es un white-label que se convierte en producto de terceros. RLS habilitado en toda tabla del …
La postura de seguridad toma en serio que este es un white-label que se convierte en producto de terceros. RLS habilitado en toda tabla del schema público, sin excepción, con una allow-list de admins en una tabla sin ninguna policy, accesible solo vía la función SECURITY DEFINER is_admin(). La service_role no se usa en ningún lado: las mutaciones del admin corren con la sesión del dueño, con el RLS como defensa final. El rate limit de login persiste en Postgres, así que sobrevive al cold start serverless, y nunca guarda la IP en claro: almacena un SHA-256(salt + IP), por exigencia de la LGPD. Hasta el JSON-LD del producto escapa '<' como \u003c antes de serializar, para que un texto venido de la base con '</script>' no inyecte HTML en la página.
PANTALLAS

01 Home de la tienda con hero animado y catálogo de destacados 
02 Sección de catálogo con inventario, condición y cuotas por producto 
03 Sección de tiendas físicas con dirección, horario y WhatsApp por sucursal
RESULTADO
Tienda completa con catálogo, panel admin de productos e inventario, y pedido directo por WhatsApp. Es una base white-label que se convierte en varias tiendas cambiando solo color, logo y contacto.












