Developers

Referência da superfície de desenvolvimento da Anthares: a CLI a44, os SDKs tipados, a REST API versionada e o ciclo local → branch → produção. O foco é o loop de trabalho diário — editar, migrar, testar, publicar e reverter.

Guia Atualizado em julho de 2026 Leitura: ~22 min CLI 4.7

Introdução

Esta página documenta como se escreve código contra a Anthares. Todas as capacidades da plataforma — Postgres gerenciado, storage, autenticação, filas, bots — são expostas por três superfícies equivalentes: a CLI a44, os SDKs oficiais e a REST API v3. As três falam com o mesmo control plane e obedecem às mesmas regras de permissão; a CLI e os SDKs são clientes gerados a partir do mesmo arquivo OpenAPI publicado em https://api.anthares44.dev/v3/openapi.json. Se um recurso existe no painel e não existe na API, isso é um bug, não um design.

O modelo mental é curto: um projeto contém um ou mais ambientes; cada ambiente tem um banco Postgres, um bucket de storage, um conjunto de segredos e uma URL. O arquivo a44.yaml na raiz do repositório descreve o projeto de forma declarativa e é a única fonte de verdade versionada. Tudo que a CLI faz é reconciliar o estado remoto com esse arquivo mais o diretório migrations/. Não existe estado relevante que viva apenas no painel web.

Os limites numéricos citados são os do plano team; free e enterprise têm tetos diferentes, anotados quando a divergência muda a decisão de arquitetura.

CLI

Binário único, sem runtime. Cobre 100% da API v3 e é o caminho usado em CI.

SDKs

TypeScript, Python e Go. Tipos do banco gerados a partir do schema real.

REST API

Versionada por caminho, paginação por cursor, rate limit anunciado em cabeçalhos.

Convenção de exemplos

Todos os exemplos usam o projeto fictício loja-api, região sa-east-1. Identificadores nos formatos prj_, env_, rev_ e evt_ são opacos: não faça parsing deles, o prefixo pode mudar sem aviso dentro da mesma versão da API.

Visão geral

A superfície de desenvolvimento se divide em quatro planos. O plano local roda na sua máquina: um Postgres 16 em contêiner, um emulador de storage compatível com S3 e um proxy que reproduz o comportamento de autenticação e rate limit do gateway. O plano de controle recebe operações administrativas — criar ambiente, aplicar migração, rodar deploy — e é onde vivem os limites de concorrência. O plano de dados é o caminho quente: conexões Postgres, leitura e escrita em storage, chamadas dos SDKs. O plano de eventos entrega webhooks e alimenta logs e métricas.

A separação importa porque as garantias diferem. O plano de controle é fortemente consistente e serializado por projeto: duas migrações simultâneas no mesmo ambiente não acontecem, a segunda recebe 409 conflict_lock. O plano de dados é otimizado para latência e aceita réplicas de leitura com atraso típico de 20–80 ms, o que significa que ler logo após escrever pode devolver o valor antigo se você optar por réplica. O plano de eventos é at-least-once: seu handler de webhook precisa ser idempotente, sem exceção.

Locala44 dev — Postgres em contêiner, emulador de storage, proxy de auth na porta 8788.
ControleMigrações, deploys, ambientes, segredos. Serializado por projeto; 4 operações mutantes simultâneas por organização.
DadosPostgres via pooler em modo transaction, storage S3-compatível, REST API. Réplica de leitura opcional.
EventosWebhooks assinados, stream de logs, métricas em janelas de 60 s.

O proxy local aplica os mesmos limites de rate limit da nuvem, mas com contadores por processo: matar o a44 dev zera tudo. Em produção o contador é distribuído, com janela deslizante de 60 s, e não zera por reinício de instância.

Paridade local

O emulador de storage não implementa multipart upload acima de 5 partes nem regras de lifecycle. Testes que dependem disso precisam rodar contra um ambiente efêmero real — veja Ambientes.

Primeiros passos

O caminho mínimo entre um diretório vazio e um endpoint respondendo em produção tem cinco comandos. O a44 init cria a44.yaml, migrations/0001_init.sql e um .gitignore com as entradas de credencial. O a44 dev sobe a pilha local e faz watch do diretório de migrações, reaplicando o schema quando um arquivo muda. O a44 deploy empacota, aplica migrações pendentes e troca o tráfego.

A instalação não exige Node nem Python: o binário é estático, com cerca de 34 MB, e verifica atualizações uma vez por dia contra o canal stable. Em CI, fixe a versão — atualização automática dentro do pipeline é uma fonte clássica de build que passou ontem e falha hoje sem nenhuma mudança de código.

  1. Instalar a CLI

    Via script oficial ou gerenciador de pacotes. Confirme com a44 --version; a saída inclui o hash do build e o canal.

  2. Autenticar

    a44 login abre o navegador e grava um token de dispositivo em ~/.a44/credentials.json com permissão 0600.

  3. Inicializar o projeto

    a44 init loja-api --region sa-east-1 escreve o a44.yaml e registra o projeto na organização ativa.

  4. Subir o ambiente local

    a44 dev levanta Postgres, storage e proxy. A primeira execução baixa cerca de 180 MB de imagens.

  5. Publicar

    a44 deploy --env production aplica migrações pendentes e promove o build. Um deploy típico leva de 40 a 90 s.

