Skip to content

ADR 0056 — Customer Auto-Provisioning Workflow (Temporal)

  • Status: Accepted
  • Date: 2026-05-14
  • Deciders: André Luiz Gallon (Architect/Operator)
  • Supersedes: —
  • Related: ADR 0053 (Cell-based Hyperscale), ADR 0054 (PQC-Everywhere), ADR 0055 (Admin Console + Marketplace)
  • Memory: project_octopus_hyperscale_admin_pqc_locked_2026_05_14.md

Context

Para hyperscale rumo a 100 000+ clientes, NÃO podemos depender de operador humano no caminho do onboarding. Cada signup precisa fluir zero-friction, zero-touch, com SLO < 30 segundos do "cliente paga" ao "cliente tem tokens habilitados no Dashboard".

Pontos críticos:

  1. Idempotência — Stripe webhook pode reentregar 5× em casos de timeout; nunca provisionar 2 deployments para 1 pagamento
  2. Durabilidade — se o orchestrator cair no meio do workflow, retomar exatamente de onde parou, não do início
  3. Visibility — operador admin precisa inspecionar todo workflow em execução (status, step atual, retry count, falhas, tempo gasto)
  4. Compliance — toda decisão automatizada vai para audit chain Merkle (regulamento GDPR/LGPD: cliente tem direito a saber quem/o que decidiu seu provisioning)
  5. Retriabilidade — falha em step N não invalida steps 1..N-1; retry só do step que falhou
  6. Saga compensation — se step N falha após M steps já completados, executar compensating actions para rollback (revogar cert, deletar tenant, refund Stripe)

Workflows imperativos com state machine ad-hoc + cron retry = receita para drift. Temporal.io (originalmente Cadence/Uber) resolve isto com workflows duráveis baseados em event sourcing + deterministic replay.

Decisão

OCTOPUS adota Temporal.io como motor de workflow durável para auto-provisioning e para outros workflows administrativos (renovação cert, churn flow, suspensão por pagamento falho, etc.).

Stack workflow

Componente Tecnologia
Workflow engine Temporal.io Cloud (Plano Pro) OU self-hosted Temporal Cluster
SDK Temporal Go SDK (consistência com resto da stack)
Activities Go funcs idempotentes, retry-policy por activity
Persistence Cassandra (Temporal default) OR Postgres + Elasticsearch para search
UI Temporal Web UI (embedded em admin.tlsstress.art via iframe RBAC-gated)

Workflow canônico: customer.OnboardingV1

                    ┌──────────────────────────────────────────────────┐
                    │   Customer clica "Buy" em tlsstress.art      │
                    └─────────────────────┬────────────────────────────┘
                                          │
                    ┌─────────────────────▼────────────────────────────┐
                    │   Stripe Checkout (cartão / wire / Apple Pay)    │
                    └─────────────────────┬────────────────────────────┘
                                          │
                    ┌─────────────────────▼────────────────────────────┐
                    │   Stripe webhook → Provisioning Orchestrator     │
                    │   (verifica assinatura HMAC; idempotency_key)    │
                    └─────────────────────┬────────────────────────────┘
                                          │
                    ┌─────────────────────▼────────────────────────────┐
                    │   Temporal: StartWorkflowExecution               │
                    │   WorkflowID = "onboarding-<stripe_session_id>"  │
                    │   (idempotência built-in via WorkflowID)         │
                    └─────────────────────┬────────────────────────────┘
                                          │
   ┌─────────────────────────────────────▼─────────────────────────────────────┐
   │  Activity 1: KYC (Sift / Stripe Radar fraud score) — opcional             │
   │  Activity 2: Mint DeploymentID (ULID, collision-checked)                  │
   │  Activity 3: Allocate cell (GeoIP customer billing address → nearest)     │
   │  Activity 4: Issue PQC client cert via Vault PKI (per-cell CA)            │
   │  Activity 5: Provision tenant schema (Postgres RLS) + Redis namespace     │
   │  Activity 6: Allocate token quota (UTXO ledger, initial mint)             │
   │  Activity 7: Configure CONNECT.Art slot reservation                       │
   │  Activity 8: Configure STUN-coord slot reservation                        │
   │  Activity 9: Generate onboarding link (signed JWT, 24h TTL)               │
   │  Activity 10: Send welcome email (Postmark, transactional)                │
   │  Activity 11: Audit chain append (Merkle entry per step)                  │
   │  Activity 12: Notify admin queue (optional: high-value customer flag)     │
   └─────────────────────┬─────────────────────────────────────────────────────┘
                         │
                         ▼
                    ┌──────────────────────────────────────────────────┐
                    │   Customer clica link → Dashboard loads          │
                    │   Tokens já ativos. SLO < 30s end-to-end.        │
                    └──────────────────────────────────────────────────┘

Idempotência

  • Stripe: webhook usa idempotency_key = stripe_session_id. Temporal WorkflowID é derivado deste mesmo ID → reentregas de webhook NÃO disparam workflows novos
  • Activities: cada activity recebe idempotency_key derivado de (WorkflowID, ActivityName, Attempt). Vault PKI rejeita issue duplicado para mesma chave; Postgres tenant create é UPSERT
  • Email: Postmark MessageStream + MessageReference previne duplicate send

