docs/white-label.md
Rama main · réplica de lectura del repositorio original.
🇧🇷 Português · [🇺🇸 English](white-label.en.md) · [🇪🇸 Español](white-label.es.md)
# Instalar para clientes (agências e revendedores)
Guia para quem instala o DeskcommCRM **para outras empresas** — agência, consultoria, revendedor — e cobra por isso.
A licença é MIT: você pode modificar, hospedar para terceiros, revender e cobrar o que quiser. Não há royalty, não há cláusula proibindo hospedagem comercial e não existe versão paga que trave funcionalidade do seu cliente.
---
## Trocar a marca
**Pela tela, e sem reiniciar nada.** Em `/admin/marca` você troca o **nome do sistema** e a **cor da marca**. Salvou, recarregou, a interface inteira já está repintada — a barra lateral, os botões, o anel de foco, o título da aba e o ícone do navegador.
A cor é **derivada**, não aplicada crua: de um hex saem onze tons nos dois temas (claro e escuro), com um piso de contraste calculado por papel e por superfície. Se a cor que você escolheu ficaria ilegível como texto de botão no tema escuro, o sistema anda os degraus necessários e a tela **te mostra** em qual tom cada coisa vai pousar, antes de salvar. Nada de "escolhi amarelo e o botão ficou branco no branco".
**O logo também.** Na mesma tela você **sobe o arquivo** — PNG ou JPG, até 512 KB. Ele vai para o storage da sua própria instalação e passa a valer na hora, sem reiniciar nada e sem você hospedar imagem em lugar nenhum. Altura fixa e largura livre, para não distorcer arte de proporção qualquer; sem logo, o nome aparece como texto.
**E o ícone do aplicativo e do navegador.** Logo abaixo do logo, esse campo recebe uma imagem quadrada — PNG ou JPG, até 512 KB, de preferência 512×512 ou maior. A mesma imagem aparece na aba de todas as telas, inclusive o login, e no aplicativo instalado. O manifest anuncia PNGs de 192×192 e 512×512, renderizados no servidor a partir do arquivo limitado no Storage; nenhum endereço arbitrário é buscado. Sem arquivo válido, mantém o símbolo do produto ou a inicial sobre a cor da marca. Remover o arquivo volta a esse desenho. O ícone é da instalação, não de cada organização; trocar de organização não renomeia o aplicativo. Navegadores podem demorar para atualizar um app já instalado; remover o aplicativo e instalá-lo novamente permite conferir a nova marca.
### CSS personalizado
Na mesma página **Marca da instalação**, o administrador da instalação pode adicionar CSS para ajustes visuais finos no login e nas telas de todas as organizações. A folha vale globalmente, sem reiniciar o servidor, e pode ser removida apagando o conteúdo e salvando.
Por segurança, não é um editor de CSS irrestrito: aceita seletores de classes e propriedades visuais de cor, borda, sombra e tipografia. Recusa seletores globais ou por ID, regras `@`, URLs e carregamento remoto, funções fora de `rgb`/`rgba`/`hsl`/`hsla`/`var`/`calc`/`min`/`max`/`clamp`, comentários, escapes, scripts, `!important` e propriedades de layout e posicionamento. O limite é 16 KB. Se uma folha já salva deixar de passar na validação, ela não é aplicada e a tela Marca mostra o motivo. Como a configuração é da instalação, qualquer ajuste também afeta as organizações atendidas por ela; confira as telas depois de salvar.
Exemplo:
```css
.text-muted-foreground {
color: #52645a;
}
.rounded-md {
border-radius: 12px;
}
```
**Se o CSS deixar as telas ilegíveis.** Uma folha válida ainda pode esconder texto (cor transparente, fonte de tamanho zero) ou cobrir a tela com uma sombra — inclusive o login e a própria página Marca. Para sair disso, abra a página com `?sem_css=1` no endereço: ela vem sem o CSS personalizado, só para você. Entre por `/login?sem_css=1`, depois abra `/admin/marca?sem_css=1` digitando o endereço, apague o conteúdo do campo e salve. Sem acesso à tela, pelo servidor: `psql "$SUPABASE_DB_URL" -c "delete from public.platform_config where chave = 'APP_CUSTOM_CSS';"` — o CSS deixa de valer em até 30 segundos. Passo a passo em [`runbooks/css-personalizado.md`](runbooks/css-personalizado.md).
O arquivo é aceito **pelos bytes, não pela extensão**. Renomear um `.svg` para `.png` não engana: o sistema lê o conteúdo, recusa e diz por quê. Isso não é preciosismo — SVG é XML e pode carregar script, que executaria se alguém abrisse a imagem direto pelo endereço dela, num bucket que é público por necessidade.
Quem preferir hospedar por conta própria continua podendo, pelo `.env`:
```bash
APP_LOGO_URL=https://cdn.suaempresa.com.br/logo.svg
```
Entre os dois, **o arquivo subido pela tela vence a URL do `.env`** — quem subiu expressou a escolha mais recente. E remover o logo de uma organização **devolve o da instalação**, não "nenhum": as camadas caem uma na outra, em vez de apagar.
### As três variáveis do `.env`, e o papel real delas
```bash
APP_NAME=Vendas Turbo CRM
APP_LOGO_URL=https://cdn.suaempresa.com.br/logo.svg
APP_ACCENT_HEX=#7a5cd6
```
O `install.sh` pergunta **duas** delas e as grava: o `APP_NAME` (Enter mantém o padrão) e o `APP_ACCENT_HEX` (Enter usa a cor do sistema). `APP_LOGO_URL` ele não pergunta — o caminho normal do logo é subir o arquivo pela tela, e esta chave existe para quem prefere hospedar por conta própria.
> A cor é pedida com validador: só `#` + 6 dígitos passa. É mais estreito do que a tela aceita, e de propósito — os **e-mails de acesso** (confirmação de conta e recuperação de senha) leem essa chave do `.env`, e eles só reconhecem essa forma. Um `#abc` ou um `7a5cd6` pintaria a interface com a sua cor e deixaria o verde do produto no primeiro e-mail que o seu cliente abre.
> ⚠️ **Trocar a cor pela tela depois NÃO reescreve os e-mails de acesso.** O texto deles vive dentro do Supabase (GoTrue), não no CRM, e quem o empurra para lá é o `marca-emails.sh` — que lê o **`.env`**, não o banco. Para os e-mails acompanharem uma cor trocada em `/admin/marca`: ajuste também o `APP_ACCENT_HEX` no `.env` e rode `bash hostgator-setup-kit/marca-emails.sh`. É por isso que a entrevista do instalador importa: ela é o único momento em que as duas pontas nascem iguais sem ninguém precisar saber disso.
O que essas variáveis são, exatamente: **semente e piso de rollback.**
- **Semente** — na primeira leitura, o que estiver no `.env` é gravado no banco. É assim que uma instalação nova já nasce com o seu nome.
- **Piso de rollback** — se você voltar para uma versão anterior do sistema, o `agent.sh` reverte a **imagem**, não o **banco**. A marca que sobrevive a qualquer volta é a que está no `.env`.
Depois da primeira leitura, **o banco manda**. Trocar a cor pela tela não pede `docker compose up -d`, não pede reinício, não pede nada: a leitura é feita a cada carregamento, com um cache curto que a própria tela invalida ao salvar.
### Por que isso é configuração, e não uma edição de código
Trocar a marca editando os arquivos-fonte funciona **uma vez**. No próximo `bash update.sh`, a imagem nova sobrescreve o patch e a marca do seu cliente volta a ser a nossa — normalmente sem ninguém perceber, até o cliente ver.
Configuração sobrevive a toda atualização. É por isso que a marca é lida em tempo de execução e nunca embutida na compilação: **uma única imagem Docker serve qualquer marca**. Pela mesma razão não existe "imagem com a sua marca": o `update.sh` regrava a linha `APP_IMAGE` do `.env` em toda atualização, e a sua imagem seria substituída pela nossa em silêncio, numa atualização de rotina.
---
## Marca por organização
**Uma instalação atende várias organizações, e cada uma pode ter a própria marca.** O admin de cada organização abre `Configurações → Marca` (`/app/settings/marca`) e define **nome**, **cor** e **logo** dela — sem precisar de você, e sem enxergar as outras.
A fronteira, que é deliberada:
| Onde | Qual marca aparece |
|---|---|
| `/login`, cadastro, recuperação de senha, verificação em duas etapas | A da **instalação** (a sua) |
| Dentro do sistema (`/app/...`), depois de entrar | A da **organização**, se ela tiver; senão, a da instalação |
| E-mails de acesso (confirmação de conta, recuperação de senha) | A da **instalação** |
| Convite de time, e-mails de LGPD | A da **organização** que enviou |
O motivo de o login ficar de fora não é limitação: **antes de a pessoa entrar, o sistema não sabe de qual organização ela é.** Pintar o login com a cor de alguma delas seria escolher uma no escuro.
Isso não torna a instalação dedicada obsoleta — ver a comparação abaixo, que continua valendo por infra, isolamento e discurso de venda.
### O que ainda não é configurável
Sendo direto, para você não descobrir na frente do cliente. Cada linha traz a razão medida, não a desculpa:
- **Domínio por organização.** Uma instalação, um domínio. Não há coluna de domínio no schema, o desvio por host no `proxy.ts` é um NOOP declarado (existe só como documentação da topologia pretendida), e no Edge não há banco para consultar antes de decidir a quem aquele host pertence. Cliente que exige o próprio domínio pede **instalação dedicada**.
- **Fonte.** A tipografia é a mesma em toda instalação. `next/font` resolve em tempo de **build**, e a imagem que a sua VPS baixa já vem construída — um seletor de fonte no painel salvaria um valor que nada leria. (A fonte é a Atkinson Hyperlegible, escolhida pelo Braille Institute por legibilidade; trocá-la não muda percepção de marca e piora a leitura de quem passa o dia no sistema.)
- **Tema.** O par claro/escuro é do design system. A sua marca move o **accent** — o que é ação, destaque e foco —, e deliberadamente **não** move o fundo da página: o fundo é o mesmo em toda marca, e é por isso que a cor da barra do navegador também é.
- **O relatório de LGPD do titular não leva a sua marca — e isso é de propósito.** Ver a seção própria abaixo.
- **O alarme de orçamento de IA** ainda sai com a nossa marca. É o único vazamento conhecido, e ele fica: hoje esse alarme não tem agendamento nenhum ligado, então consertar a marca dele não mudaria nada que alguém veja. Sai quando o alarme ganhar cron de verdade.
- **Dois nomes técnicos não mudam**: o cabeçalho `X-Deskcomm-Signature` dos webhooks de saída e o cookie de sessão. O primeiro é contrato com sistemas de terceiros que já conferem esse nome; renomear derrubaria integração de cliente **em silêncio** — o receptor não dá erro, apenas deixa de reconhecer.
---
## Os e-mails
### Os e-mails de acesso — o primeiro artefato que o cliente do seu cliente recebe
Confirmação de conta e recuperação de senha **não são enviados pelo sistema**: quem os monta e dispara é o serviço de autenticação do Supabase. Nenhum código nosso roda ali dentro — é texto que precisa ser **empurrado** para lá por API.
Por isso o kit traz um script próprio:
```bash
bash hostgator-setup-kit/marca-emails.sh
```
Ele sobe o assunto e o corpo dos dois e-mails com o **seu** nome e a **sua** cor, e de quebra configura o endereço de retorno do link (que nenhum script configurava antes, e é pré-requisito do link funcionar). O `install.sh` o chama sozinho, logo depois de criar o projeto Supabase; o `update.sh` também o chama, para que uma instalação antiga receba isso na primeira atualização.
**Se ele não tiver a chave de acesso da API do Supabase** (`SUPABASE_ACCESS_TOKEN`), ele não falha e não derruba a instalação: imprime exatamente o que fazer à mão no painel do Supabase e sai com sucesso. Esse é o caso de quem criou o projeto pelo painel e colou as credenciais, em vez de deixar o instalador criá-lo.
> ⚠️ **Se for fazer à mão, atenção ao caractere.** O link do template precisa levar `&token_hash=`, com **`&`**, nunca `?`. Com `?` o endereço fica com dois pontos de interrogação, o sistema perde o token e o usuário cai numa tela dizendo que o link expirou — quando o problema é o template. Nesse caso o sistema agora **nomeia a causa** em vez de dizer só "link inválido".
### Convite de time e e-mails de LGPD
Saem com a marca da **organização** que os originou — porque quem processou a solicitação, no produto do seu cliente, é o sistema do seu cliente.
Para enviá-los, preencha as duas variáveis (o `install.sh` pergunta as duas):
```bash
RESEND_API_KEY=re_...
RESEND_FROM_EMAIL=nao-responda@suaempresa.com.br
```
**O endereço tem de ser de um domínio verificado na SUA conta Resend.** Esse é o único pedaço que a marca não resolve: o nome que aparece na caixa de entrada é a marca; o endereço é de quem hospeda.
**Deixar em branco é uma escolha suportada, não um defeito.** Sem elas, o sistema não tenta enviar e não falha calado: o convite mostra o link de aceite **na própria tela**, para você copiar e mandar por onde quiser, e o export de LGPD fica pendente de revisão em vez de sumir. Antes, um endereço em branco fazia todo envio falhar lá na Resend com uma mensagem opaca, e o operador ia caçar rede, contêiner e chave por causa de uma variável vazia.
### O endereço de suporte que os seus clientes veem
```bash
SUPPORT_EMAIL=suporte@suaempresa.com.br
```
Aparece nas telas de conta suspensa e de cobrança. **Vazio significa vazio:** a tela simplesmente não mostra endereço nenhum — nunca cai de volta no nosso. Numa tela de conta suspensa isso importa: quem suspendeu foi você, não nós.
---
## O relatório de LGPD é a única coisa que NÃO leva a sua marca
Quando um titular de dados exerce o direito do Art. 18 II, o sistema gera um relatório em PDF. Esse documento **não leva marca nenhuma** — nem a nossa, nem a sua, nem logo, nem cor. Ele nomeia o **controlador**: a razão social da organização, mais o encarregado (DPO).
Isso é decisão de produto, não item esquecido. Num documento que responde a um direito legal, quem é nomeado responde pelos dados. **Você é operador, não controlador.** Trocar o nome ali pela sua marca não seria "completar o whitelabel" — seria piorar: hoje o nome que aparecia era obviamente o do software, e depois pareceria a declaração de quem responde juridicamente pelos dados daquelas pessoas.
**O que isso te pede na instalação:** confira a **Razão social** de cada organização em `Configurações → Empresa`. Ela nasce igual ao nome fantasia (é o que o instalador tem para dar), e um nome fantasia impresso como razão social num documento jurídico é o tipo de erro que só aparece quando alguém reclama.
O e-mail que entrega o link do export é outra coisa e leva, sim, a marca da organização: ele diz quem **operou**. O PDF diz quem **responde**. São papéis diferentes, e é por isso que os dois carregam nomes diferentes.
---
## Um cliente por instalação, ou todos numa só?
O sistema é multi-tenant desde a primeira linha: uma instalação atende várias organizações, e o isolamento entre elas é verificado no CI a cada alteração — um usuário de uma organização não enxerga nenhuma linha de outra. Não é promessa de marketing: é o teste `tests/invariants/rls-isolation.test.ts`, que cria duas organizações e prova o não-vazamento pelo mesmo caminho de autenticação que a produção usa.
Mesmo assim, os dois modelos servem a propósitos diferentes:
| | Uma instalação por cliente | Uma instalação para todos |
|---|---|---|
| **Marca** | A de cada cliente, inclusive na tela de entrada | A sua no login; a de cada organização dentro do sistema |
| **Custo de infra** | Uma VPS por cliente | Uma VPS |
| **Falha** | Isolada | Atinge todos |
| **Atualização** | Uma por vez, pode escalonar | Todos de uma vez |
| **Dado do cliente** | Fisicamente separado | Separado por RLS |
| **Melhor para** | Revender com a marca do cliente | Sua própria operação atendendo várias contas |
Se o seu cliente pergunta "onde ficam meus dados?", a instalação dedicada tem a resposta mais simples de dar — e de defender.
---
## O argumento jurídico que fecha venda no Brasil
A **Resolução CD/ANPD nº 19/2024** tornou obrigatórias as cláusulas-padrão contratuais para **transferência internacional de dados pessoais**, com o prazo de adequação encerrado em **23 de agosto de 2025**.
Todo cliente seu que usa um CRM estrangeiro realiza essa transferência e precisa do artefato contratual. Hospedando em VPS no Brasil, **o CRM em si não transfere dados para fora do país** — e, para ele, a obrigação não se aplica.
⚠️ **A ressalva é a IA, e ela vale para quase toda instalação.** A frase acima só é inteira enquanto nenhum provedor de IA estrangeiro estiver ligado. O atendimento automático manda a conversa ao provedor que o cliente conectou (Anthropic, OpenAI, Google, DeepSeek ou OpenRouter, todos fora do Brasil). O Jev, quando o administrador o liga em IA › Provedores, manda cada mensagem dos clientes à TypeSafe AI, nos Estados Unidos, uma de cada vez e sem o resto da conversa, depois de apagar CPF, telefone e e-mail. Cada tarefa do Jev tem a sua finalidade — medir o clima da conversa, perceber tentativa de manipulação do atendimento automático, escolher qual agente atende, perceber pedido para falar com uma pessoa ou para parar de receber mensagens e ler a resposta do cliente a um follow-up — e todas usam só essa mensagem, sozinha. Para escolher o agente, vão junto as intenções que a própria empresa cadastrou no roteador; para ler a resposta ao follow-up, as saídas e a dica que ela criou no passo “Classificar (IA)” do fluxo (textos dela, não do cliente). Nesses fluxos há transferência internacional, e as cláusulas-padrão valem para eles. O Jev nasce desligado e pede o aceite de quem administra antes de enviar qualquer coisa; a IA de atendimento, não. Fora da IA há um caso só: quando o administrador cadastra uma chave de Mapas em IA › Provedores (desligado por padrão), as coordenadas do pino de localização que o cliente manda pelo WhatsApp vão ao Google, fora do Brasil, para voltar com a rua e a cidade aproximadas — e esse fluxo também é transferência internacional.
⚠️ **Não venda como "servidor no Brasil = conformidade com a LGPD".** Isso é falso e um advogado desmonta na primeira pergunta: conformidade depende de base legal, finalidade, segurança e direitos do titular. O argumento correto e defensável é o de cima: sem transferência internacional, não há exigência de cláusulas-padrão.
---
## Operação
Cada instalação traz os scripts em `hostgator-setup-kit/`:
| Comando | O que faz |
|---|---|
| `bash update.sh` | Atualiza. Faz backup do banco **antes**, reaplica o schema de forma idempotente e confere a saúde no fim |
| `bash backup.sh` / `restore.sh` | Backup e restauração |
| `bash reset-password.sh` | Redefine a senha de um usuário |
| `bash reset-mfa.sh` | Remove a verificação em duas etapas de quem perdeu o aparelho |
| `bash healthcheck.sh` | Diagnóstico da instalação |
O `reset-mfa.sh` é o que você mais vai usar: a verificação em duas etapas é obrigatória para administradores, e trocar de celular sem salvar os códigos de recuperação é a chamada de suporte mais comum.
---
## Requisitos por instalação
**4 GB de RAM recomendados** (a stack sobe com 2 GB, mas opera no limite — o WAHA usa ~150 MB por sessão de WhatsApp), portas 80 e 443, Docker Compose v2 e um domínio com registro A apontando para o IP. A VPS não compila nada — baixa uma imagem pronta. O certificado HTTPS é emitido automaticamente no primeiro acesso.
Guia completo de instalação: [`hostgator-setup-kit/README.md`](../hostgator-setup-kit/README.md).
---
*Última atualização: 14 de agosto de 2026 — revisão completa depois do épico de marca própria. As duas frases mais citadas deste documento ("cores, fontes e tema não são configuráveis" e "a marca é por instalação, não por organização") deixaram de ser verdadeiras e foram reescritas com o limite honesto de hoje.*
*Este documento existe em três idiomas, e o pré-requisito que a versão anterior deste rodapé nomeava foi pago: as traduções carregam na primeira linha um selo com o hash do original, e editar `docs/white-label.md` sem re-selar reprova `pnpm test:unit`. Depois de traduzir, re-sele com `pnpm exec tsx scripts/selar-traducao.ts --todas`.*
*Os três READMEs ficaram **fora** do selo de propósito. São o arquivo mais editado do repositório: com selo, cada conserto viraria um PR bloqueado até ~490 linhas serem re-traduzidas duas vezes — e o desfecho realista disso não é tradução em dia, é alguém re-selar sem traduzir, que é o único jeito de o selo morrer. Eles entram quando alguém quiser pagar esse custo de olhos abertos.*
## Logo para cada tema
Em **Marca**, o campo **Logo** continua sendo a imagem padrão (tema claro e mensagens por e-mail). O campo **Logo para o tema escuro (opcional)** aceita uma segunda imagem PNG ou JPG de até 512 KB, preparada para fundo escuro. Essa imagem aparece sem moldura branca no menu lateral, na tela de entrada da instalação e na prévia. Sem a segunda imagem, o logo padrão conserva a proteção branca no tema escuro. Cada arquivo tem sua própria remoção; remover o escuro restaura o comportamento anterior.
A organização que envia um logo padrão próprio deixa de herdar a arte escura da instalação: assim, os dois temas não mostram marcas diferentes por acidente. Sem logos próprios, herda o par da instalação. O login sempre usa a marca da instalação. [Compatibilidade e reversão](runbooks/logo-por-tema.md).