terminal
curl -fsSL https://get.anthares44.dev/install.sh | sh -s -- --version 4.7.2

a44 --version
# a44 4.7.2 (build 9f13ac2, canal stable)

a44 login
a44 init loja-api --region sa-east-1 --template ts-http
cd loja-api && a44 dev
a44.yaml
version: 3
project: loja-api
region: sa-east-1

runtime:
  type: node
  version: "22"
  entry: src/server.ts
  memory_mb: 512
  timeout_s: 30

database:
  engine: postgres
  version: "16"
  pool_size: 20
  statement_timeout_ms: 8000

storage:
  buckets:
    - name: uploads
      public: false
      max_object_mb: 512

environments:
  production:
    min_instances: 2
    max_instances: 40
  preview:
    ephemeral: true
    ttl_hours: 72
min_instances zero e o smoke test do CI

Ambientes de preview nascem com min_instances: 0, e é o padrão certo para eles: um PR aberto por três dias custa praticamente nada. O efeito colateral aparece no pipeline — o primeiro curl do smoke test acorda a instância e paga o cold start, então um passo com timeout de 1 s falha de forma intermitente e é lido como flakiness do teste. Dê 5 s ao passo de verificação, ou use a44 env create --warm quando o job seguinte for sensível a latência. O dimensionamento em produção e o custo de cada degrau estão em Startups.

CLI

A CLI organiza comandos por substantivo: a44 db, a44 env, a44 storage, a44 keys, a44 logs, a44 deploy. Toda operação mutante aceita --dry-run, que imprime o plano sem executar, e --json, que troca a saída humana por um objeto estável adequado a jq. A saída --json é coberta pela política de versionamento; a saída humana não é, e pode mudar entre patches — nunca faça grep nela em script.

Os códigos de saída são significativos: 0 sucesso, 1 erro genérico, 2 uso incorreto, 3 falha de autenticação ou permissão, 4 conflito de lock no plano de controle, 5 timeout do lado do servidor. Um pipeline que trata 4 com retry exponencial e 3 como falha definitiva já elimina a maior parte do ruído em CI, porque conflito de lock é transitório por natureza e permissão nunca se resolve sozinha.

ComandoFlags relevantesComportamento
a44 dev--port, --seed, --no-watchSobe a pilha local. Com --seed executa migrations/seed.sql após aplicar o schema.
a44 db migrate--to, --dry-run, --lock-timeoutAplica migrações pendentes em ordem. --lock-timeout padrão 5s; ao estourar, aborta sem aplicar nada.
a44 db types--lang, --outGera tipos a partir do schema atual do ambiente. Sem --out, escreve em stdout.
a44 env create--from, --ttl, --branchCria ambiente efêmero clonando schema (não dados) do ambiente de origem.
a44 deploy--env, --strategy, --waitPublica build. Com --wait bloqueia até o health check passar ou 300 s.
a44 rollback--to, --include-dbVolta o release. --include-db exige bloco down em todas as migrações do intervalo.
scripts/check-pending.sh
#!/usr/bin/env bash
set -euo pipefail

pend=$(a44 db status --env production --json | jq '.pending | length')

if [ "$pend" -gt 0 ]; then
  echo "há $pend migração(ões) pendente(s) em produção"
  a44 db migrate --env production --dry-run
  exit 1
fi

echo "schema em dia"
Perfis nomeados

a44 --profile ci lê credenciais de ~/.a44/profiles/ci.json ou das variáveis A44_TOKEN e A44_ORG. Variáveis de ambiente sempre têm precedência sobre arquivo, o que evita que uma credencial pessoal montada por engano num contêiner de build seja usada silenciosamente.

SDKs

Os SDKs oficiais são @anthares44/client (TypeScript), anthares44 (Python, requer 3.10+) e github.com/anthares44/go-sdk. Todos implementam a mesma política de retry: até 3 tentativas para 429, 502, 503 e 504, com backoff exponencial começando em 250 ms e jitter de ±30%. Erros 4xx que não sejam 429 nunca são repetidos — repetir um 422 só multiplica o log.

A diferença prática entre o SDK e um cliente HTTP escrito à mão está em três lugares: o cliente já traz os tipos gerados do seu schema, transforma os cabeçalhos de rate limit em propriedade tipada dentro do erro, e implementa o iterador de cursor para você não escrever um while com bug de terminação. O custo é acoplamento à versão do SDK; a compatibilidade com uma versão maior da API é garantida por 18 meses após o anúncio de depreciação.

src/pedidos.ts
import { Anthares44, RateLimitError } from '@anthares44/client';
import type { Database } from './a44-types';

const a44 = new Anthares44<Database>({
  token: process.env.A44_TOKEN!,
  project: 'loja-api',
  maxRetries: 3,
  timeoutMs: 10_000,
});

export async function pedidosPagos(clienteId: string) {
  try {
    const { rows } = await a44.db
      .from('pedidos')
      .select('id, total_cents, criado_em')
      .eq('cliente_id', clienteId)
      .eq('status', 'pago')
      .order('criado_em', { ascending: false })
      .limit(50);
    return rows;
  } catch (err) {
    if (err instanceof RateLimitError) {
      // retryAfterMs vem do cabeçalho Retry-After
      throw new Error(`rate limit; tente em ${err.retryAfterMs}ms`);
    }
    throw err;
  }
}
app/pedidos.py
import os
from anthares44 import Anthares44, RateLimitError

