Web & PWA · Diseño detallado
Servicare — portal B2B
Portal de empresas afiliadas para salud ocupacional: aislamiento por RLS en el motor, 2FA, API para ERP y Wompi SAQ A.
- Next.js 16
- Supabase/Postgres RLS
- 2FA TOTP
- API REST (Bearer + IP allowlist)
- Wompi (SAQ A)
Plataforma web y portal de empresas afiliadas para una compañía de salud ocupacional en Colombia. Datos sensibles, un ERP de terceros que tiene que leer y escribir, pagos, y la Ley 1581 (Habeas Data) encima. El valor no es el stack — es dónde puse las fronteras de confianza. Diseño detallado y propuesta técnica; la capa pública está definida y la capa autenticada está especificada a nivel de implementación.
Stack: Next.js 16 (App Router) · Supabase / Postgres RLS · Auth + 2FA TOTP · API REST /api/v1 · Wompi (SAQ A) · CMS headless · Estado: diseño detallado + propuesta (capa autenticada por construir) · Rol: arquitectura + desarrollo (solo)
El problema
La empresa opera dos verticales bajo una marca —una IPS de salud ocupacional y un centro de reconocimiento de conductores— y movía todo en papel, llamadas y archivos sueltos. No había canal autenticado para las empresas afiliadas, ni forma de que su ERP existente (un sistema de terceros) leyera lo que entraba por la web.
Dos necesidades: un sitio institucional que capte y convierta, y un portal donde cada empresa afiliada vea solo lo suyo, garantizado por algo más fuerte que un WHERE company_id = ? en el código. Todo con datos sensibles (NIT, contactos, formularios, historial de solicitudes) bajo Habeas Data.
Qué diseñé
Un sitio público en Next.js (institucional + servicios + blog + formularios), y sobre él la capa que de verdad importa: el portal autenticado de empresas afiliadas, el panel de staff, la API para el ERP y los pagos. La columna vertebral es una sola: Postgres con RLS por company_id, validación en cada frontera, y compliance tratado como requisito de arquitectura, no como una página legal al final.
- Portal de afiliados con acceso solo por invitación (sin self-signup): las cuentas las crea el personal interno; el invitado recibe un token de un solo uso (hash bcrypt, expira 72h) y queda obligado a enrolar 2FA TOTP en el primer acceso.
- RLS por
company_iden todas las tablas: cada empresa ve solo sus envíos y su historial. - Panel admin para staff no técnico (gestión de empresas, inbox de formularios, contenido vía CMS headless).
- API REST
/api/v1/*para que el ERP externo lea y empuje datos server-to-server. - Pasarela Wompi en alcance SAQ A y Ley 1581 incorporada desde el esquema.
Decisiones de arquitectura interesantes
RLS como frontera real, no como decoración. El aislamiento entre empresas vive en políticas Row-Level Security de Postgres, evaluadas por el motor. La consecuencia que me importa: aunque la aplicación tenga un bug, la base rechaza la query que cruza empresas. La service_role key nunca llega al navegador. La tabla de audit log es append-only — ningún rol puede hacer UPDATE/DELETE sobre ella. Esto es lo que separa “multi-tenant” de “multi-tenant que de verdad aísla”, y es la razón principal por la que descarté WordPress + plugins: ahí un plugin desactualizado lee toda la base.
Una sola definición de “qué es válido” en las tres fronteras. El mismo esquema zod valida los argumentos que llegan del formulario del portal, el form del panel admin, y el body de la API del ERP. safeParse antes de tocar la base. Si cambia una regla (el teléfono pasa a obligatorio), cambia en un archivo. La frontera de confianza se trata igual venga de un humano o de un sistema externo.
API de ERP: defensa en capas, con el esquema correcto para cada caso. La integración server-to-server va con Bearer Token (hasheado bcrypt en DB) sobre TLS 1.3 + IP allowlist obligatorio + rate limiting por token + audit log de cada request. El orden importa: la IP fuera del rango se rechaza con 403 antes de evaluar credenciales. Elegí Bearer en vez de firma HMAC por request a propósito: el origen es controlable (rango de IPs declarado) y el dato es operativo B2B; HMAC mete fricción real con un equipo de terceros sin subir la seguridad de forma proporcional —es el patrón de Stripe/Shopify para este caso. HMAC se reserva para lo que sí lo pide: webhooks entrantes del ERP, donde el origen no es predecible (firma + replay protection con timestamp y nonce). Distinto problema, distinta herramienta.
Pagos en alcance SAQ A: el mejor código de pagos es el que no escribo. Widget Wompi client-side; los datos de tarjeta nunca tocan el servidor. La verdad transaccional vive en el dashboard de Wompi, no en nuestra DB. Esto baja el alcance PCI a cero y elimina la superficie de webhooks/conciliación. Es una limitación consciente, no un olvido.
Habeas Data desde el esquema. Consentimiento granular por formulario, consent log (timestamp, IP, user-agent, versión de política), endpoint de supresión, retención con TTL. No es una página de “política de datos” pegada al final: son columnas y políticas en la base.
Arquitectura
Qué demuestra
- Sé dónde poner la frontera de confianza: aislamiento en el motor (RLS, audit append-only) en vez de confiar en que el código no tenga bugs.
- Elijo el esquema de seguridad proporcional al caso y sé por qué — Bearer para server-to-server controlable, HMAC para webhooks entrantes; no aplico el más pesado por reflejo.
- Reduzco superficie de ataque sacando responsabilidad del sistema (pagos SAQ A) en vez de reimplementarla.
- Diseño multi-tenant que escala por configuración, no por rebuild, con compliance como columnas y políticas.
Estado y siguiente paso
Diseño detallado y propuesta técnica; la capa pública está definida y la capa autenticada (invitación + 2FA + RLS + API del ERP) está especificada a nivel de implementación, por construir. El plan separa el sitio público (MVP), la capa autenticada con la API del ERP, y los pagos. Siguiente paso: construir la capa autenticada y cerrar el contrato de la API con el equipo del ERP antes de tocar pagos. Es parte del mismo ecosistema que el agente de IA de Servicare; a futuro convergen en una sola plataforma.