Docs›Multi-tenancy›Empresas: CRUD e provisionamento
Multi-tenancy

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ó para CREATE 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 .env a 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 por TenantConnectionResolver — 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 LoginForm abre o modal login-tenant antes 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. Grava session('tenant_id') (Pool) e, em multi_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\ChangeTenantForm lista só as empresas permitidas e delega ao setTenant; 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