Deploy produção — api + chat (app) + tactflow.io (marketing)
Status: canônico · Escopo: Estado de deploy real e runbook de produção · Atualizado em: 2026-08-10
Gate P0 antes de G0 comercial: prospect não pode receber link
workers.devnem site antigo.Três hosts, um Worker atendendo dois deles. A pergunta que decide qual usar é quem abre isso:
host público o que serve api.tactflow.iomáquina /talk/*,/webhooks/*,/internal/*,/healthchat.tactflow.iogente /book/*,/ui/*,/lab,/demo/*,/embed.jstactflow.iogente landing de marketing (Pages, outro projeto) ⚠️
apiatende uma allowlist e responde 404 em todo o resto, de propósito — não redireciona. A raiz apenas se identifica (Welcome to Tactflow API) — não leva achatnem oferece caminho. A allowlist éPREFIXOS_DE_APIemsite-host.ts; caminho de backend novo precisa ser adicionado lá, senão dá 404. É o lado seguro de errar: a primeira versão listava o oposto — os caminhos a BLOQUEAR — e deixou passar/embed.js,/ui/*e as páginasclient-site-demo*.html.Arquivo estático também é recusado, e é por isso que
wrangler.tomltemrun_worker_first = ["/*"]: sem ele o binding de assets servepublic/antes de o Worker rodar, e o roteador nem é consultado.Até 10/08/2026 este bloco mandava usar
apipara tudo e dizia quechatestava saindo; era o inverso do certo, e por causa disso o retorno de checkout do Stripe apontava paraapi/book/.... Console de fornecedor (Google OAuth, Stripe, Vapi) recebeapi; qualquer link que uma pessoa vá abrir recebechat.Worker:
apps/api-worker(Cloudflare) · CI:.github/workflows/deploy-workers.yml· LLM:apps/llm-servicedesligado (ADR 0005) · DB: Neon (branchproduction).
Estado hoje (2026-07-28)
Seção intitulada “Estado hoje (2026-07-28)”⚠️ Este bloco dizia que
chat.tactflow.ionão estava no ar e que a superfície viva eraworkers.dev. Era falso desde antes de 10/08/2026 — a rota existia, criada à mão no painel, e o arquivo nunca soube. Os dois hosts respondem e os dois ficam: não são canônico e legado, são duas superfícies com públicos diferentes (ver tabela no topo).
| Item | Situação |
|---|---|
| Deploy vivo | ✅ Worker tactflow-api atende api.tactflow.io (máquina) e chat.tactflow.io (gente) |
| CI auto-deploy do Worker | ✅ deploy-workers.yml no push pra main (se CLOUDFLARE_API_TOKEN setado) |
PUBLIC_BASE_URL / PUBLIC_UI_URL | ✅ api.tactflow.io / chat.tactflow.io no wrangler.toml. São origens diferentes com propósitos diferentes — link para humano sai de PUBLIC_UI_URL |
| Custom domain declarativo | ❌ desativado — bloco [[routes]] comentado em wrangler.toml (workers.dev); token CI pode ficar sem Zone Routes até reativar |
Redirect do root / | ✅ em chat → /book/arlington-hvac (fixture = tese DFW HVAC). Em api a raiz só se identifica; todo o resto fora da allowlist dá 404 |
/health (canais) | ✅ reporta stripe/vapi/twilio/whatsapp prontos |
smoke:prod | ✅ default aponta pro workers.dev; use WORKER_URL=… para outro alvo |
| Secrets no Worker (voz) | ✅ VAPI_SERVER_URL_SECRET, DATABASE_URL, CONVERSATION_RUNTIME_API_KEY. LLM_SERVICE_URL saiu: a voz nunca passou por ele (ADR 0005) |
| Aviso do dono (e-mail) | ✅ Resend — sai por ownerAlerts.emailTo, sem depender de A2P. É o canal que entrega hoje |
| Secrets no Worker (SMS) | ❌ TWILIO_ACCOUNT_SID / TWILIO_AUTH_TOKEN / TWILIO_FROM_NUMBER ausentes → lembretes E36 degradam em silêncio (twilio_sms_skipped). Já não bloqueia o aviso do dono, que agora tem e-mail. SMS vira canal adicional quando o A2P sair (#28) |
| Neon production branch | ✅ DATABASE_URL_PRODUCTION setado em 11/08/2026 nos secrets do GitHub. Sem ele o job de partições saía verde sem criar nada |
| Stripe webhook em prod | ⚠️ Endpoint + signing secret (só necessário se depósito entrar em escopo — ver DIRECTION.md §4) |
| Demo seed em prod | ⚠️ seed:homeservices contra Neon production |
Próximo passo para ir de “construído” a “operacional”: preencher ownerAlerts.emailTo com o
e-mail real do dono no tenant. O de demo sai com owner@example.com, que é reservado pela IANA e
não chega em ninguém — o webhook avisa (vapi_config_placeholder_owner_email), como já avisava do
555-01xx no telefone.
Twilio segue pendente, mas o que ele bloqueia agora são os lembretes (E36), não o aviso do dono.
⚠️ Sobre [[routes]] em wrangler.toml: o bloco está comentado de propósito. Declará-lo faz o
wrangler deploy chamar /zones/<zone>/workers/routes, e o token não tem Zone | Workers Routes | Edit — o deploy fica vermelho. Os dois domínios custom existem, criados pela API de conta.
Para declarar no arquivo, primeiro adicione a permissão ao token. Texto antigo abaixo, desatualizado:
Para reativar descomentar chat.tactflow.io:[[routes]] em wrangler.toml, alinhar
PUBLIC_BASE_URL ao mesmo domínio, deployar, e reapontar o webhook da Vapi — o serverUrl que o
assistente devolve tem que bater com o domínio onde a Vapi manda as tool-calls.
Arquitetura alvo
Seção intitulada “Arquitetura alvo”flowchart LR DNS["tactflow.io DNS"] --> W["Worker tactflow-api"] W --> CHAT["chat.tactflow.io — GENTE<br>/book/* /ui/* /lab /embed.js"] W --> API["api.tactflow.io — MAQUINA<br>/talk/* /webhooks/* /internal/* /health"] W --> NEON["Neon Postgres production"] W -.->|desligado| LLM["LLM service (Python)"] API --> STRIPE["Stripe / Vapi webhooks"]| URL | Destino |
|---|---|
https://tactflow.io/ | Landing marketing (Pages) |
https://chat.tactflow.io/book/:slug | Chat hosted + link compartilhável |
https://chat.tactflow.io/book/:slug/widget | Iframe embed |
https://chat.tactflow.io/embed.js | Snippet para o site do cliente |
https://chat.tactflow.io/lab | Bancada interna |
https://api.tactflow.io/talk/* | Funil do site de marketing |
https://api.tactflow.io/webhooks/stripe | Confirmação de pagamento |
https://api.tactflow.io/webhooks/vapi | Voice (Vapi) |
https://api.tactflow.io/internal/calendar/google/callback | Callback de OAuth |
https://api.tactflow.io/health | Smoke / uptime |
https://api.tactflow.io/ | {"message": "Welcome to Tactflow API"} — se identifica e para aí; não aponta caminho |
https://api.tactflow.io/book/:slug | 404 de propósito — caminho de gente não mora em api |
Checklist — ordem de execução
Seção intitulada “Checklist — ordem de execução”Pré-requisitos de infra (uma vez)
Seção intitulada “Pré-requisitos de infra (uma vez)”A. Cloudflare account + DNS
Seção intitulada “A. Cloudflare account + DNS”- Zona DNS
tactflow.iona mesma conta CF que temCLOUDFLARE_ACCOUNT_ID. - Limpar/remover qualquer Pages/Worker antigo servindo
tactflow.io(site de agência anterior). -
tactflow.io+www→ Pages (marketing);api.tactflow.ioechat.tactflow.io→ este Worker (domínio custom criado pela API de conta, não pelo deploy).
B. GitHub repo secrets (para CI auto-deploy)
Seção intitulada “B. GitHub repo secrets (para CI auto-deploy)”-
CLOUDFLARE_API_TOKEN— ver Cloudflare API token abaixo (não basta só Workers Scripts). -
CLOUDFLARE_ACCOUNT_ID— ID da conta CF.
Sem estes,
deploy-workers.ymlskipa o deploy com um warning (silencioso). Confirme nos logs do Actions que o job “Deploy Worker” realmente roda.
Cloudflare API token (CI + custom_domain) — sem TTL
Seção intitulada “Cloudflare API token (CI + custom_domain) — sem TTL”Por que o CI “expira toda hora”: tokens da Cloudflare não expiram por padrão. Só morrem se você preenche o campo TTL / Expiration na criação, ou se revoga/rola o token. O fix definitivo: Account API Token (service principal) com Expiration vazio.
⚠️ Desatualizado: hoje o wrangler.toml não declara [[routes]] — ver o aviso acima. O texto abaixo descreve o que aconteceria se declarasse, e é por isso que ele está comentado. O deploy faria duas chamadas à API:
- Upload do Worker — permissão de account (Workers Scripts) ✅ costuma passar
- Sync de rotas / custom domain — permissão de zone (Workers Routes + DNS) ❌ falha se o token for só de account
Erro típico no Actions (depois de Uploaded tactflow-api):
A request to the Cloudflare API (/zones/.../workers/routes) failed.Authentication error [code: 10000]Criar o token definitivo (não use token de usuário com TTL):
- Dashboard → Manage Account → Account API Tokens (não “My Profile → API Tokens” — esse é user token e some se a conta pessoal mudar).
- Create Token → Custom token.
- Nome:
github-actions-voltron(ou similar). - Permissions:
| Scope | Permission | Resource |
|---|---|---|
| Account | Workers Scripts | Edit |
| Account | Workers R2 Storage | Edit |
| Account | Workers Queues | Edit |
| Zone | Workers Routes | Edit |
| Zone | DNS | Edit |
| Zone | Zone | Read |
- TTL / Expiration: deixar em branco (não preencher data). Sem isso o token é long-lived.
- Create → copiar o valor uma vez.
- GitHub → Settings → Secrets → Actions → atualizar
CLOUDFLARE_API_TOKEN(e confirmarCLOUDFLARE_ACCOUNT_ID). - Validar:
CLOUDFLARE_API_TOKEN=... CLOUDFLARE_ACCOUNT_ID=... node scripts/check-cloudflare-deploy-token.mjs- Actions → Deploy Workers → Run workflow (ou push em
apps/).
Revogue tokens velhos/curtos no dashboard depois que o novo passar no preflight.
É exatamente o que está em vigor: o bloco [[routes]] está removido e os dois domínios (api e chat) foram criados pela API de conta. O CI passa com token só de account; perde-se o custom domain declarativo no repositório, e essa é a dívida registrada na #65.
Secrets do Worker (uma vez, via wrangler secret put)
Seção intitulada “Secrets do Worker (uma vez, via wrangler secret put)”cd apps/api-worker
# Required — app não sobe sem esteswrangler secret put DATABASE_URL # Neon production branch connection string# LLM_SERVICE_URL: NAO setar. O servico esta desligado (ADR 0005); vazio faz chat e /labwrangler secret put CONVERSATION_RUNTIME_API_KEY # compartilhado com o LLM service
# Stripe — depósito/checkoutwrangler secret put STRIPE_SECRET_KEY # sk_live_... (ou sk_test_... pra test mode)wrangler secret put STRIPE_WEBHOOK_SECRET # whsec_... do endpoint (ver seção Stripe)
# Voice (Vapi) — webhook inboundwrangler secret put VAPI_API_KEY # provisioning API (opcional p/ inbound)wrangler secret put VAPI_SERVER_URL_SECRET # fail-closed auth do webhook (use um secret forte)
# Owner SMS (Twilio) — alertas ao dono do HSwrangler secret put TWILIO_ACCOUNT_SIDwrangler secret put TWILIO_AUTH_TOKENwrangler secret put TWILIO_FROM_NUMBER # número Twilio comprado (+1...)
# Google Calendar (opcional — só se o design partner pedir sync de agenda)wrangler secret put GOOGLE_CLIENT_IDwrangler secret put GOOGLE_CLIENT_SECRETwrangler secret put CALENDAR_TOKEN_ENCRYPTION_KEY # AES-256-GCM base64url 32 bytes| Secret | De onde vem | O que quebra se faltar |
|---|---|---|
DATABASE_URL | Neon → branch production | App não sobe (sem DB) |
LLM_SERVICE_URL | não setar — serviço desligado (ADR 0005) | Chat e /lab recusam com 503 explícito |
CONVERSATION_RUNTIME_API_KEY | Gerado por você, compartilhado c/ LLM | Worker↔LLM 401 |
STRIPE_SECRET_KEY | Stripe dashboard → API keys | Checkout desabilitado |
STRIPE_WEBHOOK_SECRET | Stripe dashboard → webhook endpoint | Pagamentos não confirmam |
VAPI_SERVER_URL_SECRET | Gere um secret aleatório forte | Webhook de voz fail-closed rejeita tudo |
TWILIO_* | Twilio console | Owner SMS não entrega |
GOOGLE_* | Google Cloud Console → OAuth credentials | Calendar sync desabilitado |
Banco + seed (produção)
Seção intitulada “Banco + seed (produção)”# 1. Migrations contra Neon productionDATABASE_URL="<neon-production-url>" pnpm schema:migrate
# 2. Seed do tenant HS (arlington-hvac) — cria voice endpoint, offerings, demo_siteDATABASE_URL="<neon-production-url>" \ PUBLIC_BASE_URL="https://api.tactflow.io" \ pnpm seed:homeservices
# 3. Validar schemaDATABASE_URL="<neon-production-url>" pnpm schema:validateStripe webhook (produção)
Seção intitulada “Stripe webhook (produção)”- Dashboard Stripe → Developers → Webhooks → Add endpoint
- URL:
https://api.tactflow.io/webhooks/stripe - Evento:
checkout.session.completed - Copiar o Signing secret (
whsec_...) wrangler secret put STRIPE_WEBHOOK_SECRET(cole o signing secret)
Google Calendar (quando o design partner pedir)
Seção intitulada “Google Calendar (quando o design partner pedir)”Google Cloud Console → OAuth client → Authorized redirect URI:
https://api.tactflow.io/internal/calendar/google/callbackVerificação pós-deploy
Seção intitulada “Verificação pós-deploy”# 1. Smoke completo (deve ficar verde)pnpm smoke:prod
# 2. Health diretocurl -s https://api.tactflow.io/health | jq .
# 3. Config do tenant HScurl -s https://chat.tactflow.io/book/arlington-hvac/config | jq '.business_name, .offerings'
# 4. Fluxo checkout (Stripe test mode)# Abra https://chat.tactflow.io/book/arlington-hvac e complete um agendamento com depósito.# Use cartão de teste 4242 4242 4242 4242.| Teste | Esperado |
|---|---|
pnpm smoke:prod | Verde (stripe ✅, config ✅, embed ✅, redirect ✅) |
GET /health | environment: production, stripe: true, vapi: true, twilio_sms: true |
GET /book/arlington-hvac/config | business_name: "Arlington Air Comfort", 2 offerings |
| Checkout test mode | Retorna pra api.tactflow.io, confirma booking |
/health após ligação Vapi | vapi: true (voice ativo) |
Rollback
Seção intitulada “Rollback”- Worker: reverter
PUBLIC_BASE_URLpraworkers.devnowrangler.toml+ redeploy, OU reverter o commit no git (CI redeploya). - Stripe webhook: dashboard → repoint pra URL antiga.
- DNS: remover
[[routes]]ou repointchat.tactflow.iopra destino anterior.
Definition of done (E34)
Seção intitulada “Definition of done (E34)”- Código: CI auto-deploy,
PUBLIC_BASE_URL=api.tactflow.io, domínios custom pela API de conta, redirect/→ HS,smoke:prod -
CLOUDFLARE_API_TOKEN+CLOUDFLARE_ACCOUNT_IDnos secrets do repo GitHub - Zona DNS
tactflow.iona conta CF; site antigo removido - Secrets do Worker setados via
wrangler secret put(tabela acima) - Migrations +
seed:homeservicesrodados contra Neon production - Stripe webhook endpoint
api.tactflow.io/webhooks/stripe+ signing secret no Worker -
pnpm smoke:prodverde emhttps://api.tactflow.io - Link de demo =
https://chat.tactflow.io/book/arlington-hvac(embed + hosted) — enviado a prospect
Criado: 2026-06-12 · Atualizado: 2026-06-22 (E34 código + automação) · owner: eng · desbloqueia G0