532 lines
28 KiB
Markdown
532 lines
28 KiB
Markdown
# 🏗️ ARQUITETURA MULTI-TENANT - SISTEMA RDO
|
|
|
|
## 📐 VISÃO GERAL DA ARQUITETURA
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ FRONTEND (React) │
|
|
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
|
│ │ Org A │ │ Org B │ │ Org C │ │
|
|
│ │ /acme-const │ │ /silva-eng │ │ /metal-xyz │ │
|
|
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
|
|
│ │ │ │ │
|
|
│ └──────────────────┴──────────────────┘ │
|
|
│ │ │
|
|
│ ┌────────▼────────┐ │
|
|
│ │ OrganizationCtx │ │
|
|
│ │ + AuthContext │ │
|
|
│ └────────┬────────┘ │
|
|
└────────────────────────────┼─────────────────────────────────────┘
|
|
│
|
|
┌────────▼────────┐
|
|
│ Supabase API │
|
|
│ (RLS Enabled) │
|
|
└────────┬────────┘
|
|
│
|
|
┌────────────────────────────▼─────────────────────────────────────┐
|
|
│ BANCO DE DADOS (PostgreSQL) │
|
|
│ │
|
|
│ ┌─────────────────────────────────────────────────────────┐ │
|
|
│ │ ORGANIZAÇÕES │ │
|
|
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
|
|
│ │ │ Org A │ │ Org B │ │ Org C │ │ │
|
|
│ │ │ (ID: 1) │ │ (ID: 2) │ │ (ID: 3) │ │ │
|
|
│ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │
|
|
│ └───────┼─────────────┼─────────────┼───────────────────┘ │
|
|
│ │ │ │ │
|
|
│ ┌───────▼─────────────▼─────────────▼───────────────────┐ │
|
|
│ │ DADOS ISOLADOS POR ORG_ID │ │
|
|
│ │ │ │
|
|
│ │ Usuários │ Obras │ RDOs │ Tarefas │ Anexos │ │
|
|
│ │ org_id=1 │ org_id=1│org_id=1│ org_id=1 │ org_id=1 │ │
|
|
│ │ org_id=2 │ org_id=2│org_id=2│ org_id=2 │ org_id=2 │ │
|
|
│ │ org_id=3 │ org_id=3│org_id=3│ org_id=3 │ org_id=3 │ │
|
|
│ └─────────────────────────────────────────────────────────┘ │
|
|
│ │
|
|
│ ┌─────────────────────────────────────────────────────────┐ │
|
|
│ │ ROW LEVEL SECURITY (RLS) │ │
|
|
│ │ • Usuário só vê dados da própria organização │ │
|
|
│ │ • Validação automática em TODAS as queries │ │
|
|
│ │ • Impossível acessar dados de outra org │ │
|
|
│ └─────────────────────────────────────────────────────────┘ │
|
|
└───────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## 🔐 FLUXO DE AUTENTICAÇÃO E AUTORIZAÇÃO
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ 1. SIGNUP DE ORGANIZAÇÃO │
|
|
└─────────────────────────────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌────────────────────────────────────┐
|
|
│ Usuário preenche formulário: │
|
|
│ • Nome da organização │
|
|
│ • Slug (URL amigável) │
|
|
│ • Email │
|
|
│ • Senha │
|
|
└────────────────┬───────────────────┘
|
|
│
|
|
▼
|
|
┌────────────────────────────────────┐
|
|
│ 1. Criar conta no Supabase Auth │
|
|
└────────────────┬───────────────────┘
|
|
│
|
|
▼
|
|
┌────────────────────────────────────┐
|
|
│ 2. Chamar função SQL: │
|
|
│ criar_organizacao_com_owner() │
|
|
│ • Cria organização │
|
|
│ • Cria perfil de usuário │
|
|
│ • Define como 'owner' │
|
|
└────────────────┬───────────────────┘
|
|
│
|
|
▼
|
|
┌────────────────────────────────────┐
|
|
│ 3. Redirecionar para: │
|
|
│ /:slug/dashboard │
|
|
└────────────────────────────────────┘
|
|
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ 2. CONVITE DE USUÁRIO │
|
|
└─────────────────────────────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌────────────────────────────────────┐
|
|
│ Admin/Owner convida: │
|
|
│ • Email do convidado │
|
|
│ • Role (engenheiro, mestre, etc) │
|
|
└────────────────┬───────────────────┘
|
|
│
|
|
▼
|
|
┌────────────────────────────────────┐
|
|
│ Sistema gera: │
|
|
│ • Token único │
|
|
│ • Link: /convite/:token │
|
|
│ • Email de convite │
|
|
└────────────────┬───────────────────┘
|
|
│
|
|
▼
|
|
┌────────────────────────────────────┐
|
|
│ Convidado clica no link │
|
|
└────────────────┬───────────────────┘
|
|
│
|
|
▼
|
|
┌────────────────────────────────────┐
|
|
│ 1. Validar token │
|
|
│ 2. Criar conta no Auth │
|
|
│ 3. Chamar aceitar_convite() │
|
|
│ 4. Vincular à organização │
|
|
└────────────────┬───────────────────┘
|
|
│
|
|
▼
|
|
┌────────────────────────────────────┐
|
|
│ Redirecionar para: │
|
|
│ /:slug/dashboard │
|
|
└────────────────────────────────────┘
|
|
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ 3. LOGIN E ACESSO │
|
|
└─────────────────────────────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌────────────────────────────────────┐
|
|
│ Usuário faz login │
|
|
│ • Email + Senha │
|
|
└────────────────┬───────────────────┘
|
|
│
|
|
▼
|
|
┌────────────────────────────────────┐
|
|
│ Supabase Auth valida │
|
|
└────────────────┬───────────────────┘
|
|
│
|
|
▼
|
|
┌────────────────────────────────────┐
|
|
│ Carregar organização(ões): │
|
|
│ SELECT * FROM organizacao_usuarios │
|
|
│ WHERE usuario_id = auth.uid() │
|
|
└────────────────┬───────────────────┘
|
|
│
|
|
▼
|
|
┌────────────────────────────────────┐
|
|
│ Se tem 1 org: │
|
|
│ → /:slug/dashboard │
|
|
│ │
|
|
│ Se tem múltiplas orgs: │
|
|
│ → /selecionar-organizacao │
|
|
└────────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## 🗄️ ESTRUTURA DO BANCO DE DADOS
|
|
|
|
### **Tabelas Principais**
|
|
|
|
```
|
|
organizacoes (TENANTS)
|
|
├── id (UUID, PK)
|
|
├── slug (VARCHAR, UNIQUE) ← URL amigável
|
|
├── nome (VARCHAR)
|
|
├── plano (VARCHAR) ← trial, basic, professional, enterprise
|
|
├── max_usuarios (INTEGER)
|
|
├── max_obras (INTEGER)
|
|
├── max_rdos_mes (INTEGER)
|
|
├── max_storage_mb (INTEGER)
|
|
├── cor_primaria (VARCHAR)
|
|
├── cor_secundaria (VARCHAR)
|
|
├── configuracoes (JSONB)
|
|
└── status (VARCHAR)
|
|
|
|
usuarios
|
|
├── id (UUID, PK, FK → auth.users)
|
|
├── organizacao_id (UUID, FK → organizacoes) ← ISOLAMENTO
|
|
├── nome (VARCHAR)
|
|
├── email (VARCHAR)
|
|
└── ativo (BOOLEAN)
|
|
|
|
organizacao_usuarios (ROLES)
|
|
├── id (UUID, PK)
|
|
├── organizacao_id (UUID, FK → organizacoes)
|
|
├── usuario_id (UUID, FK → usuarios)
|
|
├── role (VARCHAR) ← owner, admin, engenheiro, mestre_obra, usuario
|
|
└── ativo (BOOLEAN)
|
|
|
|
convites
|
|
├── id (UUID, PK)
|
|
├── organizacao_id (UUID, FK → organizacoes)
|
|
├── email (VARCHAR)
|
|
├── role (VARCHAR)
|
|
├── token (VARCHAR, UNIQUE)
|
|
├── status (VARCHAR) ← pendente, aceito, expirado
|
|
└── expira_em (TIMESTAMP)
|
|
|
|
obras
|
|
├── id (UUID, PK)
|
|
├── organizacao_id (UUID, FK → organizacoes) ← ISOLAMENTO
|
|
├── nome (VARCHAR)
|
|
├── responsavel_id (UUID, FK → usuarios)
|
|
└── ... (outros campos)
|
|
|
|
rdos
|
|
├── id (UUID, PK)
|
|
├── organizacao_id (UUID, FK → organizacoes) ← ISOLAMENTO
|
|
├── obra_id (UUID, FK → obras)
|
|
├── criado_por (UUID, FK → usuarios)
|
|
└── ... (outros campos)
|
|
|
|
rdo_atividades
|
|
├── id (UUID, PK)
|
|
├── organizacao_id (UUID, FK → organizacoes) ← ISOLAMENTO
|
|
├── rdo_id (UUID, FK → rdos)
|
|
└── ... (outros campos)
|
|
|
|
... (todas as outras tabelas seguem o mesmo padrão)
|
|
```
|
|
|
|
### **Índices Importantes**
|
|
|
|
```sql
|
|
-- Índices compostos para performance multi-tenant
|
|
CREATE INDEX idx_usuarios_org ON usuarios(organizacao_id, id);
|
|
CREATE INDEX idx_obras_org ON obras(organizacao_id, id);
|
|
CREATE INDEX idx_rdos_org ON rdos(organizacao_id, id);
|
|
|
|
-- Índices para busca por slug
|
|
CREATE INDEX idx_organizacoes_slug ON organizacoes(slug);
|
|
|
|
-- Índices para roles
|
|
CREATE INDEX idx_org_usuarios_org ON organizacao_usuarios(organizacao_id);
|
|
CREATE INDEX idx_org_usuarios_user ON organizacao_usuarios(usuario_id);
|
|
```
|
|
|
|
---
|
|
|
|
## 🔒 ROW LEVEL SECURITY (RLS)
|
|
|
|
### **Como Funciona**
|
|
|
|
```sql
|
|
-- Exemplo: Política para tabela 'obras'
|
|
CREATE POLICY "Ver obras da organização" ON obras
|
|
FOR SELECT USING (
|
|
organizacao_id IN (
|
|
SELECT organizacao_id
|
|
FROM organizacao_usuarios
|
|
WHERE usuario_id = auth.uid()
|
|
AND ativo = true
|
|
)
|
|
);
|
|
```
|
|
|
|
### **Fluxo de Query com RLS**
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ Frontend executa: │
|
|
│ const { data } = await supabase.from('obras').select('*') │
|
|
└────────────────────────┬────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ Supabase adiciona automaticamente: │
|
|
│ WHERE organizacao_id IN ( │
|
|
│ SELECT organizacao_id FROM organizacao_usuarios │
|
|
│ WHERE usuario_id = auth.uid() AND ativo = true │
|
|
│ ) │
|
|
└────────────────────────┬────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ Resultado: Apenas obras da organização do usuário │
|
|
│ • Impossível ver dados de outras organizações │
|
|
│ • Validação no nível do banco de dados │
|
|
│ • Não depende do frontend │
|
|
└─────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## 🎨 FLUXO DE PERSONALIZAÇÃO
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ CARREGAMENTO DA APLICAÇÃO │
|
|
└─────────────────────────────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌────────────────────────────────────┐
|
|
│ 1. Extrair slug da URL │
|
|
│ Exemplo: /acme-const/dashboard │
|
|
│ slug = 'acme-const' │
|
|
└────────────────┬───────────────────┘
|
|
│
|
|
▼
|
|
┌────────────────────────────────────┐
|
|
│ 2. Carregar organização: │
|
|
│ SELECT * FROM organizacoes │
|
|
│ WHERE slug = 'acme-const' │
|
|
└────────────────┬───────────────────┘
|
|
│
|
|
▼
|
|
┌────────────────────────────────────┐
|
|
│ 3. Aplicar personalização: │
|
|
│ • Logo │
|
|
│ • Cores (primária, secundária) │
|
|
│ • Configurações │
|
|
└────────────────┬───────────────────┘
|
|
│
|
|
▼
|
|
┌────────────────────────────────────┐
|
|
│ 4. Carregar configurações: │
|
|
│ • Tipos de atividade │
|
|
│ • Funções de mão de obra │
|
|
│ • Tipos de equipamento │
|
|
│ • Condições climáticas │
|
|
└────────────────┬───────────────────┘
|
|
│
|
|
▼
|
|
┌────────────────────────────────────┐
|
|
│ 5. Aplicar tema CSS: │
|
|
│ --color-primary: #3B82F6 │
|
|
│ --color-secondary: #1E40AF │
|
|
└────────────────┬───────────────────┘
|
|
│
|
|
▼
|
|
┌────────────────────────────────────┐
|
|
│ 6. Renderizar aplicação │
|
|
│ personalizada │
|
|
└────────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## 📊 SISTEMA DE QUOTAS E LIMITES
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ VALIDAÇÃO DE QUOTAS │
|
|
└─────────────────────────────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌────────────────────────────────────┐
|
|
│ Usuário tenta criar recurso │
|
|
│ (obra, RDO, usuário, etc) │
|
|
└────────────────┬───────────────────┘
|
|
│
|
|
▼
|
|
┌────────────────────────────────────┐
|
|
│ 1. Trigger no banco verifica: │
|
|
│ • Contar recursos existentes │
|
|
│ • Buscar limite do plano │
|
|
│ • Comparar │
|
|
└────────────────┬───────────────────┘
|
|
│
|
|
▼
|
|
┌────────────────────────────────────┐
|
|
│ Se dentro do limite: │
|
|
│ ✅ Permitir criação │
|
|
│ │
|
|
│ Se excedeu limite: │
|
|
│ ❌ RAISE EXCEPTION │
|
|
│ "Limite atingido. Faça upgrade" │
|
|
└────────────────┬───────────────────┘
|
|
│
|
|
▼
|
|
┌────────────────────────────────────┐
|
|
│ Frontend captura erro: │
|
|
│ • Mostrar modal │
|
|
│ • Sugerir upgrade de plano │
|
|
│ • Link para billing │
|
|
└────────────────────────────────────┘
|
|
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ MÉTRICAS DE USO │
|
|
└─────────────────────────────────────────────────────────────────┘
|
|
|
|
organizacao_metricas
|
|
├── organizacao_id
|
|
├── mes_referencia
|
|
├── total_usuarios (atual)
|
|
├── total_obras (atual)
|
|
├── total_rdos (no mês)
|
|
├── storage_usado_mb (atual)
|
|
├── limite_usuarios (do plano)
|
|
├── limite_obras (do plano)
|
|
├── limite_rdos_mes (do plano)
|
|
└── limite_storage_mb (do plano)
|
|
|
|
Atualização automática via triggers:
|
|
• Ao criar/deletar usuário
|
|
• Ao criar/deletar obra
|
|
• Ao criar/deletar RDO
|
|
• Ao fazer upload de arquivo
|
|
```
|
|
|
|
---
|
|
|
|
## 🔄 FLUXO COMPLETO DE UMA OPERAÇÃO
|
|
|
|
### **Exemplo: Criar um RDO**
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ 1. FRONTEND │
|
|
└─────────────────────────────────────────────────────────────────┘
|
|
const { organization } = useOrganization();
|
|
|
|
const { data, error } = await supabase
|
|
.from('rdos')
|
|
.insert({
|
|
obra_id: obraId,
|
|
criado_por: user.id,
|
|
data_relatorio: new Date(),
|
|
condicoes_climaticas: 'Ensolarado',
|
|
// organizacao_id será preenchido automaticamente
|
|
});
|
|
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ 2. SUPABASE API │
|
|
└─────────────────────────────────────────────────────────────────┘
|
|
• Validar autenticação (JWT token)
|
|
• Aplicar RLS policies
|
|
• Executar INSERT
|
|
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ 3. BANCO DE DADOS - TRIGGERS │
|
|
└─────────────────────────────────────────────────────────────────┘
|
|
|
|
BEFORE INSERT:
|
|
├── set_rdo_organizacao_id()
|
|
│ └── Copia organizacao_id da obra
|
|
│
|
|
├── set_rdo_numero()
|
|
│ └── Define próximo número sequencial
|
|
│
|
|
└── verificar_limite_rdos()
|
|
└── Valida se não excedeu quota
|
|
|
|
INSERT executado ✅
|
|
|
|
AFTER INSERT:
|
|
└── atualizar_metricas_organizacao()
|
|
└── Incrementa contador de RDOs
|
|
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ 4. RESPOSTA │
|
|
└─────────────────────────────────────────────────────────────────┘
|
|
• RDO criado com sucesso
|
|
• organizacao_id preenchido automaticamente
|
|
• Número sequencial atribuído
|
|
• Métricas atualizadas
|
|
• Retorna dados para o frontend
|
|
```
|
|
|
|
---
|
|
|
|
## 🎯 PONTOS-CHAVE DA ARQUITETURA
|
|
|
|
### ✅ **Isolamento Total**
|
|
- Cada organização é completamente isolada
|
|
- RLS garante segurança no nível do banco
|
|
- Impossível acessar dados de outra organização
|
|
|
|
### ✅ **Escalabilidade**
|
|
- Índices otimizados para queries multi-tenant
|
|
- Cache de configurações
|
|
- Lazy loading de recursos
|
|
|
|
### ✅ **Flexibilidade**
|
|
- Cada organização pode ter configurações únicas
|
|
- Personalização de marca (logo, cores)
|
|
- Tipos de atividade customizáveis
|
|
|
|
### ✅ **Segurança**
|
|
- RLS em todas as tabelas
|
|
- Validação de permissões por role
|
|
- Tokens seguros para convites
|
|
- Service role key nunca exposta
|
|
|
|
### ✅ **Automação**
|
|
- Triggers para propagação de dados
|
|
- Validação automática de quotas
|
|
- Atualização automática de métricas
|
|
- Numeração sequencial automática
|
|
|
|
---
|
|
|
|
## 📈 CRESCIMENTO E EVOLUÇÃO
|
|
|
|
### **Fase Atual: MVP**
|
|
- Multi-tenancy básico
|
|
- Isolamento de dados
|
|
- Sistema de convites
|
|
- Quotas por plano
|
|
|
|
### **Próximas Fases:**
|
|
- Billing e pagamentos
|
|
- Analytics avançado
|
|
- API pública
|
|
- Webhooks
|
|
- Integrações (ERPs, etc)
|
|
- Mobile app nativo
|
|
- IA para análise de produtividade
|
|
|
|
---
|
|
|
|
Esta arquitetura foi projetada para ser:
|
|
- 🔒 **Segura** - RLS em todas as camadas
|
|
- 📈 **Escalável** - Suporta milhares de organizações
|
|
- 🎨 **Flexível** - Personalizável por organização
|
|
- 🚀 **Performática** - Índices otimizados
|
|
- 🔧 **Manutenível** - Código limpo e documentado
|