a44 = Anthares44(
    token=os.environ["A44_TOKEN"],
    project="loja-api",
    max_retries=3,
    timeout_s=10.0,
)

def pedidos_pagos(cliente_id: str) -> list[dict]:
    try:
        res = (
            a44.db.table("pedidos")
            .select("id, total_cents, criado_em")
            .eq("cliente_id", cliente_id)
            .eq("status", "pago")
            .order("criado_em", desc=True)
            .limit(50)
            .execute()
        )
        return res.rows
    except RateLimitError as err:
        raise RuntimeError(f"rate limit; tente em {err.retry_after_ms}ms") from err

O construtor aceita timeoutMs por cliente e por chamada. O timeout por chamada substitui o global e não engloba o tempo dos retries: um cliente com timeoutMs: 10_000 e maxRetries: 3 pode consumir até cerca de 40 s no pior caso. Se você roda dentro de uma função com timeout_s: 30, reduza maxRetries para 1, ou o processo é interrompido antes de conseguir registrar o erro no log.

REST API

A API v3 fica em https://api.anthares44.dev/v3. Recursos de coleção sempre paginam por cursor, nunca por offset: um offset alto força o Postgres a percorrer e descartar linhas, e em tabelas grandes isso degrada de forma não linear. A resposta traz data, has_more e next_cursor; o cursor é uma string opaca derivada do par (chave de ordenação, id) e expira em 15 minutos. Cursor expirado retorna 400 cursor_expired e a única recuperação correta é reiniciar a paginação.

O rate limit é anunciado em todas as respostas por três cabeçalhos: X-RateLimit-Limit, X-RateLimit-Remaining e X-RateLimit-Reset (epoch em segundos). Ao estourar, a resposta é 429 com Retry-After em segundos inteiros. O teto padrão no plano team é 600 requisições por minuto por token no plano de dados e 60 por minuto no plano de controle — deploys e migrações são caros e propositalmente limitados. Rajadas acima do teto não são enfileiradas: são rejeitadas.

HTTPerror.codeQuando ocorreAção recomendada
400cursor_expiredCursor com mais de 15 min.Reiniciar a paginação do começo.
401token_expiredToken de dispositivo vencido.Renovar com a44 login --refresh.
403scope_missingToken sem o escopo exigido.Emitir outro token; escopos não são ampliáveis.
409conflict_lockOutra operação de controle em curso.Retry com backoff; o lock expira em 120 s.
422validation_failedCampo inválido; error.fields lista cada um.Corrigir. Nunca repetir.
429rate_limitedTeto por token estourado.Aguardar Retry-After.
GET /v3/projects/loja-api/orders
curl -sS 'https://api.anthares44.dev/v3/projects/loja-api/orders?limit=100&status=pago' \
  -H "Authorization: Bearer $A44_TOKEN" \
  -H 'A44-Environment: production' -D -

# HTTP/2 200
# x-ratelimit-limit: 600
# x-ratelimit-remaining: 583
# x-ratelimit-reset: 1784500860
# a44-request-id: req_01J8XQ2M4K
resposta paginada
{
  "data": [
    { "id": "ord_01J8X9", "total_cents": 24900, "status": "pago", "criado_em": "2026-07-14T18:22:03Z" },
    { "id": "ord_01J8XA", "total_cents": 7150,  "status": "pago", "criado_em": "2026-07-14T18:21:44Z" }
  ],
  "has_more": true,
  "next_cursor": "eyJrIjoiMjAyNi0wNy0xNCIsImkiOiJvcmRfMDFKOFhBIn0",
  "meta": { "request_id": "req_01J8XQ2M4K", "elapsed_ms": 41 }
}
src/paginate.ts
export async function* todosOsPedidos(a44: Anthares44) {
  let cursor: string | undefined;
  let paginas = 0;

  do {
    const page = await a44.orders.list({ limit: 100, cursor, status: 'pago' });
    yield* page.data;
    cursor = page.next_cursor;
    // trava de segurança: 100 páginas = 10 mil registros
    if (++paginas > 100) throw new Error('paginação não terminou em 100 páginas');
  } while (cursor);
}
Idempotência em POST

Envie Idempotency-Key com um UUID v4 em qualquer POST que crie recurso. A chave é retida por 24 h; uma repetição dentro da janela devolve a resposta original com A44-Idempotent-Replay: true, sem criar nada. Chave reutilizada com corpo diferente retorna 422 idempotency_mismatch.

Autenticação

Existem quatro tipos de credencial, com ciclos de vida distintos. O token de dispositivo é o que a44 login grava; vale 30 dias, é renovável e está preso ao seu usuário e à organização ativa. A service key é emitida por ambiente, não expira por padrão e carrega escopos explícitos — é a credencial de CI e de servidores. A chave publicável (pk_) pode ir para o navegador: só permite operações que passam por políticas de linha do Postgres e nunca lê tabela sem política definida.

Escopos são fixados na emissão e imutáveis depois. Um token com db:read não vira db:write; você emite outro. Isso é deliberado: revogar é barato, ampliar silenciosamente não. Toda emissão e toda revogação entram no audit log com ator, IP e hash truncado da chave — o valor completo aparece uma única vez, na criação, e não é recuperável depois.

