Arquitectura
Stack técnico
| Capa | Tecnología |
|---|---|
Backend (pos-backend) | Node.js 20 · Express 4 · TypeScript (compila src/ → dist/ con tsc) |
| Base de datos | PostgreSQL 17 (Supabase) · acceso vía pg (pool) como service_role |
| Auth | Supabase Auth (@supabase/supabase-js) + JWT propios por reino + TOTP (speakeasy) |
| Frontends | Next.js (monorepo npm workspaces, apps/*) |
| Proceso / deploy | PM2 · Nginx + SSL (Let’s Encrypt/Certbot) por subdominio |
| Pagos | Stripe (anunciantes) · efectivo/tarjeta/transferencia en el POS |
El repositorio es un monorepo npm workspaces (apps/*, packages/*) gestionado con Turbo. Esta documentación (docs-site/) vive fuera del workspace, aislada, para no interferir con las versiones de las apps.
Modelo híbrido PostgreSQL + Supabase
iFondita usa un modelo de dos capas sobre Supabase:
- Datos de negocio — El
pos-backendse conecta a Postgres comoservice_role(bypassa RLS) y aísla por tenant en middleware:requireRestaurantextraerestaurantIddel JWT y todas las queries filtran porrestaurant_id. - Autenticación — Supabase Auth es la fuente de verdad de credenciales. El owner de la fonda se autentica con
signInWithPassword(server-side,persistSession: false); su usuario Auth se liga a la filarestaurantsporauth_user_id. Lo mismo paraadmin_users(plataforma) yadvertisers(anunciantes). - RLS — La base tiene Row Level Security habilitado (Modelo-1:
authenticatedfiltrado porauth.uid()), relevante para el acceso directo del dashboard-owner vía Supabase; elpos-backendlo bypassa por diseño y aísla él mismo.
Proyectos Supabase: staging
pdfkobkatghthmjgdkov· prodoxnwudwgrvcrpqqznxfy(ifondita-prod).
Apps y puertos (PM2)
Cada app corre bajo PM2 y se expone por un subdominio con HTTPS (Nginx + Certbot, renovación automática).
| Subdominio | App | Puerto | Descripción |
|---|---|---|---|
ifondita.com | web (landing) | 3006 | Página pública de iFondita |
api.ifondita.com | pos-backend | 3005 | API del POS + Swagger UI en /api-docs/ |
pos.ifondita.com | pos | 3001 | Frontend del POS (punto de venta para fondas) |
fogon.ifondita.com | admin-central | 3002 | Panel interno Fogón: gestión de campañas, crear manteles, inventario, zonas, configuración |
ads.ifondita.com | advertiser-portal | 3003 | Portal del anunciante: ver progreso de campañas, contratar nuevas |
dashboard.ifondita.com | restaurant-dashboard | 3004 | Dashboard del dueño de restaurante: métricas, configuración |
analytics.ifondita.com | fogon (BI) | 3007 | Fogón BI: analytics de la red de fondas, ventas, tickets, KPIs |
docs.ifondita.com | docs-site (Nextra) | 3009 | Documentación técnica (este sitio) |
n8n.ifondita.com | n8n | 5678 | WhatsApp bot (no activo actualmente) |
Contratos técnicos clave
orders.payment_details (jsonb) — fuente de verdad del dinero
El cobro no usa tablas de pagos normalizadas. Cada orden pagada guarda un objeto payment_details en orders.payment_details:
lines[]descompone siempre en métodos-hoja:cash | card | transfer | online.methoda nivel top-level es el método único, o'mixed'silines.length > 1.SUM(lines[].amount) === total; el cambio (change) sólo puede devolverse en efectivo.- El corte de caja (
cash_sessions.by_payment_method) se reconstruye agregandopayment_details.lines[]de las órdenes pagadas desdeopened_at. - IVA incluido en los precios (
tax = subtotal - subtotal/1.16, informativo).
Auth del owner — Supabase
Login vía supabaseAdmin.auth.signInWithPassword (server-side, persistSession: false, se lee data.user directo del retorno → seguro bajo concurrencia). En el primer acceso (restaurants.auth_user_id nulo) el owner usa /setup-password, que crea el usuario en Supabase Auth y lo liga. El 2FA (totp_*) es una capa TOTP encima de Supabase Auth.
Delivery — order_type + cc_status
Las órdenes de reparto se modelan como columnas de orders: order_type = 'delivery' (o click_collect) con un ciclo de vida en cc_status (pending → accepted → preparing → ready → picked_up), sin mesa (table_id nulo). Las integraciones Rappi/Uber (webhooks HMAC) están diferidas a Nivel 2.
Inventario — best-effort
El descuento de inventario al cobrar es best-effort: envuelto en try/catch, nunca revierte ni bloquea el cobro; un fallo sólo queda visible en logs.