Sistema de gestión de inventario y fiados para licorerías, multi-empresa y desplegado como una sola aplicación en Vercel.
Cada negocio (organización) ve únicamente sus propios datos. Un administrador de plataforma da de alta las licorerías cliente con su propietario, sin acceder a lo que cada una guarda; dentro de cada licorería los permisos se reparten entre propietario, encargado y cajero.
- Inventario — productos con costo, precio de venta, categoría, proveedor y nivel mínimo de stock, con alertas cuando algo baja del mínimo.
- Movimientos — entradas, salidas y ajustes. Cada movimiento ajusta el stock y queda registrado con su motivo y su fecha.
- Fiados — cuentas de crédito por cliente. Cada abono descuenta del saldo y la cuenta pasa a
partialopaidsola. - Catálogo — categorías y proveedores, propios de cada empresa.
- Panel — total de productos, valor del inventario, alertas de stock bajo y actividad de los últimos 7 días.
- Clientes — alta de licorerías cliente con su propietario, y suspenderlas. Reservado al administrador de plataforma, que no ve los datos de ninguna.
| Capa | Tecnología |
|---|---|
| Frontend | React 18, TypeScript, Vite 7, Tailwind CSS 3, wouter, TanStack Query |
| Componentes | shadcn/ui sobre Radix, lucide-react, Recharts |
| Backend | Express 5 sobre Node, desplegado como función serverless |
| Datos | Supabase (PostgreSQL) con Row Level Security |
| Autenticación | Supabase Auth (JWT), validado en el servidor |
| Validación | Zod, con esquemas compartidos entre cliente y servidor |
| Despliegue | Vercel — estáticos y API en el mismo proyecto |
| CI/CD | GitHub Actions |
El frontend y el backend se organizan por módulo de negocio, no por tipo de archivo. Cada módulo agrupa lo suyo: rutas, esquemas y consultas.
api/
index.ts Punto de entrada serverless en Vercel
backend/
app.ts Construye la app Express (compartida por el servidor local y Vercel)
index.ts Servidor local con Vite en desarrollo
auth.ts Autenticación y contexto de organización
authorization.ts Guardas por rol
db.ts Cliente Supabase con la clave secreta
errors.ts Traducción de errores a respuestas HTTP
storage.ts Acceso a datos, siempre acotado a una organización
platform-service.ts Alta de licorerías cliente y su propietario
modules/
catalog/ Categorías y proveedores
credits/ Fiados
inventory/ Productos y movimientos
platform/ Administración de la plataforma
frontend/src/
App.tsx Rutas y guardas de sesión
lib/ Cliente Supabase, sesión, cliente HTTP
pages/ Login (landing), nueva contraseña, panel, 404
modules/ Una carpeta por módulo, con su página y sus consultas
components/ui/ Componentes de interfaz
components/layout/ Barra lateral
shared/
schema.ts Tablas, esquemas Zod y reglas compartidas
routes.ts Contrato de la API
tenancy.ts Tipos de rol
database/
migrations/ Migraciones numeradas, se aplican en orden
tests/ Comprobaciones sobre las funciones transaccionales
Vercel sirve el frontend compilado como estáticos y ejecuta la misma app Express como función serverless en /api/*. No hace falta alojar el backend aparte.
Dos detalles que conviene conocer antes de tocar el backend:
- Vercel ejecuta la función con el resolvedor ESM nativo de Node. Todo import relativo necesita extensión
.jsy los alias detsconfigno se resuelven. El CI falla si aparece un import relativo sin extensión. - Las variables
VITE_*se incrustan en el bundle del navegador al compilar. El build aborta siVITE_SUPABASE_ANON_KEYcontiene una clave secreta.
El modelo parte de una idea: el navegador no toca la base de datos. El bundle solo usa Supabase para autenticarse; cualquier lectura o escritura pasa por la API de Express, que valida el token, resuelve la organización y aplica el rol.
Aislamiento entre empresas. Cada tabla lleva organization_id. storage.ts acota todas las consultas a la organización del contexto, y claves foráneas compuestas (id, organization_id) impiden a nivel de base de datos que una fila apunte a otra de una empresa distinta.
Row Level Security. Activo en las nueve tablas y vistas. Los privilegios de tabla están revocados para anon y authenticated, así que la clave publicable no puede leer nada aunque quede expuesta — que es su naturaleza, va dentro del bundle.
Autenticación. El token se valida en el servidor contra Supabase en cada petición. La pertenencia a la organización se comprueba en la base de datos, nunca a partir de un claim del token. Una organización suspendida queda sin acceso a la API.
El administrador de plataforma no entra en los datos de nadie. Solo una membresía activa construye el contexto de organización, y su tipo únicamente admite roles de empresa: el compilador impide reintroducir la excepción. Ese rol crea licorerías, las lista y las suspende, y nada más.
Roles.
| Rol | Puede |
|---|---|
platform_admin |
Crear licorerías cliente con su propietario, listarlas y suspenderlas. No accede a los datos de ninguna |
owner / manager |
Leer y escribir productos, categorías, proveedores, movimientos y fiados |
cashier |
Leer, registrar movimientos y cobrar fiados |
Operaciones atómicas. Crear un movimiento, vender fiado y registrar un abono se ejecutan en funciones PostgreSQL con bloqueo de fila, así que dos cajas simultáneas no pueden dejar el stock inconsistente. Esas funciones solo son ejecutables por el rol de servicio.
Sesiones. Viven en sessionStorage: se cierran al cerrar la pestaña y tras 30 minutos sin actividad. La caja suele ser una máquina compartida.
Respuestas y registros. Los errores internos no se devuelven al cliente. Los registros contienen la línea de la petición, nunca el cuerpo de la respuesta. Las respuestas de la API llevan nosniff, X-Frame-Options: DENY, Referrer-Policy y Cache-Control: no-store; el HTML añade una Content Security Policy desde vercel.json.
Claves. SUPABASE_SERVICE_ROLE_KEY es una clave secreta (sb_secret_…) y solo existe en el servidor. VITE_SUPABASE_ANON_KEY es publicable (sb_publishable_…) y es pública por diseño.
Necesitas Node 20 o superior y un proyecto de Supabase.
npm install
cp .env.example .env # y rellena los cuatro valores
npm run dev # http://localhost:5000SUPABASE_URL=https://tu-proyecto.supabase.co
SUPABASE_SERVICE_ROLE_KEY=sb_secret_... # solo servidor
VITE_SUPABASE_URL=https://tu-proyecto.supabase.co
VITE_SUPABASE_ANON_KEY=sb_publishable_... # va al navegador
Las encuentras en Supabase → Settings → API Keys.
En el SQL Editor de Supabase, ejecuta database/migrations/ por número, de la
001 a la última. No hay un volcado del esquema aparte: las migraciones son la
única descripción de la base, y una segunda copia solo serviría para desviarse de
ella sin que nadie lo note.
Cada migración asume aplicada la anterior. Haz copia de seguridad antes de las que mueven datos.
En Authentication → URL Configuration:
- Site URL — la URL de tu despliegue
- Redirect URLs — esa misma URL con
/**
Sin esto, el enlace de recuperación de contraseña lleva al sitio equivocado y Supabase no avisa: acepta el redirect_to y cae en silencio al Site URL.
El servicio de correo integrado de Supabase está limitado a unos pocos envíos por hora y es para pruebas. Para producción, configura SMTP propio en Authentication → SMTP Settings.
npm run dev # Servidor de desarrollo con Vite, puerto 5000
npm run build # Compila el frontend a dist/public
npm run check # Comprueba tipos
npm test # Ejecuta los testsEl esquema de la base no se sincroniza desde código. Se cambia escribiendo una
migración en database/migrations y ejecutándola. Drizzle está aquí solo para
declarar las tablas y derivar de ellas los esquemas de Zod; no consulta la base.
Todas las rutas bajo /api exigen Authorization: Bearer <token>, salvo las de salud. Las que operan sobre datos de una empresa exigen además la cabecera X-Organization-Id.
| Método | Ruta | Rol mínimo |
|---|---|---|
| GET | /api/health, /api/health/database |
público |
| GET | /api/organizations/me |
autenticado |
| POST | /api/account/password |
autenticado |
| GET | /api/products, /api/products/:id |
miembro |
| POST, PUT, DELETE | /api/products, /api/products/:id |
encargado |
| GET | /api/movements |
miembro |
| POST | /api/movements |
cajero |
| GET | /api/categories, /api/suppliers (y /:id) |
miembro |
| POST, PUT, DELETE | /api/categories, /api/suppliers |
encargado |
| GET | /api/credits, /api/credits/stats, /api/credits/customer/:nombre |
miembro |
| POST | /api/credits, /api/credits/payment |
cajero |
| GET | /api/stats |
miembro |
| GET, POST | /api/platform/organizations |
administrador de plataforma |
| PATCH | /api/platform/organizations/:id/status |
administrador de plataforma |
El proyecto está configurado para Vercel mediante vercel.json:
- Build
vite build· Salidadist/public· Framework Other /api/*se reescribe a la función serverless; el resto sirve el SPA
Configura las cuatro variables de entorno en el proyecto de Vercel antes del primer build: las VITE_* se incrustan al compilar, no se leen en tiempo de ejecución.
.github/workflows/ci-cd.yml ejecuta en cada push y cada pull request: instalación, comprobación de tipos, tests y build. Además falla si:
- la hoja de estilos generada baja de 20 kB, señal de que Tailwind no encuentra los archivos fuente;
- aparece un import relativo sin extensión
.js, que rompería la función en Vercel.
Los tres guards nacieron de fallos reales que pasaban tipos, tests y build sin quejarse y solo se manifestaban en producción.
El job de despliegue está inactivo salvo que definas la variable de repositorio DEPLOY_VIA_ACTIONS a true. Por defecto despliega la integración de Git de Vercel; activar ambos duplicaría los despliegues.
.github/workflows/supabase-keepalive.yml llama a diario al endpoint de salud para que el proyecto de Supabase no se pause por inactividad. Usa la variable APP_URL y no necesita credenciales.
| Archivo | Contenido |
|---|---|
MODULAR_ARCHITECTURE.md |
Límites entre módulos |
SAAS_ARCHITECTURE.md |
Modelo multi-empresa |
AUTH_SETUP.md |
Autenticación y roles |
DEPLOYMENT.md |
Notas de despliegue |
TESTING.md |
Estrategia de pruebas |
MIT