ClienteSDK / curl
GatewayValida token e escopo
PolíticasRLS por ambiente
PostgresPooler + réplica
CredencialPrefixoValidadeOnde usar
Token de dispositivodev_30 dias, renovávelMáquina do desenvolvedor.
Service keysk_Sem expiração; rotação sugerida a cada 90 diasCI, backend, jobs agendados.
Chave publicávelpk_Sem expiraçãoNavegador e apps móveis, sempre com RLS ativa.
Token efêmerotmp_60 s a 12 h, definido na emissãoUpload direto e links assinados.
terminal
# service key só de leitura para o job de relatórios
a44 keys create relatorios-noturno \
  --env production \
  --scopes db:read,storage:read \
  --expires-in 90d

# rotação sem downtime: as duas chaves valem durante a sobreposição
a44 keys rotate relatorios-noturno --overlap 24h

a44 keys list --env production --json \
  | jq -r '.[] | "\(.name)\t\(.scopes|join(","))\t\(.last_used_at)"'
Service key no cliente

Uma sk_ ignora políticas de linha. Se ela chegar ao bundle do frontend, todo o banco daquele ambiente fica exposto, inclusive tabelas sem RLS. O a44 deploy roda um scanner de bundle e falha com código 1 ao encontrar o padrão sk_ em qualquer artefato servido publicamente.

Banco de dados

Cada ambiente recebe um Postgres 16 dedicado, com pgcrypto, uuid-ossp, pg_stat_statements e pgvector habilitados por padrão. As conexões passam por um pooler em modo transaction: isso multiplica a capacidade de conexões de aplicação, mas invalida recursos que dependem de sessão — LISTEN/NOTIFY, cursores nomeados fora de transação e SET sem LOCAL. Para esses casos existe a porta 6544, com conexão direta e teto bem mais baixo.

O statement_timeout padrão é 8000 ms no papel de aplicação e 120 s no papel de migração. Uma query que estoura recebe o erro 57014 e é abortada; ela não continua consumindo CPU em background. O parâmetro é ajustável por transação com SET LOCAL statement_timeout, mas o teto rígido do pooler é 300 s e não pode ser ultrapassado por configuração de sessão — jobs analíticos longos pertencem a uma réplica dedicada, não ao caminho quente.

ParâmetroPadrãoTetoDescrição
pool_size20400Conexões por instância no pooler. Acima de 400 o plano team retorna 53300.
statement_timeout_ms8000300000Aborta a query com 57014. Ajustável por transação via SET LOCAL.
idle_in_transaction_timeout30000Derruba transação ociosa que estaria segurando locks.
read_replicafalse3 réplicasRoteia SELECT para réplica; atraso típico de 20 a 80 ms.
max_response_rows5000050000Teto de linhas por resposta da REST API, independente de limit.

Os tipos gerados por a44 db types saem do catálogo real do Postgres, não de anotações no código. Colunas NOT NULL viram propriedades obrigatórias, enums do banco viram uniões literais e colunas jsonb viram unknown até que você declare um tipo em a44.types.yaml. Regenerar após cada migração e versionar o resultado é o que faz o compilador apontar um DROP COLUMN antes do deploy, em vez de o erro aparecer em runtime.

src/a44-types.d.ts (gerado)
// gerado por: a44 db types --lang ts --out src/a44-types.d.ts
// schema hash: 7c1e4a90 — NÃO editar à mão
export type StatusPedido = 'pendente' | 'pago' | 'cancelado' | 'estornado';

export interface Database {
  pedidos: {
    Row: {
      id: string;
      cliente_id: string;
      status: StatusPedido;
      total_cents: number;
      metadata: unknown;          // jsonb sem tipo declarado
      criado_em: string;          // timestamptz ISO-8601
      cancelado_em: string | null;
    };
    Insert: Omit<Database['pedidos']['Row'], 'id' | 'criado_em'> & { id?: string };
  };
}
consulta com índice parcial
-- índice parcial: cobre só as linhas do caminho quente
create index concurrently idx_pedidos_cliente_pago
  on pedidos (cliente_id, criado_em desc)
  where status = 'pago';

explain (analyze, buffers)
select id, total_cents, criado_em
from pedidos
where cliente_id = $1 and status = 'pago'
order by criado_em desc
limit 50;
-- Index Scan using idx_pedidos_cliente_pago (actual time=0.031..0.204 rows=50)

Migrações

Migrações são arquivos SQL numerados em migrations/, no formato NNNN_descricao.sql, com dois blocos delimitados por comentários mágicos: -- +a44 up e -- +a44 down. A CLI calcula um SHA-256 do bloco up na primeira aplicação e o grava na tabela a44_migrations. Se o arquivo mudar depois de aplicado, a execução seguinte falha com checksum_mismatch em vez de aplicar silenciosamente: editar migração já aplicada é sempre erro, e a correção é uma migração nova.

O bloco down é opcional para a CLI, mas obrigatório para a44 rollback --include-db. Nem toda operação é reversível de verdade: um DROP COLUMN tem down sintaticamente válido (ADD COLUMN), mas os dados não voltam. Daí a política de expansão em duas fases — adicionar a coluna nova, escrever nas duas, migrar a leitura, e só em um release posterior remover a antiga. Entre as duas fases o rollback é seguro; dentro de uma só fase, não é.

