Pra quem é este doc? Você acabou de clonar o repo, copiou
.env.examplepra.env.local, abriu o arquivo e bateu o desespero: "o que é cada uma dessas chaves e onde eu pego?". Este guia resolve isso. Sem pular etapas, sem assumir que você já configurou nada antes.Tempo estimado: 60–90 minutos pra preencher tudo do zero. Você pode fazer em partes — o app sobe com algumas chaves vazias (veja Ordem recomendada).
Custo: R$ 0 pra rodar em dev. Todos os serviços listados têm free tier generoso. Marcamos com 💳 onde a plataforma pede cartão (mesmo no plano grátis) só pra validar identidade.
- Antes de começar
- Ordem recomendada
- Supabase — banco + auth + storage
- Upstash Redis — rate limit + idempotência
- WAHA — WhatsApp
- Anthropic + Vercel AI Gateway — IA
- OpenAI — embeddings do RAG
- Sentry — monitoramento de erros
- Resend — email transacional
- Nuvemshop — integração e-commerce
- Chaves geradas localmente
- Verificação final
- Troubleshooting
- Próximos passos
O que você precisa ter instalado:
- Node.js 22 — recomendamos via nvm. No repo, rode
nvm usee ele puxa a versão certa. A suítepnpm test:dbexige Node 22+: os testes instanciam o cliente do Supabase, que precisa doWebSocketglobal (nativo só a partir do 22). - Docker Desktop — pra rodar o WAHA local. Download.
- pnpm —
npm install -g pnpm(gerenciador de pacotes que usamos). - Git — você já tem se clonou o repo.
- Conta de email principal — vai usar pra criar contas em vários SaaS.
- Cartão de crédito 💳 — alguns serviços pedem só pra "comprovar identidade" mesmo no plano grátis (Supabase, Sentry). Se ficar dentro do free tier, não cobram nada.
Como o .env.local funciona:
- Fica na raiz do projeto:
/seu-caminho/DeskcommCRM/.env.local. - Cada linha é
NOME_DA_VARIAVEL=valor— sem espaço antes/depois do=. - Strings com caracteres especiais: envolva em aspas duplas (
"valor com espaço"). - Variáveis com
NEXT_PUBLIC_no nome são expostas no browser — nunca coloque secret aí. - O resto fica server-only.
Regra de ouro: nunca commite o .env.local. Já está no .gitignore, mas confira com git status antes de qualquer push.
Se você quer rodar o app o mais rápido possível com o mínimo viável:
🟢 Mínimo pra pnpm dev subir sem erro fatal (~15 min):
- Supabase — sem isso nada funciona (auth + DB).
- Chaves geradas localmente —
INTERNAL_SECRET, encryption keys. - Upstash Redis — rate limit é gate de várias rotas.
🟡 Pra testar features de IA (+10 min): 4. Anthropic ou Vercel AI Gateway. 5. OpenAI — embeddings do RAG.
🟡 Pra testar WhatsApp (+15 min): 6. WAHA + ngrok (precisa URL pública).
⚪ Pode ficar vazio em dev (degradam graciosamente):
- Sentry — não monitora erros, mas app sobe.
- Resend — emails não saem (vão pro console.log), mas app sobe.
- Nuvemshop — UI mostra "Integração não configurada".
O que é: Backend-as-a-service. Aqui mora seu Postgres, autenticação, storage de mídia e realtime. Sem isso, nada funciona. Free tier: 500MB DB + 1GB storage + 50k MAU. Suficiente pra dev e protótipos.
- Acesse https://supabase.com → Start your project → faça login com GitHub.
- No dashboard, clique New project.
- Name:
deskcomm-dev(ou o que quiser). - Database password: clique no ícone de dado pra gerar. Salve essa senha num gerenciador (1Password, Bitwarden) — você vai precisar pra rodar migrations e nunca verá ela de novo no dashboard.
- Region:
South America (São Paulo)— latência mínima pro Brasil. - Pricing plan: Free.
- Name:
- Clique Create new project. Aguarde ~2 minutos enquanto provisiona.
- Quando carregar, vá em Project Settings (engrenagem no menu lateral) → API.
Na tela Project Settings → API:
| Campo no Supabase | Variável no .env.local |
Detalhe |
|---|---|---|
Project URL (ex: https://abc123.supabase.co) |
NEXT_PUBLIC_SUPABASE_URL |
URL pública, pode ir pro browser |
Project API keys → anon public |
NEXT_PUBLIC_SUPABASE_ANON_KEY |
Chave pública. RLS protege os dados |
Project API keys → service_role secret |
SUPABASE_SERVICE_ROLE_KEY |
CRÍTICA. Bypassa RLS. Nunca exponha. Nunca commite. |
⚠️ Aviso de segurança: aservice_roleé o equivalente a senha de root do banco. Se vazar, qualquer pessoa lê/escreve tudo. Em prod, configure rotação trimestral.
Depois de preencher as 3 variáveis acima:
# Instale o CLI do Supabase
brew install supabase/tap/supabase # macOS
# Ou siga https://supabase.com/docs/guides/cli/getting-started pra Windows/Linux
# Login (abre o browser)
supabase login
# Conecte ao seu projeto (project-ref está na URL do dashboard)
supabase link --project-ref <seu-project-ref>
# Num projeto Supabase NOVO, habilite antes as extensões que o schema usa —
# sem elas o baseline para em `type public.vector does not exist`.
psql "$SUPABASE_DB_URL" -v ON_ERROR_STOP=1 -c \
'create extension if not exists vector with schema public;
create extension if not exists citext with schema public;
create extension if not exists pg_trgm with schema public;'
# Aplica o SCHEMA — o baseline, não a cadeia de migrations.
# Re-aplicar é seguro e não erra: o arquivo é idempotente (issue #184).
psql "$SUPABASE_DB_URL" -v ON_ERROR_STOP=1 -f supabase/baseline.sql
⚠️ Não usesupabase db pushnum banco novo. As migrations0001–0009e0013são stubsSELECT 1;— o schema fundacional não está nelas, e a cadeia não sobe do zero. Odb pushpassa sem erro e te deixa com um banco vazio, e você só descobre muito depois, num erro que não aponta pra cá. Osupabase/baseline.sqlé o schema real e é exatamente o que ohostgator-setup-kit/install.shaplica na VPS.As migrations continuam sendo a fonte da verdade para quem já tem um banco e está atualizando — é o baseline que serve pra criar do zero.
Se não conseguir usar o CLI, abra SQL Editor → New query no dashboard e cole o conteúdo
de supabase/baseline.sql.
No menu lateral → Storage → New bucket:
- Name:
whatsapp-media - Public bucket: NÃO (deixe desmarcado — usamos URLs assinadas)
O que é: Redis serverless. Usado pra rate limit de API e cache de idempotency keys. Free tier: 10k commands/dia, 256MB. Mais que suficiente pra dev.
- Acesse https://upstash.com → Sign up com GitHub.
- No dashboard, clique Create Database.
- Name:
deskcomm-dev - Type: Regional (mais barato que Global pra dev)
- Region:
sa-east-1(São Paulo) — ouus-east-1se SP não estiver disponível no free tier. - Eviction: habilitado (default).
- Name:
- Clique Create.
- Na tela do banco criado, role até a seção REST API. Você vai ver:
| Campo no Upstash | Variável no .env.local |
|---|---|
| UPSTASH_REDIS_REST_URL (botão de copy) | UPSTASH_REDIS_REST_URL |
| UPSTASH_REDIS_REST_TOKEN (clique no olhinho pra revelar) | UPSTASH_REDIS_REST_TOKEN |
💡 O Upstash mostra os snippets prontos em vários formatos. Use a aba
.envque ele já formata certo — é só colar.
O que é: Servidor que se conecta ao WhatsApp e expõe API HTTP. O default fixo é devlikeapro/waha:latest-2026.7.2, engine NOWEB. A prova local desta versão criou duas sessões CORE simultâneas até SCAN_QR_CODE; não houve pairing nem envio real. Versão/engine e pós-condição da operação determinam a compatibilidade; o tier sozinho não bloqueia um segundo número.
WAHA tem um esquema de auth particular: o container guarda o hash SHA512 da chave; a app envia o plaintext em cada request. Por isso você precisa dos dois.
# 1. Gere uma string aleatória forte (no terminal)
openssl rand -hex 32
# → cola algo tipo: 7a3f9b2c1d4e5f...Esse é o plaintext. Copie. Agora gere o hash SHA512 hex dele:
# 2. Hash do plaintext
echo -n "7a3f9b2c1d4e5f..." | shasum -a 512 | awk '{print $1}'
# → cola algo tipo (longão, ~128 chars): 9f8e7d6c...
⚠️ Erro #1 de quem clona o projeto: confundir plaintext com hash. Memoriza:
- O container WAHA recebe o HASH → vai em
WAHA_API_KEY_SHA512.- O app Next.js envia o PLAINTEXT no header
X-Api-Key→ vai emWAHA_API_KEY.
WAHA assina cada webhook com HMAC SHA512. Geramos um segundo secret pra isso:
openssl rand -hex 32
# → cola em WAHA_HMAC_SECRET# Plaintext que a app Next envia no header X-Api-Key
WAHA_API_KEY=<plaintext-do-passo-1>
# Hash SHA512 do plaintext acima — usado pelo docker-compose pra configurar o container
WAHA_API_KEY_SHA512=<hash-do-passo-1>
# HMAC do webhook
WAHA_HMAC_SECRET=<plaintext-do-passo-2>
# WAHA roda em localhost:3030 (mapeamento do docker-compose, host:3030 → container:3000)
WAHA_API_BASE_URL=http://localhost:3030
# URL pública que o WAHA chama de volta — preenchido no Passo 4
WAHA_WEBHOOK_BASE_URL=WAHA precisa chamar nossa app de volta quando chega mensagem. Localhost não serve — precisa de URL HTTPS pública.
# Instale ngrok
brew install ngrok
# Cadastre conta grátis em https://ngrok.com e pegue seu authtoken
ngrok config add-authtoken <seu-token>
# Em outro terminal, expõe a porta 3000 (onde o Next.js vai rodar)
ngrok http 3000O ngrok mostra: Forwarding https://abc-123-456.ngrok-free.app -> http://localhost:3000.
Copie a URL https://... e cole em:
WAHA_WEBHOOK_BASE_URL=https://abc-123-456.ngrok-free.app
⚠️ A URL do ngrok muda toda vez que você reinicia (no plano free). Pague $8/mês pelo subdomínio fixo se for trabalhar muito com WAHA, ou use Cloudflare Tunnel (gratuito com domínio próprio).
docker compose up -dConfira em http://localhost:3030/dashboard/ que o WAHA está respondendo (painel do WAHA). Pra criar sessão e escanear QR, veja a doc oficial: https://waha.devlikeapro.com/docs/overview/quick-start/.
O que é: O cérebro da IA conversacional (Claude). Usamos o Vercel AI Gateway preferencialmente (fallback automático entre provedores, observability, zero data retention) e o Anthropic direto como fallback. Custo: pay-per-use. Anthropic dá $5 de crédito grátis ao cadastrar.
- Acesse https://vercel.com → faça login.
- No dashboard → AI (no menu lateral) → Get started with AI Gateway.
- Clique Create API Key → nome
deskcomm-dev→ copie a chave.
AI_GATEWAY_API_KEY=<chave-do-gateway>
AI_GATEWAY_BASE_URL=https://ai-gateway.vercel.sh/v1
VERCEL_AI_GATEWAY_URL=https://ai-gateway.vercel.sh/v1💡 Com o Gateway, o código usa strings tipo
"anthropic/claude-sonnet-4-6"— o Gateway resolve qual provedor chamar. Se Anthropic estiver fora, ele tenta o backup automaticamente.
- Acesse https://console.anthropic.com → Sign Up. 💳
- Adicione método de pagamento (eles dão $5 de crédito grátis).
- Settings → API Keys → Create Key → nome
deskcomm-dev→ copie.
ANTHROPIC_API_KEY=sk-ant-api03-...
⚠️ Se as duas chaves estiverem vazias, o workerai-response-workerpula comskip="ai_gateway_key_missing"— o app sobe normal, só não responde com IA. Em dev tá ok. Em prod, configure pelo menos uma das duas.
O que é: Usado só pra gerar embeddings (vetores) das bases de conhecimento dos tenants pro chatbot RAG. Não usamos GPT pra gerar texto — esse trabalho é do Claude. Custo: baratíssimo. text-embedding-3-small = $0.02 / 1M tokens.
- Acesse https://platform.openai.com → Sign up. 💳
- Adicione método de pagamento (eles não dão mais crédito grátis em conta nova).
- API Keys → Create new secret key → nome
deskcomm-dev-embeddings→ copie.
OPENAI_API_KEY=sk-proj-...O que é: Captura erros, stack traces e performance. Sem isso, você só sabe que o app quebrou quando o cliente reclama. Free tier: 5k erros/mês, 10k performance units/mês.
- Acesse https://sentry.io → Sign up com GitHub. 💳
- Crie um workspace (ou use o pessoal) → Create Project.
- Platform:
Next.js - Alert frequency: "Alert me on every new issue"
- Project name:
deskcomm-dev
- Platform:
- Após criar, o Sentry mostra o DSN numa tela de quickstart. É uma URL tipo
https://abc123@o456.ingest.sentry.io/789. - Se você fechou a tela: Project Settings → Client Keys (DSN) → copie o "DSN" público.
SENTRY_DSN=https://abc123@o456.ingest.sentry.io/789💡 O DSN é considerado "público o suficiente" — pode ir no client. Mas mantenha como server var por padrão (já está em
.env.local).
O que é: Serviço de envio de email. Usado pra magic links, reset de senha, exports LGPD, notificações. Free tier: 3k emails/mês, 100/dia. Suficiente pra dev e MVP.
- Acesse https://resend.com → Sign up com GitHub.
- API Keys → Create API Key:
- Name:
deskcomm-dev - Permission:
Sending access(nãoFull access). - Domain:
All domains(em dev) — em prod, restrinja ao domínio verificado.
- Name:
- Copie a chave (começa com
re_...). Ela só aparece uma vez.
RESEND_API_KEY=re_...
RESEND_FROM_EMAIL=onboarding@resend.dev💡 O domínio
onboarding@resend.devé compartilhado e funciona no plano free pra testes. Em prod, verifique seu próprio domínio no Resend (DNS records SPF + DKIM) e usenoreply@seudominio.com.ℹ️ Se você não configurar o Resend, o app sobe normal — só faz
console.logem vez de enviar emails de verdade. Bom pra dev sem precisar gastar quota.
O que é: Plataforma de e-commerce brasileira. Nossa integração OAuth importa pedidos, produtos, clientes pro CRM.
ℹ️ Pode pular em dev. Se essas vars ficarem vazias, a UI mostra "Integração não configurada" e você toca o resto do app normal.
- Acesse https://partners.tiendanube.com/ → Sign up como parceiro (gratuito).
- No dashboard de parceiro → Apps → Create new app.
- App name:
DeskcommCRM Dev. - Redirect URI:
https://<sua-url-ngrok>.ngrok-free.app/api/v1/integrations/nuvemshop/callback(mesmo ngrok do WAHA, ou outro). - Scopes: marque tudo relacionado a
read_orders,read_customers,read_products,write_orders(pra atualizar status).
- App name:
- Após criar, a tela do app mostra:
| Campo no portal | Variável no .env.local |
|---|---|
App ID (na URL: partners.tiendanube.com/apps/12345) |
NUVEMSHOP_APP_ID (= 12345) |
| Client ID | NUVEMSHOP_CLIENT_ID |
| Client Secret (clique pra revelar) | NUVEMSHOP_CLIENT_SECRET |
NUVEMSHOP_APP_ID=12345
NUVEMSHOP_CLIENT_ID=...
NUVEMSHOP_CLIENT_SECRET=...- Configure também a URL pública do app:
NEXT_PUBLIC_APP_URL=https://<sua-url-ngrok>.ngrok-free.appA URL do callback OAuth precisa bater exatamente com a Redirect URI cadastrada no portal — incluindo https, sem barra final.
Estas você gera você mesmo — não tem dashboard, não tem login. São strings aleatórias usadas pra criptografia interna e segredos da app.
# Rode 6x e cole cada saída numa variável diferente
openssl rand -hex 32Distribua nas variáveis:
# Bearer secret pros endpoints /api/v1/cron/* (DIFERENTE do service role key)
INTERNAL_SECRET=<saída-1>
# Criptografia de PII (LGPD)
CPF_ENCRYPTION_KEY=<saída-2>
# Criptografia de tokens OAuth Nuvemshop
NUVEMSHOP_OAUTH_ENCRYPTION_KEY=<saída-3>
# Criptografia de credenciais BYO-WAHA (cliente que roda WAHA próprio)
WAHA_BYO_ENCRYPTION_KEY=<saída-4>
# HMAC do cookie de impersonate (super-admin) — mínimo 32 chars
IMPERSONATE_COOKIE_SECRET=<saída-5>
# Assinatura de URLs de export LGPD
LGPD_SIGNING_KEY=<saída-6>
⚠️ NUNCA reutilize a mesma string em produção. Cada uma criptografa uma coisa diferente — se vazar uma, queremos blast radius limitado.
⚠️ NUNCA mudeCPF_ENCRYPTION_KEYouNUVEMSHOP_OAUTH_ENCRYPTION_KEYdepois que tiver dados em prod — você não consegue mais descriptografar o que foi salvo. Rotação dessas chaves exige migration de re-encryption.
# DPO oficial (LGPD) — vai como reply-to em emails de data request
LGPD_DPO_EMAIL=dpo@seudominio.com
# Validade dos links de export LGPD (default 72h)
LGPD_EXPORT_EXPIRES_HOURS=72
# URLs canônicas (em dev geralmente localhost; em prod aponta pra domínio)
NEXT_PUBLIC_APP_URL=http://localhost:3000
NEXT_PUBLIC_ADMIN_URL=http://localhost:3000
# Workers — ritmo com que o worker roda os handlers do event_log. Vazio = os
# defaults (2s com trabalho, 10s ocioso, 50 por lote). Não há mais opt-in: o
# `EVENT_LOG_WORKER_ENABLED` que ficava aqui nunca teve leitor e saiu em
# 2026-08-25, junto com a chegada do laço de verdade no worker.
EVENT_LOG_DRAIN_INTERVAL_MS=2000
EVENT_LOG_DRAIN_IDLE_INTERVAL_MS=10000
EVENT_LOG_DRAIN_BATCH_SIZE=50Depois de preencher tudo, valide:
# 1. Type-check (vai reclamar de env faltando)
pnpm typecheck
# 2. Sobe o app
pnpm dev
# 3. Em outro terminal, bate no health check
curl http://localhost:3000/api/v1/healthA resposta deve ser tipo:
{
"data": {
"supabase": "ok",
"redis": "ok",
"waha": "ok"
}
}Se algum service vier "degraded" ou "down", abra o terminal do pnpm dev e veja o erro — geralmente é variável faltando ou typo no valor.
O Zod (lib/env.ts) valida no startup. Olha a lista de erros que ele imprime — fala exatamente qual var está faltando ou com formato errado.
Você esqueceu de preencher NEXT_PUBLIC_SUPABASE_URL ou tem espaço/aspa errada. Confira se a linha é exatamente NEXT_PUBLIC_SUPABASE_URL=https://abc.supabase.co (sem aspas, sem espaço antes do =).
A anon key ou service role key foi colada errada (cortou no meio). JWTs do Supabase são longos (~200 chars). Volte no dashboard e use o botão Copy em vez de selecionar manualmente.
Provável: você botou o hash em WAHA_API_KEY em vez do plaintext. Confira: a app envia o que tá no .env.local no header — o container WAHA é quem tem o hash (em WAHA_API_KEY_SHA512). Refaça o passo 1 do WAHA.
- O ngrok está rodando? (
ngrok http 3000) - A URL do ngrok atual está em
WAHA_WEBHOOK_BASE_URL? (muda a cada restart no plano free). - Você reiniciou o
pnpm devdepois de mudar o.env.local? Variáveis de ambiente são lidas no boot. - Confira logs do container:
docker logs deskcomm-waha.
Algum outro processo rodando. Mata com lsof -ti:3000 | xargs kill -9 ou roda o Next em outra porta: pnpm dev -- -p 3001 (e atualize WAHA_WEBHOOK_BASE_URL no ngrok pra apontar pra nova porta).
Esperado em dev se você ainda não configurou o Resend. Emails caem no console.log. Só configure se for testar fluxos de email (LGPD export, magic link).
Confira se você está logado: supabase login — vai abrir o browser pra autorizar. Depois supabase link --project-ref <ref> de novo.
Project Settings → Database → Reset database password. Lembrando que isso invalida conexões existentes.
- Docker Desktop está rodando? Ícone na barra de menus.
- Em Mac M1/M2/M3, o
platform: linux/amd64no docker-compose pode dar warning — é normal, só roda mais devagar via emulação. Funciona.
Com tudo verde no /api/v1/health:
- Crie usuários de teste rodando
pnpm tsx scripts/seed-e2e-credentials.ts— gera.e2e-creds.jsoncom admin/manager/agent. - Leia
README.mdpra fluxo de criar sessão WAHA + escanear QR. - Leia
CLAUDE.mdpra convenções do projeto. - Veja
tasks/todo.mdpra entender o backlog atual.
Bem-vindo ao DeskcommCRM. 🛠️
Achou um erro neste guia? Abra uma issue ou mande um PR — esse doc vive da contribuição da comunidade.