mirror of
https://github.com/melgarafael/DeskcommCRM.git
synced 2026-10-02 01:28:34 +08:00
O turno lê a conversa, leva 10–40 s no modelo e envia. Se a pergunta real do
cliente chegou nesse intervalo ("Oi" / "Boa tarde" / ... / a pergunta), a
resposta sai desatualizada ("Como posso te ajudar?") e o turno seguinte, que
viu a pergunta, responde de novo. Medido numa instalação: 232 de 589 respostas
(39%) a menos de 3 min de outra ao mesmo cliente.
`respostaFicouObsoleta` usa a anotação `ultima_inbound_vista_em` que o turno já
grava antes de ler a conversa: havendo inbound mais nova, o primeiro
`send_message` do turno é recusado com `resposta_obsoleta` e o job da mensagem
nova responde a tudo. Teto (RESPOSTA_OBSOLETA_TETO_MS, 120 s; 0 desliga) pela
mensagem mais antiga sem resposta, para cliente que escreve sem parar não
ficar sem resposta.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
668 lines
38 KiB
Bash
668 lines
38 KiB
Bash
# =============================================================================
|
||
# DeskcommCRM — variáveis de ambiente
|
||
# =============================================================================
|
||
# Copie para .env.local e preencha. NUNCA commitar .env*.
|
||
# Variáveis NEXT_PUBLIC_* são expostas ao browser; o resto fica server-side.
|
||
# =============================================================================
|
||
|
||
# --- Supabase ----------------------------------------------------------------
|
||
# URL pública do projeto Supabase (ex: https://xxxx.supabase.co)
|
||
NEXT_PUBLIC_SUPABASE_URL=
|
||
# anon key (safe pra browser; RLS é o gatekeeper real)
|
||
NEXT_PUBLIC_SUPABASE_ANON_KEY=
|
||
# service role key — BYPASSA RLS. Apenas server. Nunca commitar. Rotação trimestral.
|
||
SUPABASE_SERVICE_ROLE_KEY=
|
||
# Endereço do Supabase PARA O SERVIDOR (ex: http://kong:8000). OPCIONAL.
|
||
# Vazio (o padrão) = o servidor usa a NEXT_PUBLIC_SUPABASE_URL, como sempre.
|
||
# Preencha só quando o Supabase estiver na MESMA rede do app: aí as REQUISIÇÕES
|
||
# do servidor (Auth, REST, Storage) usam o caminho curto, que não precisa sair
|
||
# para a internet. O endereço interno NÃO substitui a pública: o navegador
|
||
# continua falando com ela (Auth, Realtime e Storage) e é dela que saem os
|
||
# links que o app entrega a terceiros — mídia, avatar, PDF da LGPD e o redirect
|
||
# do login com Google. Por isso o interno não pode entrar na NEXT_PUBLIC_*.
|
||
# Nada aqui é assado no build — vale em runtime, sem rebuild da imagem.
|
||
# O navegador NUNCA a lê. Valor que não for um endereço http(s) é recusado com
|
||
# aviso no log, e vale a pública.
|
||
SUPABASE_SERVER_URL=
|
||
|
||
# --- Cron / interno ----------------------------------------------------------
|
||
# Bearer secret pros endpoints /api/v1/cron/*. DIFERENTE do service role key.
|
||
INTERNAL_SECRET=
|
||
# Secret dedicado opcional pros endpoints de cron. Se vazio, cai pra INTERNAL_SECRET.
|
||
INTERNAL_CRON_SECRET=
|
||
|
||
# Provisionamento de organizações por um sistema externo
|
||
# (POST /api/v1/tenants/provision). DESLIGADO por padrão: vazio, a rota
|
||
# responde 404. Para ligar, gere um segredo de 32+ caracteres
|
||
# (`openssl rand -hex 32`) e entregue-o só ao sistema que vai criar empresas —
|
||
# com ele, esse sistema cria organizações nesta instalação sem passar pela tela.
|
||
TENANT_PROVISIONING_SECRET=
|
||
# CRON_SECRET não é chave deste arquivo: é o nome com que o Vercel Cron injeta o
|
||
# Bearer, e vale para quem hospeda um fork lá. Em produção, se ela existir,
|
||
# lib/env.ts copia o valor para INTERNAL_CRON_SECRET. Quem usa o serviço
|
||
# `scheduler` do docker-compose NÃO precisa dela: lá o Bearer é o INTERNAL_SECRET
|
||
# (docker/scheduler/entrypoint.sh).
|
||
# Relógio local (`pnpm dev:crons`). Só bate em NEXT_PUBLIC_APP_URL; não instala
|
||
# crontab. NÃO rode contra o mesmo Supabase da VPS — disputa a fila com o prod.
|
||
# DEV_CRON_INTERVAL_MS=15000
|
||
|
||
|
||
# --- Janela do retorno agendado ----------------------------------------------
|
||
# Quão cedo e quão tarde o agente pode marcar um retorno ao cliente. Lidas pelo
|
||
# WORKER (agent-engine) e pelo APP (a capacidade que o dono liga na tela) — as
|
||
# duas leituras têm de bater, senão o mesmo horário seria aceito num caminho e
|
||
# recusado no outro. Defaults em lib/followup/janela.ts; deixe vazio para usá-los.
|
||
# FOLLOWUP_MIN_AHEAD_MS=300000 # 5 min
|
||
# FOLLOWUP_MAX_AHEAD_MS=15552000000 # 180 dias
|
||
# CRON_STAGGER_WINDOW_MS=60000 # espalha disparos do mesmo minuto (0 desliga)
|
||
|
||
# --- Encryption keys (pgcrypto) ----------------------------------------------
|
||
# Chave separada pra criptografar CPF em contacts (PII sensível LGPD).
|
||
CPF_ENCRYPTION_KEY=
|
||
# Chave pra criptografar tokens OAuth Nuvemshop em tenant_integrations.
|
||
# Agenda · Google Calendar (BYO) — opcional.
|
||
# Sem as duas, a Agenda funciona inteira: some o botão "Conectar Google".
|
||
# Onde obter: console.cloud.google.com > APIs e Serviços > Credenciais >
|
||
# ID do cliente OAuth (tipo "Aplicativo da Web").
|
||
#
|
||
# ⚠️ REGISTRE TAMBÉM O ENDEREÇO DE RETORNO, em "URIs de redirecionamento
|
||
# autorizados": a URL do app MAIS `/api/v1/agenda/google/callback`. O Google
|
||
# compara byte a byte, e registrar só a origem é o palpite natural — ele recusa
|
||
# com `redirect_uri_mismatch`, que aponta para o Google e não para a divergência.
|
||
GOOGLE_CALENDAR_CLIENT_ID=
|
||
GOOGLE_CALENDAR_CLIENT_SECRET=
|
||
|
||
# Google Ads — reporta venda de volta pro anúncio que trouxe o lead (migration
|
||
# 0296). Developer token vem do Centro de API do Google Ads (conta de GERENTE);
|
||
# o Client ID/Secret vem de um projeto no Google Cloud Console com a "Google
|
||
# Ads API" ativada. Igual ao aviso da Agenda acima, registre o endereço de
|
||
# retorno EXATO em "URIs de redirecionamento autorizados":
|
||
# <URL do app> + /api/v1/plataformas-de-anuncio/google/callback
|
||
GOOGLE_ADS_DEVELOPER_TOKEN=
|
||
GOOGLE_ADS_OAUTH_CLIENT_ID=
|
||
GOOGLE_ADS_OAUTH_CLIENT_SECRET=
|
||
|
||
NUVEMSHOP_OAUTH_ENCRYPTION_KEY=
|
||
# Chave pra criptografar credenciais BYO-WAHA (cliente que roda WAHA próprio).
|
||
WAHA_BYO_ENCRYPTION_KEY=
|
||
# Chave AES-256 (32 bytes em base64) pra criptografar API keys de provedores LLM
|
||
# (ai_provider_credentials). Gerar: openssl rand -base64 32
|
||
AI_CRED_AES_KEY=
|
||
|
||
# --- WAHA --------------------------------------------------------------------
|
||
# URL base da instância WAHA (ex: http://localhost:3000 em dev; https://waha.deskcomm.com em prod)
|
||
WAHA_API_BASE_URL=http://localhost:3000
|
||
# Plaintext da API key do WAHA. O ENV do container WAHA recebe o hash SHA512 hex desta.
|
||
# Gerar: echo -n "minha-chave-aqui" | shasum -a 512
|
||
WAHA_API_KEY=
|
||
# Segredo com que o WAHA assina os webhooks (header X-Webhook-Hmac). O app usa
|
||
# para CONFERIR a assinatura; o container do WAHA recebe o mesmo valor.
|
||
WAHA_HMAC_SECRET=
|
||
|
||
# ─── Chamada de voz WhatsApp (WaCalls, spec 18) — DESLIGADA POR PADRÃO ───────
|
||
#
|
||
# Ligá-la vincula um SEGUNDO APARELHO ao mesmo número que já atende, por um
|
||
# caminho que não é o oficial: o risco é a CONTA ser bloqueada pelo WhatsApp,
|
||
# não só o aparelho. Por isso nada aqui vem preenchido, e cada organização
|
||
# ainda precisa aceitar o risco na tela (Configurações › Segurança).
|
||
#
|
||
# Em produção o serviço vive num profile do compose que nasce desligado; ligar
|
||
# é `COMPOSE_PROFILES=voz` no .env. Vazio aqui = a feature não aparece na tela
|
||
# (banner, nunca erro).
|
||
WACALLS_API_BASE_URL=
|
||
# Bearer que o app usa para falar com o serviço. O WaCalls autenticado NÃO tem
|
||
# modo aberto: sem token, a API só aceita o cookie de login do navegador, que um
|
||
# processo server-to-server não tem. URL sem token = 401 em toda chamada.
|
||
WACALLS_API_TOKEN=
|
||
# O contêiner precisa de DUAS coisas para o áudio sair da rede local — e sem
|
||
# elas a ligação toca, conecta e fica MUDA na VPS (o WaCalls anuncia como
|
||
# candidato o IP interno do contêiner, 172.x, que navegador nenhum alcança):
|
||
# WACALLS_PUBLIC_IP o IP público da VPS, anunciado como candidato ICE
|
||
# WACALLS_WEBRTC_UDP_PORT a porta UDP fixa (sem ela, portas efêmeras
|
||
# aleatórias, impossíveis de publicar no compose)
|
||
# As duas são do CONTÊINER, não do app — por isso vivem aqui e não em lib/env.ts.
|
||
WACALLS_PUBLIC_IP=
|
||
WACALLS_WEBRTC_UDP_PORT=7881
|
||
|
||
# ── TELEFONIA POR SIP (#677) — MÓDULO OPCIONAL, DESLIGADO POR PADRÃO ────────
|
||
#
|
||
# Atender e ligar por telefone de verdade (tronco SIP + IA de voz em tempo
|
||
# real). Em produção são dois contêineres (asterisk + voice-agent) no profile
|
||
# `telefonia` do docker-compose.prod.yml: sem o profile o compose nem os cria,
|
||
# e nada aqui liga a feature sozinho.
|
||
#
|
||
# Para ligar, na VPS:
|
||
# 1. copie asterisk/pjsip.conf.example e asterisk/ari.conf.example para os
|
||
# nomes sem `.example` e ponha as credenciais do seu provedor SIP e do ARI;
|
||
# 2. ponha `telefonia` em COMPOSE_PROFILES (junto de `voz`, se já usa a
|
||
# chamada de voz do WhatsApp: COMPOSE_PROFILES=voz,telefonia);
|
||
# 3. `docker compose -f docker-compose.prod.yml --env-file .env up -d`.
|
||
#
|
||
# As portas UDP 5060 (sinalização) e 10000-10200 (áudio) passam a ser
|
||
# publicadas — é o tronco do provedor que precisa alcançá-las. A ARI (8088)
|
||
# NÃO é publicada: ela controla as chamadas e fica só na rede interna.
|
||
COMPOSE_PROFILES=
|
||
# As credenciais do ARI, iguais às de asterisk/ari.conf. Lidas pelo contêiner
|
||
# do voice-agent, não pelo app — por isso vivem aqui e não em lib/env.ts.
|
||
ARI_USERNAME=
|
||
ARI_PASSWORD=
|
||
# Porta do AudioSocket, por onde o Asterisk manda o áudio ao voice-agent. Só na
|
||
# rede interna do compose; mudar aqui exige mudar asterisk/extensions.conf.
|
||
AUDIOSOCKET_PORT=8090
|
||
# O modelo de voz em tempo real. Vazio = o default do worker.
|
||
OPENAI_REALTIME_MODEL=
|
||
# "true" exige assinatura válida em TODO webhook do WAHA. Desligado por padrão
|
||
# porque o WAHA Core não assina — exigir derrubaria a ingestão de mensagens.
|
||
# Ligue se roda WAHA Plus (ou um proxy que assine).
|
||
#
|
||
# O BANCO ESTÁ ACIMA DESTA LINHA. A tela /admin/sistema › Comportamento
|
||
# ("Exigir assinatura nas entregas do canal") grava a escolha em
|
||
# `platform_settings`, e é ela que vale a partir daí — sem reiniciar o app.
|
||
# Esta variável é o PISO: ela responde quando a instalação nunca abriu a tela e
|
||
# quando o app subiu e ainda não conseguiu ler o banco. Mudou AQUI? reinicie o
|
||
# app. Ver lib/instalacao/comportamento.ts e lib/waha/webhook-auth.ts.
|
||
WAHA_WEBHOOK_REQUIRE_SIGNATURE=false
|
||
# URL pública do app que o WAHA chama em webhooks (precisa ser HTTPS público; use ngrok em dev).
|
||
WAHA_WEBHOOK_BASE_URL=
|
||
# Retomar as sessões já pareadas quando o contêiner do WAHA reinicia. É lido
|
||
# pelo WAHA, não pelo app (por isso não está em lib/env.ts). O default do WAHA é
|
||
# False: o volume guarda a credencial, mas a sessão não sobe, e o número fica
|
||
# "conectado" no disco e mudo na prática até alguém abrir o front e clicar
|
||
# Reconectar. Só mexa aqui se quiser o comportamento antigo de propósito.
|
||
WHATSAPP_RESTART_ALL_SESSIONS=True
|
||
|
||
# --- OpenRouter: atribuição OPCIONAL ----------------------------------------
|
||
# `HTTP-Referer` e `X-Title` que a OpenRouter usa para atribuir o consumo e
|
||
# montar o ranking público do site dela. São OPCIONAIS — a doc deles diz
|
||
# "optional headers to identify your app", e a chamada funciona sem. Vazios,
|
||
# nenhum header é enviado. Preencha com o SEU domínio e o SEU nome se quiser
|
||
# que o consumo desta instalação apareça atribuído a você.
|
||
OPENROUTER_APP_URL=
|
||
OPENROUTER_APP_TITLE=
|
||
|
||
# --- Upstash Redis (rate limit, idempotency) ---------------------------------
|
||
UPSTASH_REDIS_REST_URL=
|
||
UPSTASH_REDIS_REST_TOKEN=
|
||
|
||
# --- AI providers ------------------------------------------------------------
|
||
# Vercel AI Gateway (preferencial — fallback de provedor + observability + ZDR)
|
||
# AI_GATEWAY_API_KEY: chave do gateway. Quando ausente, ai-response-worker
|
||
# pula com skip="ai_gateway_key_missing" (não bloqueia boot do app).
|
||
AI_GATEWAY_API_KEY=
|
||
AI_GATEWAY_BASE_URL=
|
||
VERCEL_AI_GATEWAY_URL=
|
||
# Fallback direto Anthropic (caso AI Gateway indisponível ou desabilitado pra um tenant)
|
||
ANTHROPIC_API_KEY=
|
||
|
||
# OpenRouter (OPCIONAL) — alternativa ao gateway da Vercel, compatível com a API
|
||
# da OpenAI, com catálogo grande de modelos num único faturamento.
|
||
# Ordem de resolução do chat: AI_GATEWAY_API_KEY > OPENROUTER_API_KEY > provider
|
||
# direto (ANTHROPIC_API_KEY / OPENAI_API_KEY). Vazio = nada muda.
|
||
#
|
||
# ALCANCE — leia antes de trocar. Esta chave alcança a classificação de
|
||
# sentimento, o bot de resposta antigo E, desde que a organização esteja em
|
||
# OpenRouter, o AGENTE do CRM: quando não há credencial cadastrada na tela, o
|
||
# agente cai nesta chave como credencial de plataforma. É o caso normal de quem
|
||
# instalou pelo kit escolhendo OpenRouter e nunca abriu a tela de Provedores.
|
||
#
|
||
# O que ela NÃO faz é ESCOLHER o provedor. Quem escolhe é a instalação
|
||
# (`AI_PROVIDER`, gravado em settings.llm.provider) ou a tela Agente de IA →
|
||
# Provedores. Preencher a chave aqui com a organização em Anthropic não migra
|
||
# nada; migrar é trocar o provedor, e aí esta chave passa a ser a que paga.
|
||
#
|
||
# ATENÇÃO ao escolher o modelo do agente na tela: ele opera por FERRAMENTAS
|
||
# (contatos, leads, funil, catálogo, transferência para humano). Modelo sem
|
||
# suporte a ferramentas NÃO dá erro — responde um texto plausível ao cliente e
|
||
# nunca cria o lead nem move o card. Por isso o painel RECUSA salvar um modelo
|
||
# sem ferramentas nos pontos "Responder o cliente" e "Trabalhar o funil", usando
|
||
# o que o próprio fabricante declara no catálogo.
|
||
# O invariante que segura estas afirmações: tests/unit/openrouter-alcance.test.ts
|
||
OPENROUTER_API_KEY=
|
||
OPENROUTER_BASE_URL= # vazio = https://openrouter.ai/api/v1
|
||
# Para embeddings do RAG, OPENROUTER_API_KEY acima também serve quando não há
|
||
# chave OpenAI da organização ou gateway configurado. A busca e a indexação usam
|
||
# o mesmo text-embedding-3-small; a chave OpenAI segue necessária para transcrever áudio.
|
||
# OPCIONAL: é o ÚLTIMO degrau OpenAI. Quem preferir pode cadastrar a chave PELA TELA, em
|
||
# IA › Credenciais (ou no próprio passo de cadastrar material) — vale por organização,
|
||
# fica cifrada no banco e dispensa mexer neste arquivo.
|
||
OPENAI_API_KEY=
|
||
# Raciocínio dos modelos da OpenAI (gpt-5.x/gpt-6): none | minimal | low | medium | high | xhigh.
|
||
# Vazio = padrão do modelo. `none` deixa o agente bem mais rápido. Só vale para modelos o*, gpt-5* e gpt-6*;
|
||
# os demais não recebem o campo. Grafia errada impede o worker de subir.
|
||
OPENAI_REASONING_EFFORT=
|
||
|
||
# --- Transcrição de áudio por outro serviço compatível ------------------------
|
||
# Sem nada aqui, a transcrição usa a chave da OpenAI acima — é o de sempre.
|
||
# Com TRANSCRIPTION_API_KEY, o worker de mídia passa a transcrever no serviço
|
||
# que você apontar (Groq, Whisper próprio, qualquer endpoint /audio/transcriptions
|
||
# compatível com a OpenAI). BASE_URL e MODEL são opcionais: sem eles vale o
|
||
# padrão do provedor da chave. Só a transcrição muda; a descrição de imagem
|
||
# continua saindo pelo binding de visão do ponto.
|
||
TRANSCRIPTION_API_KEY=
|
||
TRANSCRIPTION_BASE_URL= # ex.: https://api.groq.com/openai/v1
|
||
TRANSCRIPTION_MODEL= # ex.: whisper-large-v3
|
||
# MODEL e LANGUAGES valem também SEM TRANSCRIPTION_API_KEY, com a chave da OpenAI
|
||
# da organização (o MODEL, só com TRANSCRIPTION_BASE_URL vazio: um modelo de
|
||
# outro serviço não é pedido à OpenAI, e segue o whisper-1). Para áudio em espanhol ou português, `gpt-transcribe` com o
|
||
# idioma declarado alucina bem menos que o `whisper-1` padrão (medido: um áudio
|
||
# sem fala virava "Thanks for watching!"; com o idioma, sai vazio).
|
||
# Idiomas esperados, ISO-639-1 separados por vírgula. Vazio = detecção automática.
|
||
TRANSCRIPTION_LANGUAGES= # ex.: es ou pt,es
|
||
|
||
# --- Jev (TypeSafe AI) --------------------------------------------------------
|
||
# A chave do Jev é cadastrada pela tela, por empresa. Esta linha só muda o
|
||
# ENDEREÇO da API, e só os testes de ponta a ponta precisam disso.
|
||
JEV_API_BASE_URL= # vazio = https://api.typesafe.ai
|
||
|
||
# --- Destinos internos autorizados pelo dono da instalação --------------------
|
||
# A saída para a rede de dentro é recusada por padrão: localhost, 127., 10.,
|
||
# 192.168., 169.254. e 172.16/12 não são alcançáveis, nem pelo texto da URL nem
|
||
# pelo IP que o nome resolve. Quem administra a instalação libera endereços
|
||
# internos na tela Administração › Destinos internos — e ESSA tela é a fonte.
|
||
# Esta chave é só o PISO: vale enquanto a tela nunca foi usada.
|
||
# Vale só para destinos que a INSTALAÇÃO configura (ex.: TRANSCRIPTION_BASE_URL);
|
||
# o endereço que uma empresa escolhe no painel dela continua sem poder apontar
|
||
# para dentro. Vírgula separa; cada entrada é um IPv4 ou uma faixa CIDR IPv4:
|
||
# IA_DESTINOS_INTERNOS_PERMITIDOS=10.1.2.7,10.1.0.0/16
|
||
# Nome não entra: o que se compara é o IP que o nome resolve. Entrada fora do
|
||
# formato é ignorada — e ignorar é recusar. Vazio = nada passa.
|
||
IA_DESTINOS_INTERNOS_PERMITIDOS=
|
||
|
||
# --- Teto de gasto de IA: a alavanca de emergência ----------------------------
|
||
# Quem escolhe o teto é cada organização, na tela Uso de IA › Orçamento (teto,
|
||
# aviso, e se a IA deve PARAR ao chegar nele). Esta chave não escolhe nada: ela
|
||
# só sabe AFROUXAR o que já foi escolhido.
|
||
# on (default) respeite o que cada organização escolheu
|
||
# avisar rebaixa qualquer "parar a IA" para apenas avisar
|
||
# off desliga a proteção nesta instalação inteira
|
||
# Também valem, como off: false, 0, no, nao, não, disabled. Valor não
|
||
# reconhecido cai em `on` — de propósito: a alavanca de emergência NUNCA pode
|
||
# derrubar o app por um typo. Mudou AQUI? reinicie o app e o worker (a tela não
|
||
# pede reinício).
|
||
#
|
||
# O BANCO ESTÁ ACIMA DESTA LINHA. A tela /admin/sistema › Comportamento
|
||
# ("Proteção de gasto de IA") grava a escolha em `platform_settings`, e é ela
|
||
# que vale a partir daí. Esta variável é o PISO: ela responde quando a
|
||
# instalação nunca abriu a tela e quando o app subiu e ainda não conseguiu ler o
|
||
# banco — a janela em que uma instalação desprotegida de propósito voltaria a
|
||
# bloquear sozinha. Ver lib/instalacao/comportamento.ts.
|
||
AI_BUDGET_ENFORCEMENT=on
|
||
|
||
# ── Comportamento da instalação que o MOTOR lê (o banco está acima) ──────────
|
||
# As duas chaves abaixo têm casa na mesma tela — /admin/sistema › Comportamento
|
||
# — e o `.env` é o PISO delas, no mesmo molde do SIGNUP_MODE: a escolha salva na
|
||
# tela vive em `platform_settings` e vence sem reiniciar nada; o que está aqui
|
||
# responde quando a instalação nunca abriu a tela e quando o processo subiu e
|
||
# ainda não conseguiu ler o banco. Vazio = o padrão do produto, que é o de
|
||
# sempre (`inject` e `true`).
|
||
#
|
||
# `inject` (padrão) acrescenta o texto de divulgação de pagamento à primeira
|
||
# mensagem; `veto` bloqueia o envio sem ele e devolve ao modelo a razão, para
|
||
# ele reescrever. Vazio ou valor não reconhecido = `inject`.
|
||
DISCLOSURE_MODE=
|
||
# "true" (padrão) faz CADA envio passar por uma conferência de modelo antes de
|
||
# sair, para não prometer o que a empresa não cumpre — custa uma chamada de
|
||
# modelo por envio. Vazio = `true`; `false` desliga.
|
||
PROMISE_SEMANTIC_ENABLED=
|
||
|
||
# --- Workers -----------------------------------------------------------------
|
||
# Ritmo com que o WORKER roda os handlers do event_log (mídia, branding,
|
||
# follow-up…). Antes destes knobs os handlers só rodavam pelo cron
|
||
# `event-log-drain`, 1×/min — e a cadeia persist→derive de um áudio levava
|
||
# 103-188s contra os 45s que o turno espera pela transcrição. O cron continua
|
||
# ligado como rede de segurança. Vazio = os defaults abaixo.
|
||
EVENT_LOG_DRAIN_INTERVAL_MS=2000
|
||
EVENT_LOG_DRAIN_IDLE_INTERVAL_MS=10000
|
||
EVENT_LOG_DRAIN_BATCH_SIZE=50
|
||
|
||
# --- Elegibilidade da IA por origem do lead ----------------------------------
|
||
# Vale SÓ nos canais em que você ligou o modo "só atende quem eu autorizei"
|
||
# (Configurações do canal › ai_gate = allowlist). Nesses canais, um contato fica
|
||
# elegível para a IA quando vem de uma origem conhecida (formulário do Respondi,
|
||
# campanha, liberação manual). Este número é por quantos DIAS essa autorização
|
||
# vale — depois disso, uma submissão antiga não reativa a IA sozinha. Uma
|
||
# conversa que segue viva renova o prazo a cada turno. Vazio = 21 dias.
|
||
AI_ALLOWLIST_TTL_DAYS=21
|
||
# Stub do endpoint de teste do agente: 'true' devolve trace fabricado em vez de
|
||
# executar o agente. Deixe false — o runtime real já existe. Só ligue para
|
||
# exercitar o render da UI sem gastar token.
|
||
INTERNAL_AGENT_RUN_STUB=false
|
||
|
||
# --- Sentry ------------------------------------------------------------------
|
||
SENTRY_DSN=
|
||
|
||
# --- E-mail transacional (Resend) ---------------------------------------------
|
||
# Convite de time, entrega de export LGPD e alarme de SLA saem por aqui.
|
||
#
|
||
# RESEND_FROM_EMAIL precisa ser um endereço de um domínio VERIFICADO na SUA
|
||
# conta Resend. Vazio = e-mail desligado (o convite mostra o link de aceite na
|
||
# tela e o export de LGPD fica em `pending_review`), e isso é de propósito:
|
||
# herdar um domínio que não é seu faz todo envio falhar lá na Resend, com uma
|
||
# mensagem que não diz que o problema é configuração.
|
||
RESEND_API_KEY=
|
||
RESEND_FROM_EMAIL=
|
||
|
||
# --- E-mail transacional (SMTP) -----------------------------------------------
|
||
# O SEGUNDO caminho de e-mail, ao lado da Resend acima — não no lugar dela.
|
||
# Preenchido, o envio sai pelo SEU servidor; vazio, continua pela Resend. Quem
|
||
# já roda com Resend não precisa tocar em nada aqui.
|
||
#
|
||
# A tela /admin/email grava os mesmos campos no banco, e o BANCO PREVALECE
|
||
# sobre estas variáveis: elas servem para provisionar uma VPS sem abrir a
|
||
# interface, e ficam como piso de rollback.
|
||
#
|
||
# Use somente o hostname, sem smtp:// e sem :porta.
|
||
# 465 = TLS implícito (SMTP_SECURITY=tls); 587 = STARTTLS.
|
||
# Host ou remetente vazio mantém o convite como link copiável na tela e o
|
||
# export de LGPD em revisão pendente, em vez de tentar entregar um e-mail
|
||
# incompleto.
|
||
SMTP_HOST=
|
||
SMTP_PORT=587
|
||
SMTP_SECURITY=starttls
|
||
SMTP_USERNAME=
|
||
SMTP_PASSWORD=
|
||
SMTP_FROM_EMAIL=
|
||
SMTP_FROM_NAME=
|
||
|
||
# --- Impersonate / suporte ----------------------------------------------------
|
||
# Segredo HMAC do cookie de impersonate. Mínimo 32 chars quando a feature é usada.
|
||
IMPERSONATE_COOKIE_SECRET=
|
||
# Endereço de suporte mostrado ao CLIENTE FINAL (conta suspensa, cobrança).
|
||
# Quem instala para terceiros põe o próprio endereço aqui. Vazio = a tela não
|
||
# mostra endereço nenhum, e nunca cai num endereço do produto.
|
||
SUPPORT_EMAIL=
|
||
|
||
# --- LGPD ---------------------------------------------------------------------
|
||
# Chave usada para assinar exports de dados LGPD.
|
||
LGPD_SIGNING_KEY=
|
||
# E-mail do Encarregado (DPO). Impresso no rodapé do relatório entregue ao
|
||
# titular (`lib/lgpd/pdf-renderer.tsx`) quando a organização não tem
|
||
# `dpo_email` própria, e é o destinatário do alarme de SLA.
|
||
LGPD_DPO_EMAIL=
|
||
# Prazo, em horas, antes de um export LGPD expirar.
|
||
LGPD_EXPORT_EXPIRES_HOURS=72
|
||
|
||
# --- Nuvemshop OAuth ---------------------------------------------------------
|
||
# Obtenha em https://partners.tiendanube.com/ (criar app → copiar credenciais).
|
||
# Deixe vazio em dev/staging se ainda não tiver app aprovado — a integração
|
||
# degrada graciosamente e a UI mostra "Integração não configurada".
|
||
# Liga a integração Nuvemshop. Quando true, exige as credenciais abaixo.
|
||
NUVEMSHOP_ENABLED=false
|
||
# APP_ID aparece na URL do painel do app no portal de parceiros.
|
||
NUVEMSHOP_APP_ID=
|
||
NUVEMSHOP_CLIENT_ID=
|
||
NUVEMSHOP_CLIENT_SECRET=
|
||
# Para testar webhooks em dev exponha o app via ngrok/cloudflared (HTTPS) e
|
||
# aponte NEXT_PUBLIC_APP_URL para o túnel antes de clicar "Conectar".
|
||
|
||
# --- App URLs ----------------------------------------------------------------
|
||
# URL canônica do app (tenant). Ex: https://app.deskcomm.com
|
||
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
||
# URL canônica do super-admin. Ex: https://admin.deskcomm.com
|
||
NEXT_PUBLIC_ADMIN_URL=http://localhost:3000
|
||
|
||
# =============================================================================
|
||
# AGENT ENGINE (fusão Vendaval) — worker 24/7 do agente SDR
|
||
# =============================================================================
|
||
# Connection string do Postgres do Supabase (Settings → Database). O worker usa
|
||
# Postgres direto (fila FOR UPDATE SKIP LOCKED, locks, FTS). Recomendado: role
|
||
# dedicada com login (ex.: agent_worker) — nunca a service_role key aqui.
|
||
SUPABASE_DB_URL=
|
||
|
||
# A conexão que MEXE NO SCHEMA — e SÓ o kit (install.sh/update.sh/backup.sh) a
|
||
# usa; nenhum código do app lê esta chave. OPCIONAL: vazia, tudo roda pela de
|
||
# cima, que é o caso da nuvem (a string do pooler já vem privilegiada).
|
||
# Preencha quando o banco for um Supabase PRÓPRIO e a de cima for a role menor:
|
||
# `create extension`, o `baseline.sql` e a promoção do dono exigem o dono do
|
||
# banco. Ver docs/deploy-selfhost/README.md §2.
|
||
SUPABASE_DB_ADMIN_URL=
|
||
|
||
# --- Ritmo da fila do worker (custo de banco) --------------------------------
|
||
# Os dois governam quanto o worker conversa com o Postgres quando NÃO há trabalho
|
||
# — a conta que estourou a cota de egress do plano free do Supabase na issue #258.
|
||
# Ambos são opcionais: sem eles valem os defaults abaixo, que são o comportamento
|
||
# recomendado. Como ver o que está em vigor: a linha `agent-engine pronto` no log
|
||
# do worker imprime os dois. Quando mexer e o que medir:
|
||
# docs/runbooks/custo-e-cota-do-supabase.md
|
||
#
|
||
# TETO de espera do laço da fila. Com a fila vazia, é quanto o worker dorme entre
|
||
# uma consulta e a próxima; com job agendado, ele acorda no vencimento e este
|
||
# valor só limita a soneca. Não passe de 10000: acima disso a conexão ociosa
|
||
# expira entre as rodadas e cada consulta volta a pagar TCP+TLS — gasta MAIS.
|
||
#QUEUE_POLL_INTERVAL_MS=2000
|
||
# Ritmo do "havia trabalho e eu não peguei" (todas as vagas de
|
||
# QUEUE_MAX_CONCURRENCY ocupadas, ou outro turno rodando para o mesmo contato).
|
||
# Aqui há job vencido esperando vaga, então este valor é curto de propósito:
|
||
# aumentá-lo não economiza nada no ocioso e custa throughput no pico.
|
||
#QUEUE_CLAIM_RETRY_INTERVAL_MS=250
|
||
# Resposta obsoleta: se o cliente escreve de novo enquanto o agente ainda está
|
||
# compondo a resposta, a resposta desatualizada não sai e o turno seguinte (que
|
||
# lê a conversa inteira) responde a tudo de uma vez. Vale enquanto a mensagem
|
||
# mais antiga sem resposta tiver menos que este tempo; passado ele, a resposta
|
||
# sai mesmo assim (cliente que escreve sem parar não fica sem resposta). 0 desliga.
|
||
#RESPOSTA_OBSOLETA_TETO_MS=120000
|
||
|
||
# ── Retenção do histórico (poda diária, cron `data-retention`) ───────────────
|
||
# Espaço em disco é uma cota diferente da de tráfego: o plano free do Supabase
|
||
# limita 500 MB de BANCO, e `job_queue` + `api_audit_log` são as duas que crescem
|
||
# sozinhas. Os dois valores abaixo JÁ SÃO os defaults do código: apagar as linhas
|
||
# de um `.env` existente não muda nada, e quem atualiza sem tocar no arquivo
|
||
# recebe exatamente este comportamento. Quanto cada tabela ocupa hoje e o que
|
||
# fazer com o número: docs/runbooks/custo-e-cota-do-supabase.md
|
||
#
|
||
# Idade a partir da qual um job TERMINAL (done/failed/dead) é apagado. Job
|
||
# `pending`/`running` NUNCA é tocado — tem trabalho dentro. Apagar um job leva
|
||
# junto o `send_ledger` e o `before_send_traces` daquele run. Piso de 7 dias,
|
||
# aplicado dentro da função do banco (valor menor é elevado, com aviso no log).
|
||
JOB_QUEUE_RETENTION_DAYS=90
|
||
# Idade a partir da qual uma linha de auditoria é expurgada. O default é a
|
||
# retenção de 5 anos da regra L-10. Piso de 90 dias: nem com a chave de serviço
|
||
# esta poda alcança rastro recente. Diminuir aqui é a alavanca de quem está
|
||
# apertado de espaço e aceita guardar menos histórico.
|
||
AUDIT_LOG_RETENTION_DAYS=1825
|
||
# Idade a partir da qual a conversa da equipe com a IA sobre um caso é apagada.
|
||
# Um ano fiscal: depois disso, "por que decidimos assim" é respondido pelos
|
||
# eventos do caso, não pela deliberação. Piso de 90 dias, aplicado dentro da
|
||
# função do banco (valor menor é elevado, com aviso no log).
|
||
CASE_CHAT_RETENTION_DAYS=365
|
||
# Idade a partir da qual o registro de uma passagem do atendimento para uma
|
||
# pessoa é apagado. Cinco anos, o mesmo da auditoria: é o rastro de quem assumiu
|
||
# a conversa de quem, quando e por quê. Piso de 90 dias, aplicado dentro da
|
||
# função do banco — e passagem que NINGUÉM reconheceu nunca é apagada, em
|
||
# nenhuma idade: ela é uma pessoa ainda esperando resposta.
|
||
PASSAGEM_RETENTION_DAYS=1825
|
||
# Idade a partir da qual o registro de entrega do aviso de caso no WhatsApp da
|
||
# equipe é apagado. Seis meses: a única pergunta que essa linha responde — "o
|
||
# aviso daquele caso saiu?" — é de semanas. A linha não guarda o texto do aviso,
|
||
# só um resumo criptográfico dele. Piso de 30 dias, aplicado dentro da função do
|
||
# banco (valor menor é elevado, com aviso no log).
|
||
CASE_ALERT_RETENTION_DAYS=180
|
||
# Idade a partir da qual o candidato da prospecção nativa é expurgado. Um ano,
|
||
# alinhado ao horizonte da conversa do caso e da captação — decisão do dono
|
||
# (issue #1313). Piso de 90 dias, aplicado dentro da função do banco. O
|
||
# relógio conta da criação para quem nunca foi contatado e da última tentativa
|
||
# para quem já recebeu mensagem; `queued`/`sending` nunca entram, e os tokens
|
||
# de supressão de quem pediu exclusão são preservados para sempre.
|
||
PROSPECCAO_RETENTION_DAYS=365
|
||
# Idade a partir da qual a observação do Jev (o rótulo dele ao lado do da sua IA
|
||
# de sempre, sem texto de cliente) é apagada. Três meses: ela só responde "posso
|
||
# deixar o Jev decidir esta tarefa?". Piso de 30 dias — a janela da concordância
|
||
# no cartão —, aplicado dentro da função do banco.
|
||
JEV_OBSERVACOES_RETENTION_DAYS=90
|
||
# Idade a partir da qual um RASCUNHO sugerido por integração já VENCIDO é
|
||
# apagado (conversation_drafts). O relógio conta do `expires_at`, não da
|
||
# criação: depois do vencimento o link não abre e o consumo é recusado, e o
|
||
# texto é proposta que ninguém enviou. Trinta dias é o prazo de apurar "o link
|
||
# chegou?"; depois disso a trilha de auditoria responde sem guardar o texto.
|
||
# Piso de 7 dias (valor menor é elevado, com aviso no log).
|
||
DRAFT_RETENTION_DAYS=30
|
||
# Idade a partir da qual um candidato ao golden set (near-miss de skill e
|
||
# divergência classificador×modelo — rótulo, sem texto de cliente) é apagado.
|
||
# Três meses: a pergunta que a linha responde é de curadoria recente. Piso de 30
|
||
# dias, aplicado dentro da função do banco.
|
||
GOLDEN_CANDIDATES_RETENTION_DAYS=90
|
||
|
||
# Chave LLM de plataforma (fallback quando a org não tem credencial BYOK
|
||
# cadastrada em /app/ai/credentials). Opcional com BYOK.
|
||
ANTHROPIC_API_KEY=
|
||
|
||
# Dono ÚNICO dos eventos de despacho do agente. 'engine' (default) = o worker
|
||
# consome; 'native' = o dispatcher interno do app consome (deploy sem worker).
|
||
# NUNCA os dois — dois consumidores = turno duplicado ou perdido.
|
||
AGENT_DISPATCH_CONSUMER=engine
|
||
|
||
# Watchdog de sessão: reconcilia channel_sessions × WAHA e reenvia mensagens
|
||
# presas em queued. Usa WAHA_API_BASE_URL/WAHA_API_KEY acima (no compose o
|
||
# worker já recebe http://waha:3000). Knobs opcionais:
|
||
#WATCHDOG_INTERVAL_MS=60000
|
||
#WATCHDOG_REDRIVE_BATCH_SIZE=10
|
||
|
||
# Flywheel agendado (judge → proposta de melhoria; promoção SEMPRE manual na
|
||
# tela de Agentes IA). 0 = desligado; default 6h.
|
||
#FLYWHEEL_INTERVAL_MS=21600000
|
||
#FLYWHEEL_BATCH_LIMIT=10
|
||
|
||
# ── Marca da instalação (white-label) ───────────────────────────────────────
|
||
# Quem instala o CRM para clientes (agência, revendedor) troca a marca aqui, sem
|
||
# editar código — patch em código se perde no próximo update. Vazio = padrão.
|
||
# APP_LOGO_URL aceita qualquer URL pública de imagem; sem ela, o nome aparece
|
||
# como texto. Ver lib/branding.ts.
|
||
APP_NAME=
|
||
APP_LOGO_URL=
|
||
# Cor da marca, em hex (ex.: #506d48). Dela sai a rampa de 11 tons que pinta o
|
||
# produto inteiro — botão, anel de foco, chip, hover — nos dois temas. Vazio = a
|
||
# cor do produto. Cor sem contraste suficiente é DESLOCADA em vez de recusada, e
|
||
# marca acromática (cinza/preto/branco) não vira accent: em ambos os casos o
|
||
# motivo fica registrado. Ver lib/branding/contraste.ts.
|
||
APP_ACCENT_HEX=
|
||
|
||
# ── Quem pode criar conta nesta instalação ──────────────────────────────────
|
||
# `aberto` (padrão, e o comportamento de sempre), `com_aprovacao` (a conta é
|
||
# criada, mas a empresa só nasce quando quem administra a instalação aprova) ou
|
||
# `so_convite`. Vazio = `aberto`: nada muda para quem não mexer aqui.
|
||
#
|
||
# O BANCO ESTÁ ACIMA DESTA LINHA. Assim que alguém usa a tela em
|
||
# /admin/cadastro, a escolha passa a viver em `platform_settings` e é ela que
|
||
# vale. Esta variável é o PISO: ela responde quando a instalação nunca abriu a
|
||
# tela, e quando o app subiu e ainda não conseguiu ler o banco — que é a razão
|
||
# de ela existir, porque sem um piso declarado uma instalação fechada de
|
||
# propósito abriria nessa janela.
|
||
#
|
||
# Fechado, /signup recusa com tela (marca e idioma da instalação) quem chega
|
||
# sem convite; quem chega COM convite válido entra igual. Ver
|
||
# lib/auth/politica-de-cadastro.ts.
|
||
SIGNUP_MODE=
|
||
|
||
# ── Idioma com que a instalação NASCE ───────────────────────────────────────
|
||
# `pt-BR` (padrão) ou `es`. Quem pergunta é o install.sh; quem grava é ele
|
||
# mesmo (ou o scripts/bootstrap-owner.ts, no caminho local), em
|
||
# `organizations.locale` E na preferência do usuário dono.
|
||
#
|
||
# ⚠️ NÃO é lido pelo app em runtime, e por isso não está em `lib/env.ts`: mudar
|
||
# esta linha depois da instalação não muda a interface de ninguém. Trocar o
|
||
# idioma de quem já está instalado é pela TELA — no seletor do topo (só para
|
||
# você) ou em Configurações › Organização (para quem entrar sem preferência).
|
||
APP_LOCALE=pt-BR
|
||
|
||
# --- WhatsApp Cloud API (Meta) — Fase 3a/3b do seam de canais ---
|
||
# Opcionais: sem elas o canal oficial simplesmente não é configurado, e o WAHA
|
||
# segue funcionando. Nada aqui adiciona container ao compose — o servidor é a
|
||
# nuvem da Meta.
|
||
META_APP_ID=
|
||
# App Secret: chave do HMAC SHA-256 que valida os webhooks (X-Hub-Signature-256).
|
||
# RESERVA, desde a migration 0257: o caminho normal é a tela Admin › API Oficial
|
||
# (Meta), que guarda a chave cifrada no banco e gera o verify token. O banco
|
||
# vence o .env; o par daqui só vale enquanto a tela não tiver um par completo.
|
||
META_APP_SECRET=
|
||
META_WABA_ID=
|
||
META_PHONE_NUMBER_ID=
|
||
META_SYSTEM_USER_TOKEN=
|
||
# Token que a Meta devolve no handshake (hub.verify_token) para provar que o
|
||
# endpoint é seu. RESERVA, como o App Secret acima: pela tela Admin › API Oficial
|
||
# (Meta) o servidor o gera sozinho. Só preencha aqui se não for usar a tela — e
|
||
# nunca com um valor adivinhável. Sem token nenhum, o GET de verificação responde 403.
|
||
META_WEBHOOK_VERIFY_TOKEN=
|
||
# URL pública opcional SÓ para o callback da Meta (/api/v1/webhooks/meta/*): separa
|
||
# esse endereço do painel em NEXT_PUBLIC_APP_URL (ex.: painel sob VPN/rede interna).
|
||
# A Zernio e o canal parceiro continuam usando NEXT_PUBLIC_APP_URL. Vazia = como antes.
|
||
META_WEBHOOK_BASE_URL=
|
||
# Versão da Graph API. Explícita de propósito: bump é decisão, não deriva.
|
||
META_GRAPH_VERSION=v22.0
|
||
# Base (host) da Graph API do CANAL OFICIAL. Vazia = o host real da Meta
|
||
# (https://graph.facebook.com). Só para apontar a instalação para outro lugar:
|
||
# um receptor local para a prova em tela, ou um homólogo do fornecedor.
|
||
#
|
||
# Precisa ser uma base ABSOLUTA http:// ou https://; qualquer outra coisa (caminho
|
||
# relativo, ftp://, file://) é recusada e a instalação volta a falar com o host
|
||
# real, com aviso no log. Barra no fim é aparada.
|
||
#
|
||
# Não é campo de tela nem valor por organização: destino de chamada é decisão da
|
||
# INSTALAÇÃO, e deixá-lo por organização mandaria o token de um tenant por um host
|
||
# que o outro escolheu.
|
||
META_GRAPH_BASE_URL=
|
||
# Mesma coisa para o EIXO DE ANÚNCIO (conversões e métricas da conta de anúncios),
|
||
# que é credencial e ciclo de vida diferentes do canal — por isso a variável é
|
||
# própria. Apontar o canal para o receptor de prova não pode levar junto o
|
||
# relatório de venda.
|
||
META_ADS_GRAPH_BASE_URL=
|
||
|
||
# --- Canal via intermediário (BSP) — opcional ---
|
||
# Deixe em branco se você não usa este canal; nada quebra sem estas três.
|
||
#
|
||
# As DUAS de baixo andam juntas: o adapter só se considera configurado com as
|
||
# duas preenchidas. Preencher só uma não é meio-caminho — é o estado que fazia a
|
||
# mensagem ser marcada como enviada sem sair, e por isso a checagem agora exige
|
||
# o par. A credencial também pode viver na SESSÃO (cifrada), e aí estas ficam
|
||
# vazias.
|
||
ZERNIO_ACCOUNT_ID=
|
||
ZERNIO_API_KEY=
|
||
# Só para apontar para homologação. Vazio usa a produção do provedor.
|
||
ZERNIO_API_BASE_URL=
|
||
|
||
# --- Canal Datafy (WhatsApp oficial por parceiro homologado) — DESLIGADO POR PADRÃO ---
|
||
# Canal OPCIONAL da instalação. Vazio = ele não existe aqui: nenhuma aba em
|
||
# Conexões, a rota de conexão responde 404 e o webhook recusa entregas. Para
|
||
# ligar, ponha `true` e reinicie o app; cada empresa conecta o SEU número pela
|
||
# tela (Conexões), colando o token do provedor e o segredo de assinatura do
|
||
# webhook — nada de credencial aqui.
|
||
DATAFY_ENABLED=
|
||
# Só para apontar para homologação. Vazio usa a produção do provedor.
|
||
DATAFY_API_BASE_URL=
|
||
|
||
# --- Teto de login por IP (SÓ para CI de e2e) ---
|
||
# NÃO defina isto em produção nem numa VPS. O default (60 tentativas por IP a
|
||
# cada 5 min) é o valor correto para uso real e já é folgado por causa de NAT.
|
||
#
|
||
# Existe porque no CI todos os testes saem de UM IP (o runner): 28 specs de e2e
|
||
# estouram o teto e o login passa a responder "Muitas tentativas", reprovando
|
||
# specs que não têm nada a ver com autenticação.
|
||
#
|
||
# O teto por CONTA (5 falhas na mesma conta em 5 min) NÃO é configurável de
|
||
# propósito — é ele que barra brute force, inclusive distribuído por vários IPs.
|
||
# Valor inválido ou ausente cai no default: a falha é fechada.
|
||
# AUTH_RATE_LIMIT_LOGIN_IP=1000
|
||
|
||
# Retenção do arquivo do corpo cru dos webhooks (webhook_events_log).
|
||
# 7 dias mantém o banco dentro dos 500 MB do plano gratuito do Supabase; o
|
||
# arquivo era 86% do banco numa instalação real, crescendo ~23 MB/dia.
|
||
WEBHOOK_LOG_BODY_RETENTION_DAYS=7
|
||
# Quando a LINHA some (não só o corpo). Até aqui ela custa ~200 B e ainda
|
||
# responde quantos eventos chegaram, de que tipo e se a assinatura conferia.
|
||
WEBHOOK_LOG_ROW_RETENTION_DAYS=90
|
||
|
||
# --- Web Push (bandeja do SO com a aba fechada) ------------------------------
|
||
# Sem estas chaves, alertas só disparam com o site aberto (outra aba/minimizado).
|
||
# Gerar: npx web-push generate-vapid-keys
|
||
VAPID_PUBLIC_KEY=
|
||
VAPID_PRIVATE_KEY=
|
||
# Retenção do HISTÓRICO de leads captados (webhook_lead_captures) — a aba
|
||
# "Leads recebidos". Bem mais longa que a do arquivo acima porque aqui a linha
|
||
# É o produto: é ela que responde de qual campanha veio o cliente que fechou.
|
||
# ~1 kB por formulário preenchido, então 300/dia por um ano dão ~110 MB.
|
||
# O código que apaga aplica um piso de 30 dias — número menor não desarma a
|
||
# evidência de origem de um contato.
|
||
LEAD_CAPTURE_RETENTION_DAYS=365
|
||
# Somente laboratório local: origem HTTP exata do catálogo em 127.0.0.1.
|
||
# O app também precisa estar em loopback; vazio exige origem HTTPS pública.
|
||
EXTENSIONS_LOCAL_CATALOG_ORIGIN=
|