migrations/0042_pedidos_moeda.sql
-- +a44 up
-- lock_timeout curto: prefere falhar rápido a bloquear escritas em produção
set local lock_timeout = '3s';

alter table pedidos
  add column moeda text not null default 'BRL';

alter table pedidos
  add constraint pedidos_moeda_valida
  check (moeda in ('BRL', 'USD', 'EUR')) not valid;

-- validação em passo separado: dispensa ACCESS EXCLUSIVE na tabela inteira
alter table pedidos validate constraint pedidos_moeda_valida;

-- +a44 down
alter table pedidos drop constraint if exists pedidos_moeda_valida;
alter table pedidos drop column if exists moeda;
terminal
a44 db status --env production
# aplicadas: 41   pendentes: 1
# 0042_pedidos_moeda.sql  (up: 4 statements, down: 2 statements)

a44 db migrate --env production --dry-run
a44 db migrate --env production --lock-timeout 3s

# reverter só a última, sem tocar no código publicado
a44 db migrate --env production --to 0041
OperaçãoLock exigidoBloqueia escrita?Reversível de fato
ADD COLUMN com defaultACCESS EXCLUSIVE breveMilissegundos (PG 11+)Sim
CREATE INDEXSHARESim, durante toda a criaçãoSim
CREATE INDEX CONCURRENTLYNenhum exclusivoNãoSim, mas deixa índice inválido se falhar
ALTER COLUMN TYPEACCESS EXCLUSIVESim, reescreve a tabelaNão, se houver truncamento
DROP COLUMNACCESS EXCLUSIVE breveMilissegundosNão — dados perdidos
CONCURRENTLY não roda em transação

CREATE INDEX CONCURRENTLY exige o marcador -- +a44 no-transaction na primeira linha do arquivo. Sem ele a migração falha com 25001. Nesse modo, se a migração falhar no meio, os statements anteriores permanecem aplicados e a recuperação é manual.

Storage

O storage é compatível com a API S3 e acessível pelo endpoint https://s3.sa-east-1.anthares44.dev, o que permite usar aws-cli, rclone e qualquer SDK S3 já existente. Objetos até 5 MB podem ir em requisição única; acima disso o SDK muda automaticamente para multipart com partes de 8 MB e paralelismo 4. O teto por objeto é o max_object_mb do bucket, com máximo de 5120 MB.

Uploads do navegador não devem passar pelo seu backend. O padrão é gerar uma URL pré-assinada no servidor, com validade curta e condições explícitas de tipo e tamanho, e devolver só essa URL ao cliente. A assinatura embute content-length-range; um arquivo maior que o declarado é rejeitado pelo próprio storage com 413, sem consumir banda do seu backend nem exigir validação sua.

Navegador1. pede URL
Backend2. assina com sk_
Storage3. PUT direto
Webhook4. object.created
src/upload.ts
const { url, fields, expiresAt } = await a44.storage
  .bucket('uploads')
  .createSignedUpload({
    key: `nf/${ano}/${pedidoId}.pdf`,
    expiresInSeconds: 300,          // máximo 43200
    contentType: 'application/pdf',
    maxSizeBytes: 10 * 1024 * 1024, // vira content-length-range na política
    metadata: { pedido_id: pedidoId },
  });

// o navegador faz o PUT direto; o backend nunca vê o arquivo
// 413 se o corpo exceder maxSizeBytes
// 403 se o PUT ocorrer depois de expiresAt
Chaves com prefixo aleatório

Chaves sequenciais (2026/07/000001) concentram escrita em uma partição. Prefixar com 2 caracteres de hash do id distribui a carga e mantém a listagem por prefixo utilizável. Isso só importa acima de aproximadamente 3.500 escritas por segundo por prefixo; abaixo disso, priorize legibilidade.

Webhooks

Webhooks entregam eventos ao seu endpoint via POST com corpo JSON e três cabeçalhos: A44-Signature, A44-Timestamp e A44-Event-Id. A assinatura é um HMAC-SHA256 sobre a string {timestamp}.{corpo bruto}, usando o segredo do endpoint. Verifique sobre o corpo bruto, antes de qualquer parsing — se o seu framework reserializa o JSON, a ordem das chaves muda e a assinatura nunca bate.

A entrega é at-least-once com até 8 tentativas em backoff exponencial: 10 s, 30 s, 2 min, 10 min, 1 h, 4 h, 12 h e 24 h. Qualquer resposta 2xx em até 10 s conta como sucesso; timeout, 5xx e 429 agendam retry; 4xx que não seja 429 marca a entrega como falha permanente, sem novas tentativas. Depois de 8 falhas o endpoint é suspenso e um evento webhook.endpoint_disabled é emitido. Rejeite timestamps com mais de 5 minutos de diferença para bloquear replay.

src/webhook.ts
import { createHmac, timingSafeEqual } from 'node:crypto';

const TOLERANCIA_S = 300;

