New projects created: - michangarrito (marketplace mobile) - template-saas (SaaS template) - clinica-dental (dental ERP) - clinica-veterinaria (veterinary ERP) Architecture updates: - Move catalog from core/ to shared/ - Add MCP servers structure and templates - Add git management scripts - Update SUBREPOSITORIOS.md with 15 new repos - Update .gitignore for new projects Repository infrastructure: - 4 main repositories - 11 subrepositorios - Gitea remotes configured 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
197 lines
5.5 KiB
Markdown
197 lines
5.5 KiB
Markdown
# Patron Multi-Tenant con RLS
|
|
|
|
**Categoria:** Architecture
|
|
**Version:** 1.0.0
|
|
**Origen:** projects/gamilit, projects/erp-core
|
|
**Fecha:** 2025-12-27
|
|
|
|
---
|
|
|
|
## Descripcion
|
|
|
|
Este documento describe el patron de arquitectura multi-tenant utilizado en todos los proyectos del workspace, basado en Row-Level Security (RLS) de PostgreSQL.
|
|
|
|
## Principios
|
|
|
|
1. **Aislamiento completo**: Cada tenant solo ve sus propios datos
|
|
2. **Transparencia**: El codigo de aplicacion no necesita filtrar por tenant
|
|
3. **Performance**: RLS optimizado con indices apropiados
|
|
4. **Seguridad**: Imposible acceder a datos de otros tenants
|
|
|
|
## Arquitectura
|
|
|
|
```
|
|
┌─────────────────────────────────┐
|
|
│ Application │
|
|
│ (NestJS / Express) │
|
|
└───────────────┬─────────────────┘
|
|
│
|
|
┌───────────────▼─────────────────┐
|
|
│ Tenant Middleware │
|
|
│ SET app.current_tenant_id │
|
|
└───────────────┬─────────────────┘
|
|
│
|
|
┌───────────────▼─────────────────┐
|
|
│ PostgreSQL │
|
|
│ Row-Level Security │
|
|
│ (USING tenant_id = ...) │
|
|
└─────────────────────────────────┘
|
|
```
|
|
|
|
## Implementacion
|
|
|
|
### 1. Variable de Sesion
|
|
|
|
```sql
|
|
-- Configurar en cada request
|
|
SET app.current_tenant_id = 'uuid-del-tenant';
|
|
|
|
-- Obtener en queries
|
|
current_setting('app.current_tenant_id', true)::UUID
|
|
```
|
|
|
|
### 2. Middleware NestJS
|
|
|
|
```typescript
|
|
// src/middleware/tenant.middleware.ts
|
|
@Injectable()
|
|
export class TenantMiddleware implements NestMiddleware {
|
|
constructor(private dataSource: DataSource) {}
|
|
|
|
async use(req: Request, res: Response, next: NextFunction) {
|
|
const tenantId = this.extractTenantId(req);
|
|
|
|
if (tenantId) {
|
|
await this.dataSource.query(
|
|
`SET app.current_tenant_id = $1`,
|
|
[tenantId]
|
|
);
|
|
}
|
|
|
|
next();
|
|
}
|
|
|
|
private extractTenantId(req: Request): string | null {
|
|
// 1. Del JWT token
|
|
if (req.user?.tenantId) return req.user.tenantId;
|
|
|
|
// 2. Del header
|
|
if (req.headers['x-tenant-id']) return req.headers['x-tenant-id'];
|
|
|
|
// 3. Del subdominio
|
|
const host = req.headers.host;
|
|
// tenant.example.com -> tenant
|
|
|
|
return null;
|
|
}
|
|
}
|
|
```
|
|
|
|
### 3. RLS Policies
|
|
|
|
```sql
|
|
-- Habilitar RLS en tabla
|
|
ALTER TABLE my_table ENABLE ROW LEVEL SECURITY;
|
|
|
|
-- Politica de aislamiento
|
|
CREATE POLICY tenant_isolation ON my_table
|
|
FOR ALL
|
|
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
|
|
|
|
-- Para INSERT, asegurar que se use el tenant correcto
|
|
CREATE POLICY tenant_insert ON my_table
|
|
FOR INSERT
|
|
WITH CHECK (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
|
|
```
|
|
|
|
### 4. Estructura de Tabla
|
|
|
|
```sql
|
|
CREATE TABLE my_table (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES auth.tenants(id),
|
|
-- ... otros campos ...
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- Indice para performance
|
|
CREATE INDEX idx_my_table_tenant ON my_table(tenant_id);
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
### DO (Hacer)
|
|
|
|
- Siempre incluir `tenant_id` en tablas de datos
|
|
- Crear indice en `tenant_id`
|
|
- Habilitar RLS en TODAS las tablas con datos de tenant
|
|
- Usar el middleware en TODAS las rutas protegidas
|
|
- Testear aislamiento en cada nueva tabla
|
|
|
|
### DON'T (No hacer)
|
|
|
|
- No filtrar manualmente por tenant_id (dejar a RLS)
|
|
- No desactivar RLS para "queries rapidas"
|
|
- No hardcodear tenant_id en codigo
|
|
- No compartir conexiones entre requests sin reset
|
|
|
|
## Tablas Exentas de RLS
|
|
|
|
Algunas tablas son globales y no tienen tenant_id:
|
|
|
|
- `auth.tenants` - Lista de todos los tenants
|
|
- `config.feature_flags` - Flags globales
|
|
- `billing.plans` - Planes disponibles
|
|
|
|
## Testing
|
|
|
|
```typescript
|
|
describe('Tenant Isolation', () => {
|
|
it('should not see data from other tenant', async () => {
|
|
// Crear datos en tenant A
|
|
await setTenant(tenantA);
|
|
await createRecord({ name: 'Record A' });
|
|
|
|
// Cambiar a tenant B
|
|
await setTenant(tenantB);
|
|
const records = await getRecords();
|
|
|
|
// No debe ver record de tenant A
|
|
expect(records).not.toContainEqual(
|
|
expect.objectContaining({ name: 'Record A' })
|
|
);
|
|
});
|
|
});
|
|
```
|
|
|
|
## Troubleshooting
|
|
|
|
### Error: "Permission denied for table"
|
|
|
|
- Verificar que RLS esta habilitado
|
|
- Verificar que la politica existe
|
|
- Verificar que `app.current_tenant_id` esta configurado
|
|
|
|
### Query retorna todos los registros
|
|
|
|
- Verificar que el middleware esta activo
|
|
- Verificar que `current_setting` retorna el UUID correcto
|
|
- Verificar que la politica no tiene errores
|
|
|
|
### Performance lenta
|
|
|
|
- Crear indice en `tenant_id`
|
|
- Verificar que el indice se usa (EXPLAIN ANALYZE)
|
|
- Considerar particionamiento por tenant_id
|
|
|
|
## Referencias
|
|
|
|
- `shared/catalog/multi-tenancy/` - Modulo del catalogo
|
|
- `projects/erp-core/database/ddl/` - Ejemplos de DDL
|
|
- PostgreSQL RLS Documentation
|
|
|
|
---
|
|
|
|
*Knowledge Base - Workspace v1*
|