Arquitectura

Arquitectura

Stack técnico

CapaTecnología
Backend (pos-backend)Node.js 20 · Express 4 · TypeScript (compila src/dist/ con tsc)
Base de datosPostgreSQL 17 (Supabase) · acceso vía pg (pool) como service_role
AuthSupabase Auth (@supabase/supabase-js) + JWT propios por reino + TOTP (speakeasy)
FrontendsNext.js (monorepo npm workspaces, apps/*)
Proceso / deployPM2 · Nginx + SSL (Let’s Encrypt/Certbot) por subdominio
PagosStripe (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-backend se conecta a Postgres como service_role (bypassa RLS) y aísla por tenant en middleware: requireRestaurant extrae restaurantId del JWT y todas las queries filtran por restaurant_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 fila restaurants por auth_user_id. Lo mismo para admin_users (plataforma) y advertisers (anunciantes).
  • RLS — La base tiene Row Level Security habilitado (Modelo-1: authenticated filtrado por auth.uid()), relevante para el acceso directo del dashboard-owner vía Supabase; el pos-backend lo bypassa por diseño y aísla él mismo.

Proyectos Supabase: staging pdfkobkatghthmjgdkov · prod oxnwudwgrvcrpqqznxfy (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).

SubdominioAppPuertoDescripción
ifondita.comweb (landing)3006Página pública de iFondita
api.ifondita.compos-backend3005API del POS + Swagger UI en /api-docs/
pos.ifondita.compos3001Frontend del POS (punto de venta para fondas)
fogon.ifondita.comadmin-central3002Panel interno Fogón: gestión de campañas, crear manteles, inventario, zonas, configuración
ads.ifondita.comadvertiser-portal3003Portal del anunciante: ver progreso de campañas, contratar nuevas
dashboard.ifondita.comrestaurant-dashboard3004Dashboard del dueño de restaurante: métricas, configuración
analytics.ifondita.comfogon (BI)3007Fogón BI: analytics de la red de fondas, ventas, tickets, KPIs
docs.ifondita.comdocs-site (Nextra)3009Documentación técnica (este sitio)
n8n.ifondita.comn8n5678WhatsApp 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.
  • method a nivel top-level es el método único, o 'mixed' si lines.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 agregando payment_details.lines[] de las órdenes pagadas desde opened_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.