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
| Plano | Preco | Utilizadores | Sistemas | Platform fee |
|---|---|---|---|---|
| Starter | EUR 24.90/mes | 1 | 1 | 2.0% |
| Business | EUR 49.90/mes | 3 | 1 | 1.5% |
| Professional | EUR 89.90/mes | 5 | 2 | 1.0% |
| Enterprise | EUR 149.90/mes | 15 | 5 | 0.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
| Funcionalidade | Starter | Business | Professional | Enterprise |
|---|---|---|---|---|
| Produtos ilimitados | Sim | Sim | Sim | Sim |
| Utilizadores admin/staff | 1 | 3 | 5 | 15 |
| Multi-sistema | Nao | Nao | 2 | 5 |
| Relatorios avancados | Nao | Sim | Sim | Sim |
| API access | Nao | Nao | Sim | Sim |
| Suporte prioritario | Nao | Nao | Nao | Sim |
Addons disponiveis (32 no total)
Os addons sao activados por tenant, independentemente do plano. Exemplos:
| Addon | Preco | O que activa |
|---|---|---|
| wishlist | EUR 3.90/mes | Lista de desejos para clientes |
| loyalty | EUR 7.90/mes | Programa de pontos e fidelizacao |
| reviews | EUR 4.90/mes | Sistema de avaliacoes de produtos |
| recommendations | EUR 5.90/mes | Recomendacoes personalizadas |
| advanced-search | EUR 6.90/mes | Pesquisa por atributos e filtros avancados |
| moloni | EUR 19.90/mes | Faturacao 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:
| Campo | Tipo | Descricao |
|---|---|---|
logo_url | string (URL MinIO) | Logo da loja |
primary_color | string (hex) | Cor principal |
secondary_color | string (hex) | Cor secundaria |
banner_url | string (URL MinIO) | Banner da pagina inicial |
tagline | string | Slogan da loja |
social_instagram | string (URL) | Perfil Instagram |
social_facebook | string (URL) | Pagina Facebook |
social_twitter | string (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:
-
O admin do tenant configura um registo CNAME no seu DNS:
loja.defenseops.pt CNAME stores.inallweb.com -
O SysAdmin regista o dominio custom na plataforma:
POST /api/sysadmin/tenants/{tenant_id}/domainsAuthorization: Bearer <sysadmin_jwt>Content-Type: application/json{"domain": "loja.defenseops.pt"} -
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
| Estado | Descricao |
|---|---|
not_started | Tenant ainda nao iniciou o onboarding |
pending | Onboarding em curso (KYC submetido, aguarda Stripe) |
active | Conta Stripe Connect activa, pode receber pagamentos |
restricted | Conta com restricoes (documentos em falta) |
disabled | Conta 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
| Role | Scope | Permissoes |
|---|---|---|
| SysAdmin | Plataforma toda | Provisionar tenants, gerir planos, ver metricas globais, acesso a todos os endpoints /api/sysadmin/* |
| Admin | Tenant proprio | Controlo total do seu tenant: produtos, encomendas, staff, branding, configuracoes |
| Staff | Tenant proprio | Operar no tenant (processar encomendas, gerir stock); sem acesso a configuracoes financeiras |
| Customer | Tenant proprio | Ver 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:
| Campo | Valor anonimizado |
|---|---|
user_{id}@test.inallweb.com | |
| Nome | Cliente {id} |
| Telefone | +351900000{id} |
| NIF | 000000000 |
| Morada | Rua de Teste, {id}, Lisboa |
Nunca copiar dados de prod sem anonimizar. Esta operacao deve ser automatizada via cron — nunca executada manualmente em producao.