Visión general de la arquitectura
Todos los tenants comparten la misma base de datos de Supabase. El aislamiento lo aplican las políticas de Row-Level Security (RLS)reglas a nivel de PostgreSQL que filtran cada consulta según los claims JWT del usuario autenticado.
Cuando un usuario inicia sesión, su JWT contiene un claim workspace_id. Cada política RLS verifica este claim contra una columna workspace_id en la tabla. Los usuarios literalmente no pueden consultar datos de otros workspacesla base de datos lo aplica, no el código de la aplicación.
Este patrón es el estándar de la industria que empresas como Factorial o Holded en Barcelona usan en sus herramientas internasy con Supabase puedes implementarlo en una fracción del tiempo y coste. Para SaaS latinoamericanos como Kavak o Kueski, el mismo patrón aplica: cada concesionario o prestamista tiene sus datos aislados.
Configurando la estructura de workspace
Tablas que necesitas:
1. workspaces (id, name, plan, created_at) 2. workspace_members (id, workspace_id, user_id, role, created_at) 3. Todas tus tablas de negocio (con columna workspace_id)
Cuando un usuario se registra, creas un workspace y un registro workspace_members en una función de base de datos de Supabase. El workspace_id se añade al JWT del usuario a través de un hook de claims personalizado.
Para SaaS B2B en España recomendamos también almacenar el CIF/NIF en la tabla workspaces. Facilita la facturación y permite el enriquecimiento automático de datos de clientes a través de la API del Registro Mercantil o servicios como Empresa Activa. Para LATAM, almacenar el RFC (México) o CUIT (Argentina) tiene el mismo efecto facilitador.
Escribiendo políticas RLS
Para cada tabla con alcance de tenant, activas RLS y añades estas políticas:
Política SELECT: CREATE POLICY "usuarios pueden leer datos de su workspace" ON tu_tabla FOR SELECT USING (workspace_id = (auth.jwt() ->> 'workspace_id')::bigint);
Política INSERT: CREATE POLICY "usuarios pueden insertar en su workspace" ON tu_tabla FOR INSERT WITH CHECK (workspace_id = (auth.jwt() ->> 'workspace_id')::bigint);
Esto aplica el aislamiento a nivel de base de datosindependientemente de lo que haga tu frontend o API. Un enlace de datos incorrecto en WeWeb o un bug en Xano nunca puede filtrar datos entre clientesla base de datos es la última línea de defensa.
Control de acceso basado en roles
Dentro de un workspace, diferentes usuarios tienen diferentes permisos (owner, admin, member). Almacena esto en workspace_members.role.
Para RBAC a nivel de datos, usa funciones de Supabase en tus políticas RLS:
CREATE FUNCTION get_user_role() RETURNS text AS $$ SELECT role FROM workspace_members WHERE user_id = auth.uid() AND workspace_id = (auth.jwt() ->> 'workspace_id')::bigint $$ LANGUAGE sql STABLE;
Luego en políticas: USING (get_user_role() IN ('admin', 'owner'))
Para clientes enterprise en España es habitual también querer acceso basado en departamento (por ejemplo, "solo RRHH puede ver datos de nómina"). Esto se construye fácilmente como roles adicionales o una tabla de permisos separada. El mismo patrón aplica para empresas en México o Colombia con estructuras organizativas complejas.
Conectando todo en WeWeb
En WeWeb configuras tu fuente de datos de Supabase con el token del usuario autenticado. WeWeb envía el JWT automáticamente con cada solicitud.
Para la navegación y UI de tu app, lee el rol desde una variable global (poblada desde la tabla workspace_members al iniciar sesión). Muestra/oculta elementos de menú, botones y secciones enteras según el rolesto es solo presentación; la seguridad real está en RLS.
Nunca codifiques IDs de tenant en WeWeb. Todo el filtrado de datos debe venir del JWT del usuario autenticadoSupabase gestiona el resto. Esto hace que tu app de WeWeb sea segura de distribuir incluso a clientes técnicamente avanzados que inspeccionan el tráfico de red. En App Studio hemos implementado este patrón en más de 50 apps en producción; contáctanos si quieres que lo hagamos para tu proyecto.