Pular para o conteúdo

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.dev nem site antigo.

Três hosts, um Worker atendendo dois deles. A pergunta que decide qual usar é quem abre isso:

hostpúblicoo que serve
api.tactflow.iomáquina/talk/*, /webhooks/*, /internal/*, /health
chat.tactflow.iogente/book/*, /ui/*, /lab, /demo/*, /embed.js
tactflow.iogentelanding de marketing (Pages, outro projeto)

⚠️ api atende 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 a chat nem oferece caminho. A allowlist é PREFIXOS_DE_API em site-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áginas client-site-demo*.html.

Arquivo estático também é recusado, e é por isso que wrangler.toml tem run_worker_first = ["/*"]: sem ele o binding de assets serve public/ antes de o Worker rodar, e o roteador nem é consultado.

Até 10/08/2026 este bloco mandava usar api para tudo e dizia que chat estava saindo; era o inverso do certo, e por causa disso o retorno de checkout do Stripe apontava para api/book/.... Console de fornecedor (Google OAuth, Stripe, Vapi) recebe api; qualquer link que uma pessoa vá abrir recebe chat.

Worker: apps/api-worker (Cloudflare) · CI: .github/workflows/deploy-workers.yml · LLM: apps/llm-service desligado (ADR 0005) · DB: Neon (branch production).


⚠️ Este bloco dizia que chat.tactflow.io não estava no ar e que a superfície viva era workers.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).

ItemSituação
Deploy vivo✅ Worker tactflow-api atende api.tactflow.io (máquina) e chat.tactflow.io (gente)
CI auto-deploy do Workerdeploy-workers.yml no push pra main (se CLOUDFLARE_API_TOKEN setado)
PUBLIC_BASE_URL / PUBLIC_UI_URLapi.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 declarativodesativado — 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 branchDATABASE_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 chat.tactflow.io: descomentar [[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.


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"]
URLDestino
https://tactflow.io/Landing marketing (Pages)
https://chat.tactflow.io/book/:slugChat hosted + link compartilhável
https://chat.tactflow.io/book/:slug/widgetIframe embed
https://chat.tactflow.io/embed.jsSnippet para o site do cliente
https://chat.tactflow.io/labBancada interna
https://api.tactflow.io/talk/*Funil do site de marketing
https://api.tactflow.io/webhooks/stripeConfirmação de pagamento
https://api.tactflow.io/webhooks/vapiVoice (Vapi)
https://api.tactflow.io/internal/calendar/google/callbackCallback de OAuth
https://api.tactflow.io/healthSmoke / uptime
https://api.tactflow.io/{"message": "Welcome to Tactflow API"} — se identifica e para aí; não aponta caminho
https://api.tactflow.io/book/:slug404 de propósito — caminho de gente não mora em api

  • Zona DNS tactflow.io na mesma conta CF que tem CLOUDFLARE_ACCOUNT_ID.
  • Limpar/remover qualquer Pages/Worker antigo servindo tactflow.io (site de agência anterior).
  • tactflow.io + www → Pages (marketing); api.tactflow.io e chat.tactflow.io → este Worker (domínio custom criado pela API de conta, não pelo 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.yml skipa 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:

  1. Upload do Worker — permissão de account (Workers Scripts) ✅ costuma passar
  2. 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):

  1. Dashboard → Manage Account → Account API Tokens (não “My Profile → API Tokens” — esse é user token e some se a conta pessoal mudar).
  2. Create Token → Custom token.
  3. Nome: github-actions-voltron (ou similar).
  4. Permissions:
ScopePermissionResource
AccountWorkers ScriptsEdit
AccountWorkers R2 StorageEdit
AccountWorkers QueuesEdit
ZoneWorkers RoutesEdit
ZoneDNSEdit
ZoneZoneRead
  1. TTL / Expiration: deixar em branco (não preencher data). Sem isso o token é long-lived.
  2. Create → copiar o valor uma vez.
  3. GitHub → Settings → Secrets → Actions → atualizar CLOUDFLARE_API_TOKEN (e confirmar CLOUDFLARE_ACCOUNT_ID).
  4. Validar:
Terminal window
CLOUDFLARE_API_TOKEN=... CLOUDFLARE_ACCOUNT_ID=... node scripts/check-cloudflare-deploy-token.mjs
  1. 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)”
Terminal window
cd apps/api-worker
# Required — app não sobe sem estes
wrangler secret put DATABASE_URL # Neon production branch connection string
# LLM_SERVICE_URL: NAO setar. O servico esta desligado (ADR 0005); vazio faz chat e /lab
wrangler secret put CONVERSATION_RUNTIME_API_KEY # compartilhado com o LLM service
# Stripe — depósito/checkout
wrangler 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 inbound
wrangler 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 HS
wrangler secret put TWILIO_ACCOUNT_SID
wrangler secret put TWILIO_AUTH_TOKEN
wrangler 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_ID
wrangler secret put GOOGLE_CLIENT_SECRET
wrangler secret put CALENDAR_TOKEN_ENCRYPTION_KEY # AES-256-GCM base64url 32 bytes
SecretDe onde vemO que quebra se faltar
DATABASE_URLNeon → branch productionApp não sobe (sem DB)
LLM_SERVICE_URLnão setar — serviço desligado (ADR 0005)Chat e /lab recusam com 503 explícito
CONVERSATION_RUNTIME_API_KEYGerado por você, compartilhado c/ LLMWorker↔LLM 401
STRIPE_SECRET_KEYStripe dashboard → API keysCheckout desabilitado
STRIPE_WEBHOOK_SECRETStripe dashboard → webhook endpointPagamentos não confirmam
VAPI_SERVER_URL_SECRETGere um secret aleatório forteWebhook de voz fail-closed rejeita tudo
TWILIO_*Twilio consoleOwner SMS não entrega
GOOGLE_*Google Cloud Console → OAuth credentialsCalendar sync desabilitado
Terminal window
# 1. Migrations contra Neon production
DATABASE_URL="<neon-production-url>" pnpm schema:migrate
# 2. Seed do tenant HS (arlington-hvac) — cria voice endpoint, offerings, demo_site
DATABASE_URL="<neon-production-url>" \
PUBLIC_BASE_URL="https://api.tactflow.io" \
pnpm seed:homeservices
# 3. Validar schema
DATABASE_URL="<neon-production-url>" pnpm schema:validate
  1. Dashboard Stripe → Developers → Webhooks → Add endpoint
  2. URL: https://api.tactflow.io/webhooks/stripe
  3. Evento: checkout.session.completed
  4. Copiar o Signing secret (whsec_...)
  5. wrangler secret put STRIPE_WEBHOOK_SECRET (cole o signing secret)

Google Cloud Console → OAuth client → Authorized redirect URI:

https://api.tactflow.io/internal/calendar/google/callback

Terminal window
# 1. Smoke completo (deve ficar verde)
pnpm smoke:prod
# 2. Health direto
curl -s https://api.tactflow.io/health | jq .
# 3. Config do tenant HS
curl -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.
TesteEsperado
pnpm smoke:prodVerde (stripe ✅, config ✅, embed ✅, redirect ✅)
GET /healthenvironment: production, stripe: true, vapi: true, twilio_sms: true
GET /book/arlington-hvac/configbusiness_name: "Arlington Air Comfort", 2 offerings
Checkout test modeRetorna pra api.tactflow.io, confirma booking
/health após ligação Vapivapi: true (voice ativo)

  • Worker: reverter PUBLIC_BASE_URL pra workers.dev no wrangler.toml + redeploy, OU reverter o commit no git (CI redeploya).
  • Stripe webhook: dashboard → repoint pra URL antiga.
  • DNS: remover [[routes]] ou repoint chat.tactflow.io pra destino anterior.

  • 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_ID nos secrets do repo GitHub
  • Zona DNS tactflow.io na conta CF; site antigo removido
  • Secrets do Worker setados via wrangler secret put (tabela acima)
  • Migrations + seed:homeservices rodados contra Neon production
  • Stripe webhook endpoint api.tactflow.io/webhooks/stripe + signing secret no Worker
  • pnpm smoke:prod verde em https://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