Multi-tenancy

· Padrão Banco de Dados
Juan Kalleo
Juan Kalleo
Backend/API
This page hasn't been translated to English yet — showing the original Portuguese content.

Um banco só, várias organizações isoladas dentro dele (multi-tenancy por linha, não por schema/banco separado) — a hierarquia real tem três níveis.

A hierarquia real

a_tenant (o cliente SaaS — o estado, a prefeitura) → a_orgao (órgão/CNPJ, pertence a um tenant) → a_unidade (divisão/setor, pertence a um órgão) → users.a_unidade_id (opcional — nem todo usuário está preso a uma unidade específica).

create_table "a_orgaos", force: :cascade do |t|
  t.string "nome"
  t.bigint "a_tenant_id", null: false
  t.index ["a_tenant_id"], name: "index_a_orgaos_on_a_tenant_id"
end

create_table "a_unidades", force: :cascade do |t|
  t.string "nome"
  t.bigint "a_orgao_id", null: false
end

create_table "users", force: :cascade do |t|
  t.bigint "a_unidade_id"
end

add_foreign_key "a_orgaos", "a_tenants"
add_foreign_key "a_unidades", "a_orgaos"
add_foreign_key "users", "a_unidades"

dependent: :restrict_with_error — nunca cascata na hierarquia de tenant

# ATenant
has_many :a_orgaos, dependent: :restrict_with_error
# AOrgao
has_many :a_unidades, dependent: :restrict_with_error

Deletar um tenant que ainda tem órgão vinculado falha, não apaga em cascata. É deliberado: um dependent: :destroy na ponta de cima da hierarquia de tenant apagaria silenciosamente todo o negócio de um cliente inteiro se alguém remover a linha errada — restrict_with_error obriga a decisão consciente de remover cada nível primeiro, explicitamente.

Onde o isolamento entre tenant é aplicado de verdade

Não é no schema — nenhuma tabela de negócio comum tem um WHERE tenant_id = ? implícito automático tipo Row-Level Security do Postgres. O isolamento vive na camada de autorização: a_usuarios_papeis (a atribuição de papel a um usuário) carrega um escopo polimórfico (escopo_type/escopo_id, apontando pra uma AUnidade, AOrgao ou ATenant real) mais um nível (a_escopo_papel_id: Unidade, Órgão, Tenant ou Plataforma). accessible_by(ability) — ver Filtros e busca e Autorização no Padrão API — é o que de fato filtra a query pelo escopo do papel do usuário logado, não uma constraint de banco.

belongs_to :escopo, polymorphic: true, optional: true
validates :escopo, presence: true, unless: -> { a_escopo_papel_id == AEscopoPapel.plataforma.id }

Só o escopo Plataforma dispensa um escopo específico — faz sentido, porque "Plataforma" já significa "sem tenant nenhum, acesso cross-tenant" por definição.

Leitura de apoio