Zum Hauptinhalt springen

Virtual Stores System — Gestao de Tenants

Documento de referencia para administradores de sistema da In All Web. Audiencia: equipa tecnica interna (SysAdmin).


O que e um Tenant

Cada loja online na plataforma e um tenant independente. Um tenant tem:

  • Um slug unico (ex: defenseops)
  • Um plano de subscricao activo
  • Um conjunto de addons opcionais
  • Branding proprio (logo, cores, banner)
  • Um dominio automatico e, opcionalmente, um dominio custom
  • Uma conta Stripe Connect associada (apos onboarding)
  • Utilizadores proprios (admin, staff, customers)

Os dados de cada tenant sao completamente isolados dos outros via Row-Level Security (RLS) na base de dados.


Provisioning de um Novo Tenant

Endpoint

POST /api/sysadmin/tenants
Authorization: Bearer <sysadmin_jwt>
Content-Type: application/json

Payload

{
"slug": "nome-da-loja",
"name": "Nome Comercial da Loja",
"plan": "starter",
"owner_email": "dono@exemplo.com",
"owner_name": "Nome do Proprietario"
}

Campos obrigatorios: slug, name, plan, owner_email.

O slug deve ser unico, em minusculas, sem espacos, com hifens como separadores. E usado no subdominio automatico.

Resposta de sucesso

{
"id": "uuid",
"slug": "nome-da-loja",
"name": "Nome Comercial da Loja",
"plan": "starter",
"status": "active",
"domain": "nome-da-loja.store.inallweb.com",
"created_at": "2026-04-09T10:00:00Z"
}

O sistema cria automaticamente:

  • O subdominio {slug}.store.inallweb.com
  • O utilizador admin com o email fornecido (email de convite enviado via Zeptomail)
  • A estrutura de dados inicial do tenant (categorias padrao, configuracoes)

Planos Disponiveis

PlanoPrecoUtilizadoresSistemasPlatform fee
StarterEUR 24.90/mes112.0%
BusinessEUR 49.90/mes311.5%
ProfessionalEUR 89.90/mes521.0%
EnterpriseEUR 149.90/mes1550.5%

O campo plan aceita os valores: starter, business, professional, enterprise.

Alterar plano de um tenant

PATCH /api/sysadmin/tenants/{tenant_id}
Authorization: Bearer <sysadmin_jwt>
Content-Type: application/json

{
"plan": "business"
}

A alteracao de plano tem efeito imediato. O feature gate passa a reflectir o novo plano no proximo pedido do tenant.


Feature Gating por Plano

O FeatureGateMiddleware avalia o plano activo do tenant e os addons em cada pedido.

Funcionalidades por plano

FuncionalidadeStarterBusinessProfessionalEnterprise
Produtos ilimitadosSimSimSimSim
Utilizadores admin/staff13515
Multi-sistemaNaoNao25
Relatorios avancadosNaoSimSimSim
API accessNaoNaoSimSim
Suporte prioritarioNaoNaoNaoSim

Addons disponiveis (32 no total)

Os addons sao activados por tenant, independentemente do plano. Exemplos:

AddonPrecoO que activa
wishlistEUR 3.90/mesLista de desejos para clientes
loyaltyEUR 7.90/mesPrograma de pontos e fidelizacao
reviewsEUR 4.90/mesSistema de avaliacoes de produtos
recommendationsEUR 5.90/mesRecomendacoes personalizadas
advanced-searchEUR 6.90/mesPesquisa por atributos e filtros avancados
moloniEUR 19.90/mesFaturacao fiscal portuguesa via Moloni

Activar ou desactivar addon

POST /api/sysadmin/tenants/{tenant_id}/addons
Authorization: Bearer <sysadmin_jwt>
Content-Type: application/json

{
"addon": "wishlist",
"active": true
}

Branding do Tenant

Cada tenant pode personalizar a aparencia do seu storefront. O branding e injectado via CSS variables no Next.js storefront em tempo de render (SSR).