export function verificar(rawBody: Buffer, headers: Record<string, string>) {
  const ts = Number(headers['a44-timestamp']);
  if (!Number.isFinite(ts)) throw new Error('timestamp ausente');
  if (Math.abs(Date.now() / 1000 - ts) > TOLERANCIA_S) throw new Error('replay');

  const esperado = createHmac('sha256', process.env.A44_WEBHOOK_SECRET!)
    .update(`${ts}.`)
    .update(rawBody)          // corpo BRUTO, nunca JSON.stringify(req.body)
    .digest();

  const recebido = Buffer.from(headers['a44-signature'], 'hex');
  if (recebido.length !== esperado.length) throw new Error('assinatura inválida');
  if (!timingSafeEqual(recebido, esperado)) throw new Error('assinatura inválida');

  return JSON.parse(rawBody.toString('utf8'));
}
app/webhook.py
import hmac, hashlib, json, os, time

TOLERANCIA_S = 300

def verificar(raw_body: bytes, headers: dict) -> dict:
    ts = int(headers["a44-timestamp"])
    if abs(time.time() - ts) > TOLERANCIA_S:
        raise ValueError("replay")

    esperado = hmac.new(
        os.environ["A44_WEBHOOK_SECRET"].encode(),
        f"{ts}.".encode() + raw_body,   # corpo BRUTO
        hashlib.sha256,
    ).digest()

    recebido = bytes.fromhex(headers["a44-signature"])
    if not hmac.compare_digest(recebido, esperado):
        raise ValueError("assinatura inválida")

    return json.loads(raw_body)

Para idempotência, guarde A44-Event-Id em uma tabela com chave única e trate violação de unicidade como sucesso silencioso. Reter esses ids por 7 dias cobre com folga a janela de retry de 24 h e ainda os reprocessamentos manuais disparados por a44 events replay --since 24h.

Túnel local

a44 webhooks tunnel --to http://localhost:3000/hooks encaminha eventos reais para a sua máquina e imprime corpo e assinatura de cada entrega. O segredo do túnel é distinto do de produção, então um handler que valida assinatura funciona sem alteração de código.

Deploy

Um deploy tem quatro fases observáveis: build, migrate, health e switch. O build roda em contêiner isolado, sem rede externa depois da instalação de dependências, o que torna builds reprodutíveis e impede que um postinstall exfiltre segredos. As migrações pendentes são aplicadas antes de a nova versão receber tráfego — consequência prática: o schema precisa ser compatível com a versão antiga durante a janela, porque ela ainda está atendendo.

A troca é blue-green por padrão. A revisão nova sobe em paralelo, recebe health checks a cada 2 s por até 60 s, e só então o roteador move 100% do tráfego. Requisições em voo na revisão antiga têm 30 s de drenagem. Com --strategy canary o tráfego vai a 5%, 25% e 100% em intervalos de 5 minutos, com rollback automático se a taxa de erro 5xx da revisão nova passar de 2% em qualquer estágio.

build~35 s
migratelock por projeto
health2 s × 30
switchdrena 30 s
terminal
a44 deploy --env production --strategy canary --wait
# rev_01J8Y2  build ok (34.8s)  migrate ok (1 aplicada)  canary 5% ...

a44 releases list --env production --limit 5 --json \
  | jq -r '.[] | "\(.id)\t\(.created_at)\t\(.status)\t\(.commit[0:7])"'

# rollback só de código: a imagem anterior já está quente
a44 rollback --env production --to rev_01J8X7

# rollback de código e schema: executa os blocos down na ordem inversa
a44 rollback --env production --to rev_01J8X7 --include-db

O rollback só de código leva de 8 a 15 s porque não há build nem download: a revisão anterior fica retida por 30 dias ou 20 revisões, o que vier primeiro. Já o --include-db é destrutivo por definição e exige confirmação interativa ou --yes. Se qualquer migração no intervalo não tiver bloco down, o comando aborta antes de executar qualquer statement — não existe rollback parcial de schema.

Rollback não desfaz efeito externo

Reverter o release não cancela e-mails enviados, cobranças processadas nem webhooks já entregues aos seus consumidores. Para operações com efeito externo, use chave de idempotência e um registro de intenção no banco em vez de contar com rollback.

Ambientes

Ambientes efêmeros são o mecanismo para revisar mudança de schema sem disputar um staging compartilhado. Ao abrir um pull request, o CI chama a44 env create pr-482 --from production --schema-only: a plataforma provisiona um Postgres novo, aplica todas as migrações do branch, sobe o runtime e devolve a URL https://pr-482.loja-api.a44.app. O provisionamento leva de 25 a 60 s conforme o número de migrações; acima de cerca de 400 migrações passa de 2 minutos e vale ativar --from-snapshot, que restaura um dump do schema em vez de reaplicar o histórico inteiro.

Ambientes efêmeros nunca copiam dados de produção. Com --seed a CLI executa migrations/seed.sql; com --anonymize é possível importar uma amostra de até 10.000 linhas por tabela, com as colunas marcadas @pii substituídas por valores gerados. O TTL padrão é 72 horas e o ambiente é destruído ao expirar ou ao fechar o PR, o que vier primeiro. Cada organização no plano team mantém 15 ambientes efêmeros simultâneos; a 16ª criação retorna 409 quota_exceeded.

Por branch

Um ambiente por PR, URL previsível, destruído no merge. Sem fila por staging.

Schema-only

Estrutura de produção, zero dados reais. Nenhum PII sai do ambiente de origem.

TTL curto

72 h por padrão, ajustável até 14 dias. A expiração destrói banco, bucket e segredos.

terminal
a44 env create pr-482 \
  --from production --schema-only --seed \
  --branch feat/moeda --ttl 72h

