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.
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.
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.
a44 dev — Postgres em contêiner, emulador de storage, proxy de auth na porta 8788.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.
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.
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.Autenticar
a44 loginabre o navegador e grava um token de dispositivo em~/.a44/credentials.jsoncom permissão0600.Inicializar o projeto
a44 init loja-api --region sa-east-1escreve oa44.yamle registra o projeto na organização ativa.Subir o ambiente local
a44 devlevanta Postgres, storage e proxy. A primeira execução baixa cerca de 180 MB de imagens.Publicar
a44 deploy --env productionaplica migrações pendentes e promove o build. Um deploy típico leva de 40 a 90 s.
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
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
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.
| Comando | Flags relevantes | Comportamento |
|---|---|---|
a44 dev | --port, --seed, --no-watch | Sobe a pilha local. Com --seed executa migrations/seed.sql após aplicar o schema. |
a44 db migrate | --to, --dry-run, --lock-timeout | Aplica migrações pendentes em ordem. --lock-timeout padrão 5s; ao estourar, aborta sem aplicar nada. |
a44 db types | --lang, --out | Gera tipos a partir do schema atual do ambiente. Sem --out, escreve em stdout. |
a44 env create | --from, --ttl, --branch | Cria ambiente efêmero clonando schema (não dados) do ambiente de origem. |
a44 deploy | --env, --strategy, --wait | Publica build. Com --wait bloqueia até o health check passar ou 300 s. |
a44 rollback | --to, --include-db | Volta o release. --include-db exige bloco down em todas as migrações do intervalo. |
#!/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"
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.
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;
}
}
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.
| HTTP | error.code | Quando ocorre | Ação recomendada |
|---|---|---|---|
400 | cursor_expired | Cursor com mais de 15 min. | Reiniciar a paginação do começo. |
401 | token_expired | Token de dispositivo vencido. | Renovar com a44 login --refresh. |
403 | scope_missing | Token sem o escopo exigido. | Emitir outro token; escopos não são ampliáveis. |
409 | conflict_lock | Outra operação de controle em curso. | Retry com backoff; o lock expira em 120 s. |
422 | validation_failed | Campo inválido; error.fields lista cada um. | Corrigir. Nunca repetir. |
429 | rate_limited | Teto por token estourado. | Aguardar Retry-After. |
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
{
"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 }
}
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);
}
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.
| Credencial | Prefixo | Validade | Onde usar |
|---|---|---|---|
| Token de dispositivo | dev_ | 30 dias, renovável | Máquina do desenvolvedor. |
| Service key | sk_ | Sem expiração; rotação sugerida a cada 90 dias | CI, backend, jobs agendados. |
| Chave publicável | pk_ | Sem expiração | Navegador e apps móveis, sempre com RLS ativa. |
| Token efêmero | tmp_ | 60 s a 12 h, definido na emissão | Upload direto e links assinados. |
# 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)"'
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âmetro | Padrão | Teto | Descrição |
|---|---|---|---|
pool_size | 20 | 400 | Conexões por instância no pooler. Acima de 400 o plano team retorna 53300. |
statement_timeout_ms | 8000 | 300000 | Aborta a query com 57014. Ajustável por transação via SET LOCAL. |
idle_in_transaction_timeout | 30000 | — | Derruba transação ociosa que estaria segurando locks. |
read_replica | false | 3 réplicas | Roteia SELECT para réplica; atraso típico de 20 a 80 ms. |
max_response_rows | 50000 | 50000 | Teto 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.
// 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 };
};
}
-- í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 é.
-- +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;
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ção | Lock exigido | Bloqueia escrita? | Reversível de fato |
|---|---|---|---|
ADD COLUMN com default | ACCESS EXCLUSIVE breve | Milissegundos (PG 11+) | Sim |
CREATE INDEX | SHARE | Sim, durante toda a criação | Sim |
CREATE INDEX CONCURRENTLY | Nenhum exclusivo | Não | Sim, mas deixa índice inválido se falhar |
ALTER COLUMN TYPE | ACCESS EXCLUSIVE | Sim, reescreve a tabela | Não, se houver truncamento |
DROP COLUMN | ACCESS EXCLUSIVE breve | Milissegundos | Não — dados perdidos |
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.
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 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.
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'));
}
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.
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.
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.
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.
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
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.
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
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
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.
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
{
"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.
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.
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.
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.Um ambiente efêmero por PR
Elimina a fila por staging e faz a revisão acontecer com o schema real já aplicado.
Idempotência em toda escrita
Idempotency-KeyemPOSTeA44-Event-Idem webhook. O retry vai acontecer.Alerte em saturação, não só em erro
db.pool_saturationacima 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.
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.