Campos de branding:

CampoTipoDescricao
logo_urlstring (URL MinIO)Logo da loja
primary_colorstring (hex)Cor principal
secondary_colorstring (hex)Cor secundaria
banner_urlstring (URL MinIO)Banner da pagina inicial
taglinestringSlogan da loja
social_instagramstring (URL)Perfil Instagram
social_facebookstring (URL)Pagina Facebook
social_twitterstring (URL)Perfil Twitter/X

Actualizar branding via API

PATCH /api/sysadmin/tenants/{tenant_id}/branding
Authorization: Bearer <sysadmin_jwt>
Content-Type: application/json

{
"primary_color": "#1a2b3c",
"tagline": "O melhor armamento airsoft de Portugal"
}

O admin do proprio tenant tambem pode alterar o branding a partir do painel de administracao.


Dominios

Dominio automatico

Cada tenant recebe automaticamente o subdominio {slug}.store.inallweb.com. Nao requer configuracao adicional — o DNS wildcard *.store.inallweb.com ja esta configurado no Cloudflare.

Dominio custom

O tenant pode usar um dominio proprio (ex: loja.defenseops.pt). O processo e:

  1. O admin do tenant configura um registo CNAME no seu DNS:

    loja.defenseops.pt CNAME stores.inallweb.com
  2. O SysAdmin regista o dominio custom na plataforma:

    POST /api/sysadmin/tenants/{tenant_id}/domains
    Authorization: Bearer <sysadmin_jwt>
    Content-Type: application/json

    {
    "domain": "loja.defenseops.pt"
    }
  3. O sistema verifica o CNAME e activa o dominio. O TLS e gerido automaticamente pelo Traefik/Caddy via Let's Encrypt.

Listar dominios de um tenant

GET /api/sysadmin/tenants/{tenant_id}/domains
Authorization: Bearer <sysadmin_jwt>

Stripe Connect — Onboarding

Cada tenant precisa de completar o onboarding Stripe Connect para poder receber pagamentos. O processo e federado — o form KYC esta embebido na loja (whitelabel), sem redireccionar para o Stripe.

Estado do onboarding

EstadoDescricao
not_startedTenant ainda nao iniciou o onboarding
pendingOnboarding em curso (KYC submetido, aguarda Stripe)
activeConta Stripe Connect activa, pode receber pagamentos
restrictedConta com restricoes (documentos em falta)
disabledConta desactivada pelo Stripe

Verificar estado Stripe de um tenant

GET /api/sysadmin/tenants/{tenant_id}/stripe
Authorization: Bearer <sysadmin_jwt>

Resposta:

{
"stripe_connect_account_id": "acct_xxxxxxxxxxxxx",
"onboarding_status": "active",
"charges_enabled": true,
"payouts_enabled": true
}

O campo stripe_connect_account_id e guardado na tabela tenants da base de dados. Se for null, o onboarding ainda nao foi completado.


Canary Tenant

O tenant com slug inallweb e o tenant canary da plataforma. E usado exclusivamente para testes automaticos E2E em todos os ambientes, incluindo staging e prod.

Regras criticas:

  • Nunca remover o tenant inallweb
  • Nunca usar tenants de clientes reais em testes automaticos
  • Em staging e prod, os testes E2E correm apenas no canary inallweb
  • Variavel de ambiente: E2E_CANARY_TENANT=inallweb
  • User canary: admin@inallweb.com

Os dados criados pelos testes no canary sao limpos no afterAll de cada suite.


Hierarquia de Roles