Saga compensation

Se Activity N falha permanentemente após retries esgotados:

Activity 1-N-1 (já completadas) → reverse order compensating:
  - Activity 12 (notify admin): no-op (notification não compensável; loga)
  - Activity 11 (audit append): append "rollback" entry (audit chain é append-only)
  - Activity 10 (email): no-op (email enviado é enviado)
  - Activity 9 (onboarding link): revoke JWT em revocation list
  - Activity 8 (STUN slot): release reservation
  - Activity 7 (CONNECT slot): release reservation
  - Activity 6 (token quota): burn allocated tokens (UTXO transition)
  - Activity 5 (tenant schema): mark "rollback_pending" (sweep nightly drops)
  - Activity 4 (PKI cert): revoke cert (CRL update + OCSP responder)
  - Activity 3 (cell allocation): release allocation
  - Activity 2 (DeploymentID): mark "rolled_back" in ID registry
  - Activity 1 (KYC): no-op (KYC score persisted; reuso futuro)

Final action:
  - Stripe: refund full amount (Activity 0)
  - Send "provisioning failed, refund issued" email
  - Page on-call admin via PagerDuty

SLO

Métrica Target Alerta
Workflow p50 latency (start → email sent) < 15s > 30s
Workflow p99 latency < 30s > 60s
Workflow success rate > 99.5% < 99.0% sustained 1h
Saga compensation rate < 0.2% > 0.5% sustained 1h
Email delivery success > 99.5% < 99.0% sustained 1h

Stripe webhook hardening

  • Endpoint: https://provisioning.tlsstress.art/v1/stripe-webhook (separado de admin.tlsstress.art para isolar blast radius de webhook abuse)
  • Assinatura HMAC-SHA256 verificada com Stripe webhook secret rotacionado trimestralmente
  • Rate limit: 100 req/s por IP, burst 200 (Stripe nunca excede isto)
  • Replay protection: Stripe-Signature header com timestamp; rejeita > 5min skew
  • Idempotency: dedup window de 7 dias via Redis + persisted in Postgres webhook_events
  • IP allowlist: somente Stripe IPs publicados (validados via Stripe API hourly)
  • Payload PQC-encrypt re-encryption antes de enfileirar em Temporal (Stripe ainda não PQC; mas após validação HMAC e antes de persistir, criptografamos novamente com AES-256-GCM PQC-safe + ML-KEM-768 envelope)

Outros workflows na mesma plataforma Temporal

  • customer.RenewalV1 — renovação cert anual (cert-manager + Vault)
  • customer.ChurnV1 — cliente cancela (saga reversa de Onboarding)
  • customer.SuspendV1 — pagamento falho 3× (suspende sem deletar dados; restore se pago)
  • customer.QuotaTopupV1 — cliente compra pacote adicional de tokens
  • customer.UpgradeV1 — cliente muda de tier (Pro → Enterprise)
  • system.CellRebalanceV1 — operations rebalance cells quando saturação > 70%
  • system.CertRotationV1 — rotação cert global (Sigstore + Vault)
  • system.AuditChainReconcileV1 — daily Merkle root reconcile

Alternativas consideradas

Alternativa Por que rejeitada
Cron + state machine ad-hoc em Postgres Receita para drift. Visibility ruim. Idempotência manual = fonte constante de bugs.
AWS Step Functions Lock-in AWS. Limitações de payload size + state. Não multi-cloud-portable.
Cadence (Temporal predecessor) Temporal é o fork maintido. Cadence Uber stale.
Apache Airflow Designed for batch / ETL, não para workflows interativos sub-30s.
Custom orchestrator em Go Reimplementar Temporal = 6-12 meses. Não vale.

Consequências

Positivas

  • Onboarding 100% automatizado, zero humano no caminho até Enterprise tier
  • Visibility completa via Temporal UI (admin pode inspecionar workflow em tempo real)
  • Saga compensation built-in resolve cleanup de falhas parciais
  • Reuso da plataforma Temporal para todos workflows admin futuros (renewal, churn, etc.)
  • Hyperscale-ready: Temporal handles 100k+ workflows/s sem problemas

Negativas

  • Custo Temporal Cloud ~$300-1500/mês inicial (depende volume); self-hosted Cluster custa Cassandra + sysadmin
  • Curva de aprendizado: Temporal SDK tem semantics deterministic-replay que pegam dev desavisado
  • Workflow versioning é necessário para evolução (Temporal tem suporte built-in mas requer disciplina)

Neutras

  • Persistence backend (Cassandra default) é hot path — precisa ser HA própria

Wave-1 implementação

PR antecipado:

  • PR-OCTOPUS-6: Provisioning Orchestrator com customer.OnboardingV1 workflow, Stripe webhook receiver, 12 activities cabladas, saga compensation completa, Temporal Web UI embedded.

Estimativa: 5-7 dias com 1 dev fulltime fluente em Temporal Go SDK.