Arquitectura
Documento de arquitectura del monorepo. Acompaña al PRD (sección 13) y al modelo de datos.
1. Visión general
Tres piezas de código y un backend gestionado:
┌─────────────────────────┐
│ packages/shared │
│ @envios/shared │
│ · tipos TypeScript │
│ · esquemas zod │
│ · máquina de estados │
│ · helpers puros (payout)│
└───────────┬─────────────┘
│ import (fuente de verdad)
┌─────────────────┴──────────────────┐
│ │
┌─────────▼─────────┐ ┌─────────▼─────────┐
│ apps/web │ │ apps/mobile │
│ Next.js (TS) │ │ Expo / RN (TS) │
│ empresas + admin│ │ repartidores │
└─────────┬─────────┘ └─────────┬─────────┘
│ │
└──────────────┬──────────────────────┘
│ @supabase/supabase-js
┌────────▼─────────┐
│ Supabase │
│ Postgres+PostGIS │
│ Auth · Storage │
└──────────────────┘
2. Monorepo (pnpm workspaces + Turborepo)
- pnpm workspaces maneja las dependencias y los links entre paquetes locales.
Workspaces declarados en
pnpm-workspace.yaml:apps/*ypackages/*. Las apps referencian el paquete compartido con"@envios/shared": "workspace:*". - Turborepo (
turbo.json) orquesta las tareas (dev,build,lint,typecheck) con caché y respetando el grafo de dependencias (^build). .npmrcfijanode-linker=hoisted: Expo/Metro necesitan unnode_modulesplano para resolver dependencias en un monorepo.- El scope
@envios/*es un codename técnico desacoplado de la marca: no hace falta tocarlo si el producto se renombra (ya pasó una vez: de RutaYa a DaleRuta).
3. packages/shared — la fuente de verdad
Es el corazón del repo. Define una sola vez los contratos del dominio para que web y
mobile no diverjan. Se distribuye como TypeScript sin build (main/types apuntan a
src/index.ts); cada app lo transpila:
- web:
transpilePackages: ['@envios/shared']ennext.config.mjs. - mobile: Metro observa la raíz del workspace (
watchFolders) y lo transpila.
Contenido (ver DATA-MODEL.md para el detalle):
| Módulo | Qué expone |
|---|---|
brand.ts |
resolveAppName(), DEFAULT_APP_NAME (nombre parametrizable). |
enums.ts |
Enums del dominio (z.enum) + tipos inferidos. |
geo.ts |
GeoPoint, Zone, DestinationZone. |
common.ts |
Primitivas: Id, timestamps, money, Dimensions, TimeWindow. |
entities.ts |
Esquemas zod + tipos de todas las entidades + DEFAULT_PLATFORM_CONFIG. |
state-machine.ts |
Transiciones de Package y Shipment, isPayoutReleasable, deriveShipmentStatus. |
payments.ts |
computePayout() (puro) + interfaz PaymentProvider (mock pendiente). |
geocoding.ts |
Interfaz Geocoder + tipos (impl. Nominatim en apps/web/lib/geocoder.ts). |
dto.ts |
CourierPackageView + toCourierPackageView() (omite confirmationCode). |
Patrón zod-first: los esquemas son la fuente de verdad de validación y los tipos
TypeScript se infieren con z.infer, evitando duplicar definiciones.
4. apps/web — Next.js (empresas + back-office)
- App Router, TypeScript, React 19.
- Se despliega en Vercel con Root Directory
apps/web. - Clientes de Supabase con
@supabase/ssr(cookie-based) enapps/web/lib/supabase/:client.ts(browser),server.ts(Server Components / Actions) ymiddleware.ts(refresh de sesión). Así las queries corren bajo el usuario logueado y el RLS aplica. - Etapa 2 (implementado): registro/login de empresa (Supabase Auth, email/password),
publicar un envío con sus paquetes (
/empresa/publicaciones/nueva) y listarlos (/empresa). Los forms se validan con los esquemas zod de@envios/shared(createShipmentInputSchema), y elconfirmationCodese genera en el servidor. - Mapeo DB (snake_case) ↔ tipos de
@envios/shared(camelCase) enlib/mappers.ts; tipos de la base enlib/supabase/types.ts.
5. apps/mobile — Expo (repartidores)
- Expo + React Native, TypeScript, configurado para monorepo
(
metro.config.js:watchFolders+nodeModulesPaths). - Nombre vía
app.config.tsleyendoEXPO_PUBLIC_APP_NAME(slugestable). - Navegación: expo-router (routing por archivos en
app/), organizada por grupos de rol:(auth)(login + registro) y(courier)(tabs del repartidor). El día que las empresas usen la app, suman un grupo(company)sin reescribir la navegación. El guard de auth vive enapp/_layout.tsx(AuthGate): sin sesión →(auth), con sesión →(courier). - Cliente de Supabase en
apps/mobile/lib/supabase.tsconAsyncStoragepara persistir sesión;lib/session.tsxlo expone como contexto (useSession). - Etapa repartidor (implementado): registro completo del courier
(
(auth)/register: datos + vehículo + zona base + zonas de destino sobre el catálogoareas- radio + disponibilidad), login y perfil. El alta valida con
registerCourierInputSchemade@envios/sharedy crea el perfil server-side vía el trigger role-aware. El home lista las rutas disponibles (lib/routes.tssobre la vistacourier_available_packages) filtradas por zona base / zonas declaradas (PRD §6.2), agrupadas por (publicación + destino). Falta tomar la ruta (Assignment) y el matching fino por radio/PostGIS.
- radio + disponibilidad), login y perfil. El alta valida con
6. Backend — Supabase
- Postgres + PostGIS para el matching geográfico (zonas, radios).
- Auth para usuarios (company / courier / admin).
- Storage para fotos de prueba de entrega y etiquetas/QR.
- En esta fase el backend no está modelado en código: sólo se inicializa el cliente por variables de entorno. El esquema de tablas, RLS y queries se incorporan después.
7. Base de datos y migraciones
El esquema vive versionado en supabase/migrations/: tablas
(companies, couriers + courier_destination_areas, areas, shipments, packages,
assignments, platform_config), la vista courier_available_packages, la función de toma
atómica claim_route (FOR UPDATE SKIP LOCKED), PostGIS, RLS explícito con policies + grants
(la auto-exposición de
tablas del proyecto está desactivada) y un trigger on_auth_user_created role-aware que,
según raw_user_meta_data.role, crea el perfil de company (default, alta desde la web) o de
courier (alta desde la app). Cómo aplicarlas: ver el README.
7.b Backend serverless — Edge Functions
Para la monetización por acceso (el repartidor paga para desbloquear el depósito de una
ruta que reservó) hay dos Edge Functions (Deno) en supabase/functions/:
create-pickup-payment (la llama la app con el JWT del courier; crea la preferencia en Mercado
Pago con el token secreto y devuelve el init_point) y mp-webhook (verify_jwt = false; es la
fuente de verdad del pago: re-consulta el pago en la API de MP y, si está approved, marca el
assignment y revela el depósito). El claim es de dos fases: claim_route reserva (pending +
expiración de 15 min); el pago aprobado lo pasa a approved. Regla de oro: el redirect a la app NO
es prueba de pago; el webhook server-to-server sí.
8. Lo que todavía NO está
- Pagos: el flujo de acceso (fee al tomar) está modelado con Edge Functions + Mercado Pago,
pendiente de credenciales/deploy. El escrow/split de la entrega (95/5) sigue sin implementar:
existe
PaymentProvider(interfaz) ycomputePayout(cálculo), pero no hay movimiento de dinero. - Integración con la API de Mercado Envíos (OAuth de la empresa) — ver PRD §9.
- Matching geográfico fino (radio/PostGIS), notificaciones push, pruebas de entrega.
- Tomar una ruta ya está (
claim_route+assignments, con claim atómico). Falta la pantalla de "mis rutas tomadas" y revelar la dirección exacta del depósito sólo tras tomar (hoy el courier ve sólo la zona). - El read del repartidor ya no expone el
confirmationCode: la app lee de la vistacourier_available_packages(sólo columnas seguras). El path de la empresa sigue protegido por RLS; falta el control a nivel columna para el resto de los flujos del courier (pickup/entrega).