🇺🇸 English · 🇪🇸 Español
🟢 In production — first customer confirmed · Continuous deployment on Vercel
🌐 Public demo: vadelivery.vercel.app
Local delivery marketplace (à la PedidosYa / Rappi) built for a small city. Stack: Next.js (App Router) · Supabase · PostgreSQL · TailwindCSS · TypeScript · Vercel.
- Node.js 20+
- pnpm (
npm i -g pnpm) - Free Supabase account
- A test account on Mercado Pago Developers
pnpm install
cp .env.example .env.local- https://supabase.com/dashboard → New project
- Settings → API → copy Project URL, anon key and service_role key into
.env.local
SQL Editor → New query → paste supabase/schema.sql → Run.
Then run supabase/seed/seed.sql for demo data.
- Authentication → Providers → enable Email (OTP provider).
- Authentication → URL Configuration → Site URL:
http://localhost:3000. - Authentication → Email Templates → optional: translate the copy.
- In the MP developers panel, create an application.
- Copy
MP_ACCESS_TOKENandMP_PUBLIC_KEYto.env.local(use TEST credentials). - For local webhooks: use ngrok (
ngrok http 3000) and configurehttps://XXX.ngrok.io/api/webhooks/mercadopagoin MP → Webhooks → select the "Payments" event. - Copy the webhook secret into
MP_WEBHOOK_SECRET.
pnpm db:types
pnpm devDesign decisions (stack, security, webhook idempotency, realtime, known trade-offs) are documented in docs/ARCHITECTURE.md.
- Full folder structure, route groups
- Design system: coral palette + green accent + stone neutrals, Geist typography
- Complete SQL schema (13 migrations, role-based RLS, idempotent RPC)
- Seed with 5 stores + 25 demo products
- Home, store page (SSR catalog + ISR), 404, error boundary
- Shop components: StoreCard, ProductCard, CategoryPill, PromoBanner
- Passwordless login with 6-digit OTP
- Session helpers and RBAC (
getSession,requireAuth,requireRole) - next-safe-action with
action,authAction,adminAction - 5-step store onboarding: data → address → operation → products → publish
- Panel layout with sidebar (desktop) + bottom nav (mobile)
- Server Actions: stores, products
- OTP delivery via Brevo SMTP (shared subdomain
brevosend.com; with a custom domain + DKIM the sender would benoreply@<domain>— details in ARCHITECTURE.md)
- Zustand store persisted in localStorage
- "Single-store cart" logic (switch modal)
- ProductCard with add-to-cart + feedback animation
- Cart page with summary, quantity controls, minimum-order validation
- Sticky CartFloatingButton
💳 Mercado Pago — full integration
- Checkout with preference creation (
createPreference) and redirect to the official MP flow - Webhook at
/api/webhooks/mercadopagothat re-queries the payment from MP's API (getPayment) before touching the DB — mitigates fake-webhook injection even without HMAC apply_payment_webhookRPC in Postgres: idempotent (INSERT ... ON CONFLICT), survives retries without duplicating orders- Maps MP status → internal enum (
pending/approved/rejected/cancelled) - HMAC verification pending — documented as a conscious decision in ARCHITECTURE.md
🛒 Secure order pricing
- Pricing service: subtotal / total / commission computed 100% server-side
createOrderAction: never trusts client-side prices, reads them from the DB by IDCheckoutForm: address + payment method + notes, on a single screen
📡 Realtime tracking
useOrderRealtime: hook subscribed topostgres_changes(Supabase Realtime), no pollingOrderTracker: 5-step visual stepper with animations/pedido/[id]page: live tracking + full detail + store contact- Store actions: accept / mark ready / reject
- Store panel: live KDS for incoming orders, product CRUD with images and modifier groups (combos, options, min/max), promotions, sales statistics, operating hours
- Multi-store switcher: owners with several locations switch stores from a selector in the panel
- WhatsApp notifications to store owner: on every new order via Meta Cloud API with an approved template (
nuevo_pedido_es_MX) — system user with a non-expiring token - Admin panel: management of stores, drivers, users, finance and orders
- Driver app: available orders, accept/reject, status updates (
/driver/disponibles,/driver/activo) - Driver tracking on map: live position client ↔ client via Supabase Realtime broadcast
- Transactional email for confirmation / status changes and web push
- MP webhook HMAC verification (known gap — documented)
- Tests: Vitest + Playwright setup (in progress, part of the author's testing course)
src/
├── app/
│ ├── (auth)/ Login + signup (OTP)
│ ├── (shop)/ Consumer marketplace
│ │ ├── page.tsx Home
│ │ ├── s/[storeSlug]/ Store page
│ │ ├── carrito/
│ │ ├── checkout/ ← block 4
│ │ └── pedido/[id]/ ← block 4 (tracking)
│ ├── (account)/ Logged-in customer area
│ ├── comercio/
│ │ ├── onboarding/ 5 steps
│ │ └── (panel)/ Main panel
│ ├── driver/ Driver app
│ ├── admin/
│ └── api/webhooks/mercadopago/ ← block 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 ← block 4
│ ├── order/ order-tracker, order-tracker-live ← block 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 ← block 4
│ └── services/
│ ├── pricing.service.ts ← block 4
│ └── mercadopago.service.ts ← block 4
│
├── lib/
│ ├── supabase/ client, server, admin
│ └── utils.ts
│
├── stores/cart.ts Zustand
├── schemas/ Zod
├── hooks/use-order-realtime.ts ← block 4
├── styles/globals.css
└── middleware.ts
supabase/
├── migrations/ 13 files
├── schema.sql concatenated
├── seed/seed.sql 5 stores + 25 products
└── config.toml
- Customer builds cart →
/checkout - Confirms →
createOrderAction:- Loads real products from the database (does not trust client-side prices)
- Applies promotion if a code was passed
- Computes pricing server-side
- Inserts
orders+order_items+payments(statuspending)
- If cash → status
pending→ store accepts and moves topreparing - If Mercado Pago:
- Creates a preference with
external_reference = order.id - Redirects to
init_point - Customer pays in MP
- Webhook
/api/webhooks/mercadopagoreceives the notification getPayment(id)against MP to confirm the payment (HMAC pending)- Calls RPC
apply_payment_webhook(idempotent):- Upsert in
paymentsbymp_payment_id - If
approved→orders.payment_status='approved',status='confirmed', creates adeliveriesrow
- Upsert in
- Creates a preference with
- Store accepts →
preparing→ready - Driver claims →
picked_up→delivered→completed - Realtime: the customer sees changes on
/pedido/[id]without refreshing
| Token | Hex | Usage |
|---|---|---|
primary-500 |
#FF4D3A | Brand |
primary-600 |
#E63823 | CTAs |
accent-500 |
#22C55E | Free shipping, success |
warning-500 |
#F59E0B | % off promos |
neutral-900 |
#1C1917 | Headings |
neutral-500 |
#78716C | Secondary text |
Typography: Geist. Mobile-first: container max-w-screen-sm.
| Role | Can |
|---|---|
customer |
Browse, order, see own orders, cancel before acceptance |
store_owner |
Everything for their store + accept/reject/mark ready |
store_staff |
Whatever is allowed in store_users |
delivery_driver |
Claim available orders, update statuses |
admin |
Everything |
RLS enforces rules at the database level. Critical mutations go through Server Actions with service_role after permission validation.
pnpm dev # next dev
pnpm build # next build
pnpm lint
pnpm type-check
pnpm db:types # regenerates src/types/database.types.ts
pnpm db:seed # runs seed.sql- Sandbox: use TEST credentials and the test buyer account (created in MP Developers).
- Local webhook:
ngrok http 3000and paste the HTTPS URL in MP → Webhooks. - Idempotency: delete
paymentsrows with the samemp_payment_idto reproduce.
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=your_secret
Juan M. — Full-stack developer with a product mindset.
- GitHub: @Juanmd14
Built as an end-to-end case study: architecture, data, auth, payments, realtime and UX, keeping the stack scoped to real production tools.
MIT © 2026 Juan M.