a44 env list --json | jq -r '.[] | select(.ephemeral) | "\(.name)\t\(.expires_at)"'

# comparar schema do efêmero com produção antes do merge
a44 db diff --source pr-482 --target production
# + pedidos.moeda text not null default 'BRL'
# + constraint pedidos_moeda_valida

a44 env destroy pr-482 --yes
Segredos herdados

Um ambiente efêmero herda apenas segredos marcados propagate: true no a44.yaml. Credenciais de gateway de pagamento e de provedores externos devem apontar para sandbox — herdar um segredo de produção em ambiente de PR é o modo mais comum de disparar cobrança real a partir de um teste.

Integrações

A integração com CI é feita por a44 auth oidc, que troca o token OIDC do runner por um token Anthares válido por 15 minutos, restrito ao ambiente e aos escopos configurados na relação de confiança. Isso elimina o segredo de longa duração do repositório e impede que um fork malicioso assuma a identidade: a troca valida repository, ref e workflow contra a política antes de emitir qualquer token.

Além do CI, há integrações de primeira classe com repositórios Git (comentário automático com a URL do ambiente efêmero), com o canal de alertas (mensagem estruturada em falha de deploy) e com rastreamento distribuído via OTLP. Todas são declaradas no mesmo a44.yaml, o que mantém a configuração revisável em pull request em vez de escondida em um painel.

.github/workflows/deploy.yml
name: deploy
on:
  push:
    branches: [main]

permissions:
  id-token: write     # obrigatório para a troca OIDC
  contents: read

jobs:
  publicar:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v4
      - uses: anthares44/setup-cli@v4
        with:
          version: 4.7.2          # fixe a versão; sem isso o build não é reprodutível
      - run: a44 auth oidc --env production --ttl 15m
      - run: a44 db types --lang ts --out src/a44-types.d.ts
      - run: git diff --exit-code src/a44-types.d.ts
      - run: a44 db migrate --env production --dry-run
      - run: a44 deploy --env production --strategy canary --wait
      - if: failure()
        run: a44 rollback --env production --to previous --yes
a44.yaml — bloco integrations
integrations:
  ci:
    provider: github
    oidc:
      trust:
        repository: acme/loja-api
        refs: ["refs/heads/main", "refs/pull/*/merge"]
      scopes: [db:write, deploy:write, env:write]

  telemetry:
    otlp_endpoint: https://otlp.anthares44.dev/v1/traces
    sample_rate: 0.05             # 5% das requisições; 1.0 em ambiente de PR
    sample_rate_errors: 1.0

  alerts:
    on: [deploy.failed, webhook.endpoint_disabled, db.connections_exhausted]
    channel: eng-plantao
Amostragem de traces

sample_rate: 1.0 em produção a 600 req/min gera cerca de 26 milhões de spans por mês e é a causa mais comum de fatura de telemetria inesperada. Amostre em 5% e mantenha sample_rate_errors: 1.0 para reter todos os traces com erro, que são os que interessam na investigação.

Monitoramento

Três sinais são expostos por padrão: logs estruturados, métricas em janelas de 60 segundos e traces OTLP. Os logs são JSON linha a linha, retidos por 30 dias no plano team, e sempre carregam request_id, revision e env. O mesmo request_id aparece no cabeçalho A44-Request-Id da resposta HTTP, o que permite ir de um relato de usuário até o log exato sem busca por texto livre.

As métricas mais úteis no dia a dia são http.p95_ms, http.error_rate, db.pool_saturation, db.slow_queries (acima de 1000 ms) e webhook.delivery_lag_s. A saturação do pool é o indicador mais preditivo de incidente: acima de 0,85 sustentado por 3 minutos, a latência p95 costuma dobrar antes de qualquer erro aparecer, porque as requisições passam a esperar conexão em fila em vez de falhar imediatamente.

terminal
a44 logs tail --env production --filter 'level >= "error"' --since 15m

a44 logs query --env production \
  --filter 'request_id == "req_01J8XQ2M4K"' --json

a44 metrics get db.pool_saturation --env production --window 1h --step 60s
# 14:00 0.41  14:01 0.44 ... 14:37 0.87  14:38 0.91  <- fila de conexões
linha de log
{
  "ts": "2026-07-14T18:22:03.418Z",
  "level": "error",
  "env": "production",
  "revision": "rev_01J8Y2",
  "request_id": "req_01J8XQ2M4K",
  "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
  "msg": "query cancelada por statement_timeout",
  "sql_state": "57014",
  "duration_ms": 8004,
  "route": "GET /v3/projects/loja-api/orders"
}

db.pool_saturation

Alerta em 0,85 por 3 min. Precede a subida de p95 em vários minutos.

webhook.delivery_lag_s

Acima de 60 s indica handler lento; o retry acumula fila.

db.slow_queries

Contagem acima de 1000 ms por minuto. Cruze com pg_stat_statements.

TracesOTLP, amostrados. Mostram onde o tempo foi gasto dentro de uma requisição.
MétricasJanelas de 60 s, retenção de 13 meses. Respondem "está piorando?".
LogsJSON, 30 dias. Respondem "o que exatamente aconteceu naquela requisição?".

Boas práticas

