Runbook — backup e restore do JeP
docs/runbooks/backup-restore-jep.md
Runbook — backup e restore do JeP
Estado: em operação contra a PRODUÇÃO do ARMELS desde 2026-09-03 (autorizado pelo board, gatilho "dado de cliente real"), e contra staging desde a missão E0. Os dois ambientes rodam o mesmo mecanismo com alvo, credencial B2, destinatários
agee prefixo próprios.⛔ Produção é READ-ONLY neste runbook:
pg_dump+SELECTde contagem. Nenhuma escrita, nenhum DDL, e nenhum restore aponta para lá — o drill só restaura em Postgres efêmero, eguarda_alvo_efemerorecusa qualquer host gerenciado (*.supabase.co,*.supabase.com, o ref de produção).
O que existe
| Peça | staging | produção (ARMELS) |
|---|---|---|
| Scripts (fonte da verdade) | scripts/backup/ neste repo | idem |
| Wrapper de cron | scripts/backup/wrapper-cron.sh → neo:/usr/local/bin/tynna-backup-jep.sh | idem |
| Deploy em execução | neo:/opt/tynna-backup-jep/ | idem |
| Credencial do alvo | neo:/etc/tynna-backup-jep.env (600) | neo:/etc/tynna-backup-jep-prod.env (600) |
| Credencial do B2 | neo:/etc/tynna-backup-jep-b2.env — app-key backup-jep-staging, prefixo jep-staging/ | neo:/etc/tynna-backup-jep-b2-prod.env — app-key backup-jep-producao, prefixo jep-producao/ |
| Identidade age do drill | neo:/etc/tynna-backup-jep.d/drill-identity.key (root-only) | a mesma |
| Chave do fundador | Keychain do Mac, JEP_BACKUP_AGE_KEY_FOUNDER_STAGING | Keychain do Mac, JEP_BACKUP_AGE_KEY (conta tynna) + cópia offline |
| Canal de alarme | neo:/etc/paperclip-alerts.env (Telegram) | idem |
| Artefatos | B2 tynna-paperclip-backups/jep-staging/ | B2 tynna-paperclip-backups/jep-producao/ |
| Log | neo:/var/log/tynna-backup-jep.log | idem |
Menor privilégio no B2 — a fronteira, provada
Cada ambiente tem app-key própria, restrita a um namePrefix, com as 5
capabilities mínimas (listBuckets, listFiles, readFiles, writeFiles, deleteFiles). Medido por mutação real em 2026-09-03 com a key de produção:
escrita/list/delete dentro de jep-producao/ passam; list da raiz,
jep-staging/ e daily/ devolvem 0 nomes; cat de um objeto real de
jep-staging/ devolve 0 bytes; escrita e delete cruzados são recusados.
⚠️ Armadilha medida: rclone lsf sai com rc=0 e listagem vazia
quando a key não alcança o prefixo. O código de saída não é sinal de
autorização — conte os nomes devolvidos, não o rc.
Agendamento
0 2 * * * (usuário paperclip) offsite-backup.sh # off-site do Paperclip
30 3 * * * /usr/local/bin/tynna-backup-jep.sh dump # staging, diário
0 4 * * * /usr/local/bin/tynna-backup-jep.sh dump production # PRODUÇÃO, diário
0 5 1 * * /usr/local/bin/tynna-backup-jep.sh drill # drill staging, dia 1
0 6 1 * * /usr/local/bin/tynna-backup-jep.sh drill production # drill PRODUÇÃO, dia 1
Quatro horários distintos, nenhum colide. O prune de cada ambiente é escopado
no próprio prefixo — daily/ (Paperclip) nunca é tocado, e a credencial de
produção sequer o enxerga.
O 2º argumento do wrapper é opcional e default staging, de propósito: as
linhas de cron antigas seguem valendo sem edição, e apontar para produção é um
ato explícito, escrito na linha de comando. O wrapper ainda confere que o
ambiente pedido bate com o JEP_BACKUP_ENV declarado dentro do env file — um
arquivo trocado por engano aborta em vez de rodar contra o alvo errado.
O que o artefato contém
Dois arquivos por execução, ambos cifrados com age:
jep-<env>-<STAMP>.sql.gz.age—pg_dumpdos schemaspublic+auth.jep-<env>-<STAMP>.manifest.json.age— contagem de linhas por tabela lida na origem no momento do dump. É contra ele que o drill confere integridade, sem nunca reconectar na origem (requisito duro quando a origem for produção).
Por que auth entra junto
public tem 6 foreign keys para auth (ex.: public.auth_audit_log.user_id
→ auth.users.id). Um dump só de public não restaura — morre em violação
de FK. Isso foi medido, não suposto: foi o erro real do drill antes da correção.
Lacunas — o que o backup NÃO cobre
| Fora do artefato | Tamanho medido em produção (2026-09-03) | Consequência num restore real |
|---|---|---|
storage.* | 8 tabelas de metadado; 10 objetos / 625 kB de arquivo real, todos no bucket trabalhos-pdf (comprovantes, balaustres-pdf, loja-logos, potencia-docs estão vazios) | Os arquivos em si vivem no S3 do Supabase e não entram em pg_dump nenhum. Hoje não têm backup nenhum. |
realtime.* | 2 tabelas | Configuração de replicação; recriada pelo provedor. |
cron.* | 2 tabelas | Jobs agendados somem. Os alertas de tesouraria (pg_cron) precisam ser recriados à mão. |
vault.* | 1 tabela | Segredos do projeto; recriados à mão. |
| Config do projeto | — | Chaves de API, URLs de redirect, provedores de auth, Edge Functions, políticas de storage — nada disso é banco. |
Restaurar num projeto Supabase NOVO não é "rodar este arquivo". Lá o GoTrue
já é dono do schema auth, e a carga vira "dados de auth para dentro do schema
existente". Este artefato prova integridade (tudo que a origem tinha volta,
linha a linha); não é um restore turnkey de projeto Supabase.
Restore manual (o fundador, sozinho)
A metade privada da chave do fundador vive só no Keychain do Mac dele
(produção: serviço JEP_BACKUP_AGE_KEY, conta tynna, com cópia offline;
staging: JEP_BACKUP_AGE_KEY_FOUNDER_STAGING, conta jep-staging). Ela não
existe em lugar nenhum da frota. Com ela:
# 1. identidade a partir do Keychain (nunca em arquivo permanente)
security find-generic-password -s JEP_BACKUP_AGE_KEY -a tynna -w > /tmp/id.key
# 2. decifrar o artefato baixado do B2
age --decrypt -i /tmp/id.key < jep-production-<STAMP>.sql.gz.age | gunzip > dump.sql
# 3. restaurar num Postgres limpo (NUNCA por cima de um banco com conteúdo)
createdb restore_teste
psql -d restore_teste -v ON_ERROR_STOP=1 -f dump.sql
rm -f /tmp/id.key
Alarme
Alarme por transição de estado (Telegram), nunca diário: um backup que falha 30 dias seguidos manda 1 alarme, não 30. Alarme que toca todo dia vira ruído e para de ser lido — que é como falha silenciosa nasce.
Estado em neo:/var/lib/tynna-backup-jep/<componente>.state.
O drill também recusa artefato obsoleto (JEP_BACKUP_RPO_MAX_SEG, 48h por
padrão): se o cron do dump parar, o drill fica vermelho mesmo com o arquivo
íntegro. Cobertura parcial e declarada — se dump e drill pararem juntos, só um
monitor externo pega.
Produção — números medidos (2026-09-03)
| Métrica | Produção (ARMELS) | Staging (referência) |
|---|---|---|
| Tabelas no artefato | 101 (public 78 + auth 23) + 3 views | 101 |
| Linhas conferidas no restore | 3.218 (60 tabelas com linhas) | 459 |
| Banco na origem | 22 MB | 18 MB |
| Artefato cifrado | 448.126 B (≈438 KiB) + manifesto 5.343 B | ≈84 KiB |
| SQL decifrado | 1.602.220 B | 528.128 B |
| Dump ponta-a-ponta | 25 s (pg_dump 6 s) | 11 s |
| RTO | 32 s ponta-a-ponta (recorte só-restore 10 s) | 18 s |
| RPO | 53 s no drill imediato; ≤ 24 h em regime | 21 s |
Método: date +%s no início do drill até a verificação de integridade passar
(inclui subir o Postgres efêmero, baixar do B2, decifrar, restaurar e conferir);
RPO derivado do stamp no nome do artefato, nunca inferido.
O que estes números NÃO capturam: o tempo humano de decidir restaurar, e o provisionamento de um projeto Supabase novo. Restaurar num projeto Supabase novo não é "rodar este arquivo" — ver a seção de lacunas.
Apontar um ambiente novo para produção (exige [BOARD])
Não é sessão de construção, é troca de alvo. Num env file próprio (não
edite o de staging), p. ex. /etc/tynna-backup-jep-prod.env:
JEP_BACKUP_ENV=production
JEP_DATABASE_URL='<conexão do projeto de produção, senha percent-encoded>'
JEP_BACKUP_ALLOW_PRODUCTION=1 # a guarda recusa produção sem isto
JEP_BACKUP_B2_PREFIX=jep-producao # prefixo próprio, retenção própria
⛔ Nunca escreva arquivo de credencial com sed ou interpolação de shell.
Chave B2 e senha contêm /, + e $; gere o arquivo e entregue-o por
stdin (ssh host 'umask 077; cat > /etc/...'), conferindo o sha256sum
dos dois lados. Isso já quebrou um arquivo e vazou um segredo em 2026-09-03.
A guarda é fail-closed nos dois eixos e foi exercitada: sem
JEP_BACKUP_ALLOW_PRODUCTION=1 ou sem JEP_BACKUP_ENV=production, o
script aborta ao ver o ref etoaddrlxbehtelapygj no alvo.
Diagnóstico rápido
ssh neo-root 'tail -30 /var/log/tynna-backup-jep.log'
ssh neo-root 'grep -H "" /var/lib/tynna-backup-jep/*.state'
ssh neo-root '/usr/local/bin/tynna-backup-jep.sh drill' # drill de staging agora
ssh neo-root '/usr/local/bin/tynna-backup-jep.sh drill production' # drill de PRODUÇÃO agora
São quatro componentes de alarme independentes:
backup-jep-staging, drill-jep-staging, backup-jep-production,
drill-jep-production.
Só leitura. Toda mudança nestes documentos é pull request.