RoleScopePermissoes
SysAdminPlataforma todaProvisionar tenants, gerir planos, ver metricas globais, acesso a todos os endpoints /api/sysadmin/*
AdminTenant proprioControlo total do seu tenant: produtos, encomendas, staff, branding, configuracoes
StaffTenant proprioOperar no tenant (processar encomendas, gerir stock); sem acesso a configuracoes financeiras
CustomerTenant proprioVer e editar os seus proprios dados; historico de encomendas

Os roles SysAdmin nao pertencem a nenhum tenant especifico — tem acesso transversal via endpoints /api/sysadmin/*.


Monitoring de Tenants

Listar todos os tenants

GET /api/sysadmin/tenants
Authorization: Bearer <sysadmin_jwt>

Parametros de query opcionais: plan, status, page, per_page.

Resposta (paginada):

{
"items": [
{
"id": "uuid",
"slug": "defenseops",
"name": "DefenseOps Airsoft",
"plan": "professional",
"status": "active",
"created_at": "2025-10-15T08:00:00Z"
}
],
"total": 42,
"page": 1,
"per_page": 20
}

Detalhe de um tenant

GET /api/sysadmin/tenants/{tenant_id}
Authorization: Bearer <sysadmin_jwt>

Metricas de um tenant

GET /api/sysadmin/tenants/{tenant_id}/metrics
Authorization: Bearer <sysadmin_jwt>

Resposta:

{
"orders_total": 1247,
"orders_last_30_days": 89,
"revenue_last_30_days": 12450.00,
"products_count": 634,
"customers_count": 892
}

Health de um tenant

GET /api/sysadmin/tenants/{tenant_id}/health
Authorization: Bearer <sysadmin_jwt>

Resposta:

{
"operational": true,
"last_order_at": "2026-04-09T09:45:00Z",
"stripe_active": true,
"minio_accessible": true
}

Desactivar ou Suspender um Tenant

Suspender (acesso bloqueado, dados preservados)

PATCH /api/sysadmin/tenants/{tenant_id}
Authorization: Bearer <sysadmin_jwt>
Content-Type: application/json

{
"status": "suspended"
}

Enquanto suspenso, o storefront retorna 503 e o admin panel rejeita o login com mensagem de suspensao.

Reactivar

PATCH /api/sysadmin/tenants/{tenant_id}
Content-Type: application/json

{
"status": "active"
}

RGPD e Proteccao de Dados

Os dados pessoais de cada tenant estao completamente isolados. Um tenant nunca tem acesso aos dados de outro tenant.

Direito ao apagamento

Quando um cliente final (Customer) solicita o apagamento dos seus dados:

DELETE /api/sysadmin/tenants/{tenant_id}/customers/{customer_id}/personal-data
Authorization: Bearer <sysadmin_jwt>

A operacao anonimiza os campos pessoais (nome, email, telefone, morada) mas preserva os registos de encomendas para fins fiscais e contabilisticos (obrigacao legal).

Eliminacao completa de tenant

A eliminacao de um tenant apaga todos os dados associados, incluindo produtos, encomendas, clientes, imagens no MinIO e a conta Stripe Connect (desligada do tenant, nao eliminada no Stripe).

Esta operacao e irreversivel. Deve ser confirmada explicitamente.

DELETE /api/sysadmin/tenants/{tenant_id}
Authorization: Bearer <sysadmin_jwt>
X-Confirm-Delete: yes

Regras de logs

  • Nunca logar dados pessoais (emails, nomes, NIF, moradas, numeros de telefone)
  • Logar o ID do tenant e do utilizador, nunca o conteudo dos campos pessoais
  • Exemplo correcto: "acesso ao perfil do cliente 123 no tenant defenseops"
  • Exemplo incorrecto: "acesso ao perfil joao@example.com"

Anonimizacao para staging/dev

Antes de copiar dados de producao para staging ou dev, e obrigatorio correr o script de anonimizacao:

python stores-management/backend/scripts/anonymize_db.py \
--source prod \
--target staging

Transformacoes aplicadas:

CampoValor anonimizado
Emailuser_{id}@test.inallweb.com
NomeCliente {id}
Telefone+351900000{id}
NIF000000000
MoradaRua de Teste, {id}, Lisboa

Nunca copiar dados de prod sem anonimizar. Esta operacao deve ser automatizada via cron — nunca executada manualmente em producao.