🇪🇸 Español · 🇺🇸 English
🟢 En producción — primer cliente confirmado · Deploy continuo en Vercel
🌐 Demo pública: vadelivery.vercel.app
Plataforma de delivery local tipo PedidosYa/Rappi para una ciudad pequeña. Stack: Next.js (App Router) · Supabase · PostgreSQL · TailwindCSS · TypeScript · Vercel.
La instancia en vadelivery.vercel.app está en producción con un cliente real, así que sólo se puede recorrer la UI del marketplace en modo navegación (home, fichas de comercio, catálogo, carrito) — no se realizan pedidos de prueba para no contaminar la base del cliente.
Para ver el flujo completo (checkout con Mercado Pago, tracking en vivo, panel del comercio con KDS, app del repartidor) hay dos caminos:
- 📹 Pedirme un walkthrough en video o una llamada corta donde lo recorro contra una instancia de staging.
- 🛠️ Clonarlo local con el setup de abajo (15 min): trae 5 comercios + 25 productos en el seed y se puede probar punta a punta con credenciales TEST de MP.
- Node.js 20+
- pnpm (
npm i -g pnpm) - Cuenta gratis en Supabase
- Cuenta de testing en Mercado Pago Developers
pnpm install
cp .env.example .env.local- https://supabase.com/dashboard → New project
- Settings → API → copiar Project URL, anon key y service_role key a
.env.local
SQL Editor → New query → pegar supabase/schema.sql → Run.
Después correr supabase/seed/seed.sql para datos demo.
- Authentication → Providers → habilitar Email (provider OTP).
- Authentication → URL Configuration → Site URL:
http://localhost:3000. - Authentication → Email Templates → opcional: traducir el copy.
- En el panel de developers MP, crear una aplicación.
- Copiar
MP_ACCESS_TOKENyMP_PUBLIC_KEYa.env.local(usar credenciales TEST). - Para webhooks en local: usar ngrok (
ngrok http 3000) y configurarhttps://XXX.ngrok.io/api/webhooks/mercadopagoen MP → Webhooks → seleccionar evento "Pagos". - Copiar el secret de webhook a
MP_WEBHOOK_SECRET.
pnpm db:types
pnpm devLas decisiones de diseño (stack, seguridad, idempotencia de webhooks, realtime, trade-offs conocidos) están explicadas en docs/ARCHITECTURE.md.
- Estructura completa de carpetas, route groups
- Sistema de diseño: paleta coral + acento verde + neutros stone, tipografía Geist
- Schema SQL completo (13 migraciones, RLS por rol, RPC idempotente)
- Seed con 5 comercios + 25 productos demo
- Home, ficha comercio (catálogo SSR + ISR), 404, error boundary
- Componentes shop: StoreCard, ProductCard, CategoryPill, PromoBanner
- Login passwordless con OTP de 6 dígitos
- Helpers de sesión y RBAC (
getSession,requireAuth,requireRole) - next-safe-action con
action,authAction,adminAction - Onboarding del comercio en 5 pasos: datos → dirección → operación → productos → publicar
- Layout panel con sidebar (desktop) + bottom nav (móvil)
- Server Actions: stores, products
- Envío de OTP por Brevo SMTP (subdominio compartido
brevosend.com; con dominio propio + DKIM el sender quedaría comonoreply@<dominio>— detalle en ARCHITECTURE.md)
- Zustand store persistido en localStorage
- Lógica de "carrito por comercio único" (modal de switch)
- ProductCard con add to cart + animación de feedback
- Página de carrito con resumen, control de cantidades, validación de mínimo
- CartFloatingButton sticky
💳 Mercado Pago — integración completa
- Checkout con creación de preferencia (
createPreference) y redirect al flow oficial de MP - Webhook en
/api/webhooks/mercadopagoque re-consulta el pago a la API de MP (getPayment) antes de actualizar BD — mitiga inyección de webhooks falsos sin HMAC - RPC
apply_payment_webhooken Postgres: idempotente (INSERT ... ON CONFLICT), sobrevive reintentos sin duplicar pedidos - Mapeo de estados MP → enum interno (
pending/approved/rejected/cancelled) - Verificación HMAC pendiente — documentado como decisión consciente en ARCHITECTURE.md
🛒 Order pricing seguro
- Pricing service: subtotal / total / comisión calculados 100% server-side
createOrderAction: nunca confía en precios del cliente, los lee desde BD por IDCheckoutForm: dirección + método pago + notas, en una sola pantalla
📡 Realtime tracking
useOrderRealtime: hook con suscripción apostgres_changes(Supabase Realtime), sin pollingOrderTracker: stepper visual de 5 pasos con animaciones- Página
/pedido/[id]: tracking en vivo + detalle completo + contacto comercio - Acciones del comercio: aceptar / marcar listo / rechazar
- Panel comercio: KDS de pedidos en vivo, CRUD de productos con imágenes y modifier groups (combos, opciones, mínimos/máximos), promociones, estadísticas de ventas, horarios de operación
- Multi-store switcher: owners con varios locales cambian de comercio desde un selector en el panel
- Notificaciones WhatsApp al comercio: en cada pedido nuevo vía Meta Cloud API con template aprobado (
nuevo_pedido_es_MX) — system user con token sin expiración - Panel admin: gestión de comercios, repartidores, usuarios, finanzas y pedidos
- App del repartidor: pedidos disponibles, aceptar/rechazar, marcar estados (
/driver/disponibles,/driver/activo) - Tracking del repartidor en mapa: posición en vivo cliente ↔ cliente vía Supabase Realtime broadcast
- Email transaccional para confirmación / cambios de estado y push web
- Verificación HMAC del webhook MP (gap conocido — documentado)
- Tests: setup de Vitest + Playwright (en progreso, parte del curso de testing del autor)
src/
├── app/
│ ├── (auth)/ Login + registro (OTP)
│ ├── (shop)/ Marketplace cliente final
│ │ ├── page.tsx Home
│ │ ├── s/[storeSlug]/ Ficha de comercio
│ │ ├── carrito/
│ │ ├── checkout/ ← bloque 4
│ │ └── pedido/[id]/ ← bloque 4 (tracking)
│ ├── (account)/ Zona logueada del cliente
│ ├── comercio/
│ │ ├── onboarding/ 5 pasos
│ │ └── (panel)/ Panel principal
│ ├── driver/ App del repartidor
│ ├── admin/
│ └── api/webhooks/mercadopago/ ← bloque 4
│
├── components/
│ ├── ui/ button, input, label, switch, form-field
│ ├── shop/ store-card, product-card, category-pill, promo-banner
│ ├── cart/ cart-floating-button
│ ├── checkout/ checkout-form ← bloque 4
│ ├── order/ order-tracker, order-tracker-live ← bloque 4
│ ├── store-admin/ onboarding-{stepper,basic,address,operation,products,publish}
│ └── shared/ shop-header, bottom-nav, login-form
│
├── server/
│ ├── auth/session.ts
│ ├── actions/
│ │ ├── safe-action.ts clients
│ │ ├── auth.ts
│ │ ├── stores.ts
│ │ ├── products.ts
│ │ └── orders.ts ← bloque 4
│ └── services/
│ ├── pricing.service.ts ← bloque 4
│ └── mercadopago.service.ts ← bloque 4
│
├── lib/
│ ├── supabase/ client, server, admin
│ └── utils.ts
│
├── stores/cart.ts Zustand
├── schemas/ Zod
├── hooks/use-order-realtime.ts ← bloque 4
├── styles/globals.css
└── middleware.ts
supabase/
├── migrations/ 13 archivos
├── schema.sql concatenado
├── seed/seed.sql 5 comercios + 25 productos
└── config.toml
- Cliente arma carrito →
/checkout - Confirma →
createOrderAction:- Trae productos reales de la BD (no confía en precios del cliente)
- Aplica promoción si vino código
- Calcula pricing server-side
- Inserta
orders+order_items+payments(statuspending)
- Si efectivo → status
pending→ comercio acepta y pasa apreparing - Si Mercado Pago:
- Crea preferencia con
external_reference = order.id - Redirige al
init_point - Cliente paga en MP
- Webhook
/api/webhooks/mercadopagorecibe notificación getPayment(id)contra MP para confirmar el pago (HMAC pendiente)- Llama RPC
apply_payment_webhook(idempotente):- Upsert en
paymentspormp_payment_id - Si
approved→orders.payment_status='approved',status='confirmed', crea filadeliveries
- Upsert en
- Crea preferencia con
- Comercio acepta →
preparing→ready - Repartidor toma →
picked_up→delivered→completed - Realtime: el cliente ve los cambios en
/pedido/[id]sin refrescar
| Token | Hex | Uso |
|---|---|---|
primary-500 |
#FF4D3A | Marca |
primary-600 |
#E63823 | CTAs |
accent-500 |
#22C55E | Envío gratis, success |
warning-500 |
#F59E0B | Promos % off |
neutral-900 |
#1C1917 | Títulos |
neutral-500 |
#78716C | Texto secundario |
Tipografía: Geist. Mobile-first: container max-w-screen-sm.
| Rol | Puede |
|---|---|
customer |
Navegar, pedir, ver sus pedidos, cancelar antes de aceptación |
store_owner |
Todo lo de su comercio + aceptar/rechazar/marcar listo |
store_staff |
Lo permitido en store_users |
delivery_driver |
Tomar pedidos disponibles, marcar estados |
admin |
Todo |
RLS aplica reglas a nivel BD. Mutaciones críticas vía Server Action con service_role previa validación de permisos.
pnpm dev # next dev
pnpm build # next build
pnpm lint
pnpm type-check
pnpm db:types # regenera src/types/database.types.ts
pnpm db:seed # corre seed.sql- Sandbox: usar credenciales TEST y la cuenta de comprador de test (creada en MP Developers).
- Webhook local:
ngrok http 3000y pegar la URL HTTPS en MP → Webhooks. - Idempotencia: borrar
paymentscon el mismomp_payment_idpara reproducir.
NEXT_PUBLIC_APP_URL=http://localhost:3000
NEXT_PUBLIC_APP_NAME=Trae App
NEXT_PUBLIC_SUPABASE_URL=https://XXX.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...
SUPABASE_SERVICE_ROLE_KEY=eyJ...
MP_ACCESS_TOKEN=TEST-...
MP_PUBLIC_KEY=TEST-...
MP_WEBHOOK_SECRET=tu_secret
Juan M. — Desarrollador full-stack enfocado en producto.
- GitHub: @Juanmd14
Proyecto construido como caso end-to-end: arquitectura, datos, auth, pagos, realtime y UX, manteniendo el stack acotado a herramientas de producción reales.
MIT © 2026 Juan M.

