Empresas: CRUD e provisionamento
Tela de Empresas (TenantList/TenantForm/TenantProvisionForm), mad:tenant:provision (sqlite/mysql/pgsql/madcloud), registro durável via mad_iam_tenant.db_config e seleção/troca de empresa (setTenant fail-closed, ChangeTenantForm).
Empresas: CRUD, provisionamento e troca
Esta página é o passo-a-passo operacional do modo Bridge: cadastrar a empresa, criar o banco dedicado dela, deixar a conexão registrada de forma durável e escolher/trocar a empresa corrente. A visão conceitual das quatro estratégias está em Estratégias de multi-tenancy.
1. Cadastrar a empresa
Tela Empresas — controllers TenantList, TenantForm e
TenantProvisionForm, ligados ao grupo Administrador. O registro vive em
mad_iam_tenant (conexão iam, control-plane — nunca roteado por tenant).
| Coluna | Papel |
|---|---|
name / slug |
identificação; o slug deriva o nome da conexão e o caminho sqlite |
connection_name |
vazio = single/pool; preenchido = Bridge (tenant_<slug> por convenção) |
db_config |
locator JSON da conexão física — registro durável, lido no boot |
active |
'Y' — o registrar só considera tenants ativos |
Unidades (mad_iam_unit) apontam para a empresa por tenant_id e não têm
mais connection_name: o banco pertence à empresa, não à filial.
2. Provisionar o banco
Os dois caminhos — botão Provisionar na tela e o command — chamam a mesma
camada, App\Service\Tenant\TenantProvisioningService.
php artisan mad:tenant:provision acme
php artisan mad:tenant:provision acme --database=/path/acme.sqlite --connection=tenant_acme
| Flag | Default | Descrição |
|---|---|---|
{slug} |
— | slug do tenant (firstOrNew — cria o registro se não existir) |
--database |
database/tenants/<slug>.sqlite (sqlite) / tenant_<slug> (servidor) |
alvo físico |
--connection |
tenant_<slug> |
nome da conexão a registrar |
O que o serviço faz, nesta ordem: valida slug (guard anti-traversal) e conexão
(allowlist mad.tenant.allowed_connections) → cria o alvo físico conforme o
driver → registra a conexão em runtime → replica o schema do data-plane
(TenantProvisioner) → persiste connection_name + db_config →
TenantConnectionRegistrar::flush(). É idempotente: a replicação só cria a
tabela que falta.
Drivers: sqlite (cria o arquivo), mysql/mariadb e pgsql
(CREATE DATABASE), e madcloud quando o app roda hospedado — nesse caso o
control plane decide o nome final da base. Driver não suportado aborta com
RuntimeException.
O que é replicado: as tabelas do data-plane (business, comm, ged,
ai, incluindo as tabelas model-less de conversa do agente) mais as
tabelas dos modelos de dados do app (MAD_DATA_MODELS). O control-plane
(iam, log) nunca é replicado — identidade, auditoria e fila continuam
unificadas no banco compartilhado.
Prévia sem efeito colateral: TenantProvisioningService::plan() devolve
driver, nome de conexão, alvo e a lista de tabelas que seriam criadas — é o
que alimenta o drawer de confirmação da tela. Não cria arquivo nem toca o
banco.
3. Credenciais de provisionamento
// config/mad.php → mad.tenant.provisioning
'template' => env('MAD_TENANT_TEMPLATE_CONNECTION', 'business'),
'admin_host' => env('DB_TENANT_ADMIN_HOST'),
'admin_port' => env('DB_TENANT_ADMIN_PORT'),
templateé a conexão data-plane de onde o tenant herda host/credencial de runtime.admin_*são credenciais elevadas, usadas só paraCREATE DATABASE. Vazias, herdam o template (dev, onde o usuário de runtime já pode criar base).- Nenhuma credencial vai para o
db_config— o locator de servidor é esparso ({driver, database, template}) e o resto é mergeado do.enva cada boot.
4. Registro durável (db_config)
App\Providers\TenantServiceProvider roda no boot e chama
TenantConnectionRegistrar::registerAll(), que lê mad_iam_tenant.db_config
dos tenants ativos e injeta database.connections.<connection_name>. É por
isso que não é preciso declarar a conexão do tenant em
config/database.php.
- No-op resiliente enquanto a tabela não existe (web installer / banco fresco).
- Memo por processo; toda escrita chama
flush(). - Worker que bootou antes do provisionamento se auto-cura pelo
lookup()lazy chamado porTenantConnectionResolver— sempre depois da allowlist.
Antes disso, o provisionamento registrava a conexão apenas em memória e a request seguinte falhava fail-closed com "conexão não definida".
5. Escolher e trocar a empresa
- Login. Com mais de uma empresa permitida, o
LoginFormabre o modallogin-tenantantes da unidade (a empresa define o banco; a unidade vem depois). Com uma só, ela é selecionada automaticamente. AuthenticationService::setTenant($id)é fail-closed. Valida o id contra as empresas do usuário (getSystemUserTenants()) e lança "Unauthorized access to that tenant" fora da lista. Gravasession('tenant_id')(Pool) e, emmulti_database,session('tenant_database')(Bridge); descarta o token de embed em sessão, que carrega o contexto de empresa da emissão.- Troca sem deslogar.
App\Control\Iam\ChangeTenantFormlista só as empresas permitidas e delega aosetTenant; trocar de empresa reseta a unidade para uma da nova empresa.
6. Sincronizar schema depois
php artisan mad:tenant:migrate # todos os tenants provisionados
php artisan mad:tenant:migrate --tenant=acme
Ver também
- Estratégias de multi-tenancy — Single,
Pool, Bridge e Hybrid; planos control/data e o middleware
mad.api. - Row-scope vs MCP-scope — o outro eixo de filtro (usuário/unidade, só no agente de IA).
- CLI — referência dos comandos
mad:tenant:*.