O modelo padrão SaaS B2B
Você está construindo Linear/Notion/Vercel-like. A hierarquia consolidada: Org → Team → Project → Resource. Cada nível tem roles, sharing inter-níveis, guests externos. Implementar errado = dor permanente. Implementar bem destrava enterprise sales.
A hierarquia visualizada
🗺️ Modelo Linear/Notion/Vercel-like
Organization
Owner / Admin
Plan / Billing
Team / Workspace
Team roles
Cross-team sharing
Project / Workspace
Document / Issue / Resource
Sharing externo
Schema relacional baseline
-- Hierarchy
CREATE TABLE organizations (id UUID PRIMARY KEY, name TEXT, plan TEXT);
CREATE TABLE teams (id UUID PRIMARY KEY, org_id UUID REFERENCES organizations);
CREATE TABLE projects (id UUID PRIMARY KEY, team_id UUID REFERENCES teams);
CREATE TABLE documents (id UUID PRIMARY KEY, project_id UUID REFERENCES projects);
-- Users + memberships
CREATE TABLE users (id UUID PRIMARY KEY, email TEXT UNIQUE);
CREATE TABLE org_members (
org_id UUID REFERENCES organizations,
user_id UUID REFERENCES users,
role TEXT NOT NULL, -- 'owner', 'admin', 'member', 'guest'
is_guest BOOLEAN DEFAULT false,
PRIMARY KEY (org_id, user_id)
);
CREATE TABLE team_members (
team_id UUID REFERENCES teams,
user_id UUID REFERENCES users,
role TEXT NOT NULL, -- 'admin', 'member', 'viewer'
PRIMARY KEY (team_id, user_id)
);
-- Sharing externo (granular por resource)
CREATE TABLE resource_shares (
resource_type TEXT NOT NULL,
resource_id UUID NOT NULL,
shared_with_user_id UUID REFERENCES users,
shared_with_email TEXT,
role TEXT NOT NULL, -- 'editor', 'commenter', 'viewer'
invited_by UUID REFERENCES users,
expires_at TIMESTAMPTZ,
PRIMARY KEY (resource_type, resource_id, COALESCE(shared_with_user_id, NULL), COALESCE(shared_with_email, NULL))
);Postgres RLS para isolation simples
-- Habilitar RLS
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
-- Policy: só vê documents da org dele
CREATE POLICY tenant_isolation ON documents
USING (
project_id IN (
SELECT p.id FROM projects p
JOIN teams t ON p.team_id = t.id
WHERE t.org_id = current_setting('app.current_org_id')::UUID
)
);
-- App seta o context antes de cada query
SET LOCAL app.current_org_id = 'org-uuid-aqui';
SELECT * FROM documents WHERE id = 'doc-uuid'; -- RLS aplica filter automaticamenteReBAC engine para sharing complexo
# SpiceDB schema para SaaS B2B
definition user {}
definition organization {
relation owner: user
relation admin: user
relation member: user
permission manage = owner + admin
permission view = manage + member
}
definition project {
relation parent: organization
relation editor: user
relation viewer: user
// Sharing externo
relation guest_editor: user
relation guest_viewer: user
permission edit = editor + guest_editor + parent->manage
permission view = edit + viewer + guest_viewer + parent->view
}
definition document {
relation project: project
relation external_editor: user
relation external_viewer: user
permission edit = external_editor + project->edit
permission view = edit + external_viewer + project->view
}Checks em produção
// SpiceDB SDK
import { v1 } from '@authzed/authzed-node';
const client = v1.NewClient(process.env.AUTHZED_TOKEN!);
async function canViewDocument(userId: string, docId: string): Promise<boolean> {
const resp = await client.checkPermission({
resource: { objectType: 'document', objectId: docId },
permission: 'view',
subject: { object: { objectType: 'user', objectId: userId } },
});
return resp.permissionship === v1.CheckPermissionResponse_Permissionship.HAS_PERMISSION;
}
// Em route handler
app.get('/documents/:id', async (req, res) => {
const canView = await canViewDocument(req.user.id, req.params.id);
if (!canView) return res.status(403).send('Forbidden');
// ... resto
});Trade-offs RLS vs ReBAC
| Aspecto | Postgres RLS | ReBAC engine |
|---|---|---|
| Isolation simples por tenant | ✅ Excelente | ✅ Mas overkill |
| Sharing cruzado inter-tenant | ❌ Complicado | ✅ Natural |
| Roles customizadas por org | ⚠️ Aplicação implementa | ✅ Schema flexível |
| Performance sub-ms | ✅ Filter em SQL | ✅ Cache distribuído |
| Operacional | ✅ Já tem Postgres | ⚠️ Mais um serviço |
| Debug | ⚠️ EXPLAIN com policy | ✅ ZedToken + tracing |
AspectoIsolation simples por tenant
Postgres RLS✅ Excelente
ReBAC engine✅ Mas overkill
AspectoSharing cruzado inter-tenant
Postgres RLS❌ Complicado
ReBAC engine✅ Natural
AspectoRoles customizadas por org
Postgres RLS⚠️ Aplicação implementa
ReBAC engine✅ Schema flexível
AspectoPerformance sub-ms
Postgres RLS✅ Filter em SQL
ReBAC engine✅ Cache distribuído
AspectoOperacional
Postgres RLS✅ Já tem Postgres
ReBAC engine⚠️ Mais um serviço
AspectoDebug
Postgres RLS⚠️ EXPLAIN com policy
ReBAC engine✅ ZedToken + tracing
✅
Trilha Authorization Engineering concluída. Badge Authz Architect desbloqueado.