A regra que mais reduz incidentes é separar mudança de schema de mudança de comportamento. Um release que só adiciona coluna e outro que passa a usá-la são reversíveis de forma independente; um release que faz os dois só é reversível se você aceitar perder dados. O custo é um deploy a mais por mudança estrutural, o que em um time que publica várias vezes ao dia é ruído desprezível.

A segunda é tratar tipos gerados como artefato versionado, não como etapa de build. Se a44 db types roda no CI e o resultado não é comparado com o que está no repositório, o compilador valida o schema do momento do build, não o que você revisou. Um git diff --exit-code logo após a geração transforma divergência de schema em falha de PR, em vez de erro em runtime três horas depois do merge.

  1. Fixe versões em CI

    CLI, runtime e SDK com versão exata. Atualização automática em pipeline quebra build sem mudança de código.

  2. Escreva o bloco down primeiro

    Se você não consegue escrever o down, a migração não é reversível e precisa ir em duas fases.

  3. Um ambiente efêmero por PR

    Elimina a fila por staging e faz a revisão acontecer com o schema real já aplicado.

  4. Idempotência em toda escrita

    Idempotency-Key em POST e A44-Event-Id em webhook. O retry vai acontecer.

  5. Alerte em saturação, não só em erro

    db.pool_saturation acima de 0,85 por 3 min avisa antes de a taxa de erro subir.

Faça

Migração aditiva, tipos versionados, retry só em 429 e 5xx, cursor em vez de offset, assinatura verificada sobre corpo bruto.

Evite

Editar migração aplicada, sk_ no frontend, sample_rate: 1.0 em produção, DROP COLUMN no mesmo release que remove o uso da coluna.

FAQ

As perguntas abaixo cobrem os pontos que mais aparecem em suporte relacionados ao ciclo de desenvolvimento. Casos que envolvem cobrança, contratos ou requisitos de conformidade estão documentados em Enterprise.

Quando um comportamento aqui divergir do observado, o campo a44-request-id da resposta ou o request_id do log é a informação que torna o relato investigável. Sem ele, a reprodução costuma ser inviável do lado da plataforma e o chamado volta pedindo exatamente isso.

Editei uma migração já aplicada e recebo checksum_mismatch. Como resolver?

Restaure o conteúdo original — o hash está em a44_migrations.checksum e o conteúdo aplicado sai de a44 db show 0042. Aplique a correção como migração nova. Existe a44 db repair --accept-checksum, mas só é correto quando a alteração foi cosmética (comentário, espaçamento) e o efeito no banco é comprovadamente idêntico.

Posso usar LISTEN/NOTIFY com a string de conexão padrão?

Não. O pooler roda em modo transaction e a conexão volta ao pool ao fim de cada transação, então a notificação nunca chega ao seu processo. Use a porta 6544 (conexão direta, teto de 40 conexões no plano team) ou, de preferência, os webhooks do plano de eventos, que já trazem retry e assinatura.

Por que a verificação de assinatura de webhook falha só em produção?

Quase sempre porque o framework consome o corpo e você assina o JSON reserializado. Em Express, registre a rota com express.raw({ type: 'application/json' }) antes do json() global. Em FastAPI, use await request.body() em vez do modelo Pydantic. Localmente o problema se esconde porque os payloads do túnel são pequenos e já vêm com as chaves ordenadas.

Qual o limite real da paginação por cursor?

limit aceita até 1000 por página, e a resposta é cortada em 50.000 linhas ou 6 MB, o que vier primeiro. O cursor expira em 15 min. Para extrações acima de alguns milhões de linhas, use a44 db export --format parquet, que roda contra a réplica e não consome cota da API.

O rollback --include-db restaura dados apagados?

Não. Ele executa os blocos down na ordem inversa, e um down que recria uma coluna a recria vazia. Para recuperar dados use o point-in-time recovery, que cobre os últimos 7 dias com granularidade de 1 segundo via a44 db restore --to '2026-07-14T18:00:00Z'. A restauração cria um ambiente novo e nunca sobrescreve o existente.

Quantos ambientes efêmeros posso ter e o que acontece ao estourar?

15 simultâneos no plano team e 60 no enterprise. A criação seguinte retorna 409 quota_exceeded e o job de CI termina com código 1. Ambientes expirados são removidos por um coletor que roda a cada 10 minutos, então a cota pode demorar esse tempo para liberar depois de um fechamento em massa de PRs.

Os SDKs fazem retry automático em POST?

Sim, para 429 e 5xx, e é exatamente por isso que Idempotency-Key importa. Os SDKs geram a chave por chamada quando você não fornece uma, o que torna o retry seguro dentro do mesmo processo — mas não entre processos. Se o job pode reiniciar do zero, gere e persista a chave você mesmo antes da primeira tentativa.

Como fixar a versão da API e o que acontece em uma depreciação?

A versão está no caminho (/v3) e não muda sob você. Ao anunciar depreciação, a versão antiga continua servindo por 18 meses, as respostas passam a incluir A44-Deprecation: true e Sunset com data em RFC 9110, e o painel lista os tokens que ainda chamam endpoints marcados. Após a data, as chamadas retornam 410 version_sunset.


Referência completa

Esta página cobre o fluxo de trabalho. A referência exaustiva de cada endpoint, parâmetro e código de erro está em Reference, gerada a partir do mesmo OpenAPI que alimenta os SDKs.