Backup de Produção — Processo, Política e Roadmap
Status: Documentação aprovada — workflow/scripts a entregar na próxima iteração Criado em: 2026-06-19 Owner operacional: PO (disparo manual) SSOT deste processo: este documento
🛠 Para backup local manual e pragmático (sob custódia do PO, fora do CI), use o script standalone em
scripts/backup/backup-prod-local.shcom o runbook ao lado emscripts/backup/PROD_BACKUP_LOCAL.md. Este documento descreve o workflow CI futuro (v2.x), ainda não implementado.
1. Resumo executivo
| Item | Valor |
|---|---|
| O que é | Backup completo e cifrado do banco de produção (mjvuzsizjlcalyfmbquy) |
| Quem dispara | PO ou desenvolvedor autorizado, via GitHub Actions → Prod Backup (manual) |
| Cadência | Sob demanda (manual). Antes de migrations destrutivas e snapshots semanais mínimos |
| Onde fica | Artifact do GitHub Actions, cifrado AES256 (gpg --symmetric) |
| Retenção | 7 dias (default), configurável até 30 |
| Restauração | Manual, sempre contra ambiente isolado (nunca produção) |
2. Motivação — por que este backup existe
O Supabase já oferece PITR (Point-In-Time Recovery) e backups diários gerenciados. Este processo não substitui isso — ele cobre lacunas operacionais que o backup gerenciado não resolve:
- Controle de janela. Precisamos congelar um momento específico (ex.: imediatamente antes de aplicar uma migration que mexe em
escola_faturas), e não depender do snapshot diário do Supabase que pode estar a 23h de distância. - Cópia off-platform sob nosso controle. Se a conta Supabase for comprometida, suspensa ou tiver incidente regional, o PITR vai junto. Um artifact cifrado no GitHub é um vetor independente.
- Trilha auditável ligada ao Git. Cada execução do workflow registra: ator, timestamp UTC, SHA do commit, run URL, retention, manifest com row counts. O PITR do Supabase não dá nada disso.
- Snapshot pontual antes de operações arriscadas. Migrations destrutivas, rebuilds, importações em massa, mudanças de RLS — tudo isso ganha um "ponto de retorno" explícito e nominal.
Casos de uso reais
- (a) Pré-migration. Fase 2 do
MIGRATION_SAFETY_PROTOCOLquer um snapshot antes deALTER TABLE/DROP COLUMN/UPDATEem massa. - (b) Pré-incidente. Detectou anomalia em produção (ex.: corrupção em
escola_faturas, perda demural_dados_publicados) e quer congelar o estado antes de qualquer intervenção corretiva. - (c) Forense LGPD. Resposta a titular de dados pedindo histórico em janela específica (preciso recuperar
usuarios.email+logs_transacoesde uma data X). - (d) Reconstrução cirúrgica. Recuperar uma tabela (
responsaveis, por exemplo) sem fazer rollback global — restaurar o dump em staging e copiar a linha de interesse.
O que NÃO substitui
- PITR do Supabase — granularidade de segundos, retenção longa, gerenciado. Continua sendo o DR primário.
- Replicação lógica/streaming — fora de escopo de "backup manual".
- DR formal cross-region — fora de escopo.
3. Escopo do backup — o que entra
Backup não é export filtrado. Entra tudo que pode ser dumpado via pg_dump.
| Schema | Conteúdo | Por que incluir |
|---|---|---|
public | Todas as 86 tabelas de domínio | Core do app |
auth | auth.users, auth.identities, auth.sessions etc. | Sem isso o restore não vincula usuários a papéis |
storage | storage.buckets, storage.objects (metadata) | Necessário para reconstruir referências; objetos binários ficam para v2.1 |
realtime | Configuração de canais | Sem isso, postgres_changes não funciona pós-restore |
vault | Secrets gerenciados pelo Supabase | Necessário para reconstrução completa |
extensions | Extensões instaladas | pg_cron, pg_net, pgcrypto etc. |
graphql, graphql_public | Schema do PostgREST/GraphQL | Geração de tipos |
Tabelas explicitamente incluídas (não há allowlist — entra tudo, mas vale registrar as sensíveis): logs_transacoes, login_otps, portal_otps, token_blacklist, senha_historico, mural_dados_publicados, email_outbox, email_eventos, sms_log, ntfy_alertas, portal_login_tentativas, portal_rate_metrics, cadastro_tokens, importacao_alunos_sessoes, importacao_resultados_sessoes, twilio_billing_log.
O que NÃO entra (e por quê)
| Item | Por que fora | Onde vai |
|---|---|---|
| Objetos binários em buckets de Storage | pg_dump só pega metadados | Roadmap v2.1 |
| Edge Function code | Versionado no Git | Não é "backup de banco" |
| Edge Function secrets | Gerenciados pelo Supabase dashboard | Não é "backup de banco" |
| Estado do worker Cloudflare | Efêmero por design | N/A |
| Cache do Upstash Redis | Efêmero por design | N/A |
| Logs do Supabase Analytics (BigQuery) | Acesso via API separada | Fora de escopo |
4. Arquitetura do workflow
┌─────────────────────────────────────────────────────────────────┐
│ GitHub Actions UI: "Run workflow" → confirm=BACKUP │
└───────────────────────────────┬─────────────────────────────────┘
│
┌───────────────▼─────────────────┐
│ Environment: prod-backup │
│ (required reviewer aprova) │
└───────────────┬─────────────────┘
│
┌───────────────▼─────────────────┐
│ Install postgresql-client-17 │
│ Source _pgurl-helpers.sh │
│ Validate PGURL_PROD (ref-lock) │
└───────────────┬─────────────────┘
│
┌───────────────▼─────────────────┐
│ pg_dump --format=custom │
│ (read-only contra prod) │
└───────────────┬─────────────────┘
│
┌───────────────▼─────────────────┐
│ pg_dumpall --globals-only │
│ (best-effort) │
└───────────────┬─────────────────┘
│
┌───────────────▼─────────────────┐
│ generate-manifest.sh │
│ (row counts, sha256, metadata) │
└───────────────┬─────────────────┘
│
┌───────────────▼─────────────────┐
│ tar -czf → gpg --symmetric │
│ AES256 + BACKUP_PASSPHRASE │
└───────────────┬─────────────────┘
│
┌───────────────▼─────────────────┐
│ upload-artifact (cifrado) │
│ retention=7d (configurável) │
└───────────────┬─────────────────┘
│
┌───────────────▼─────────────────┐
│ Cleanup: rm plaintext (always) │
│ Summary: sha256, size, tabelas │
└─────────────────────────────────┘Garantias
| Garantia | Como é enforced |
|---|---|
| Read-only contra prod | Só pg_dump/pg_dumpall/SELECT. Zero DROP/TRUNCATE/ALTER |
| Anti-inversão de URL | _pgurl-helpers.sh (pgurl_validate_format, pgurl_assert_*) — reaproveita guard do staging-rebuild |
| Ref-locked em prod | Script aborta se PGURL_PROD_REF != mjvuzsizjlcalyfmbquy |
| Cifragem obrigatória | Upload falha se BACKUP_PASSPHRASE ausente; plaintext nunca sai do runner |
| Plaintext destruído | Step cleanup com if: always() apaga .dump/.sql/.json/.tar.gz (mantém só .gpg) |
| Auditável | Manifest + summary do GitHub registram ator, run_id, commit, tabelas |
Reuso de infra existente
scripts/staging/_pgurl-helpers.sh— validação de PGURL (formato, modo, ref, inversão).- Bloco "Install PostgreSQL client 17" do
staging-rebuild.yml— copiado verbatim (matches Supabase 17.x). - Padrão
workflow_dispatch+environmentcom reviewer — espelhastaging-rebuild.yml.
5. Conteúdo do tarball cifrado
Artifact final: prod_backup_<UTC_TS>.tar.gz.gpg.
Após gpg --decrypt + tar -xzf:
| Arquivo | Conteúdo | Gerado por |
|---|---|---|
prod_full_<ts>.dump | Dump custom-format com schema + dados de todos os schemas listados em §3 | pg_dump --format=custom --compress=9 --no-owner --no-privileges --schema=public --schema=auth --schema=storage --schema=realtime --schema=vault --schema=extensions --schema=graphql --schema=graphql_public |
prod_globals_<ts>.sql | Roles e grants globais (best-effort — Supabase pode bloquear roles internos) | pg_dumpall --globals-only --no-role-passwords |
prod_manifest_<ts>.json | Metadados estruturados (ver abaixo) | generate-manifest.sh |
Especificação do prod_manifest_<ts>.json
{
"timestamp_utc": "2026-06-19T22:30:00Z",
"ref": "mjvuzsizjlcalyfmbquy",
"pg_version": "PostgreSQL 17.2",
"extensions": [
{"name": "pg_cron", "version": "1.6"},
{"name": "pg_net", "version": "0.10.0"},
{"name": "pgcrypto", "version": "1.3"}
],
"row_counts_by_table": {
"public.escolas": 142,
"public.usuarios": 1280,
"public.logs_transacoes": 458091,
"auth.users": 1280,
"storage.objects": 3421
},
"dump_sha256": "9f8a...c2",
"dump_bytes": 184729104,
"actor": "@username",
"run_id": "987654321",
"run_url": "https://github.com/org/repo/actions/runs/987654321",
"retention_days": 7,
"commit_sha": "abc123..."
}6. Política de Segurança e LGPD
O dump contém PII massiva: CPF, telefone, e-mail, senha_hash (Argon2id), tokens, OTPs históricos, conteúdo de mensagens. Tratamento obrigatório:
Regras obrigatórias
- Cifragem AES256 antes do upload.
gpg --batch --symmetric --cipher-algo AES256 --passphrase "$BACKUP_PASSPHRASE". Sem isso, o job falha. - Passphrase no environment, não em repo secrets.
BACKUP_PASSPHRASEmora emSettings → Environments → prod-backup → Secrets— só visível após approval do reviewer. - Plaintext apagado em
if: always()— mesmo se o job falhar, os arquivos.dump/.sql/.json/.tar.gzsão removidos. Só o.gpgsobe. - Retention curta por default (7d). Operador pode escolher 1–30, nunca mais. Manual cleanup via UI permitido.
- Acesso ao artifact = quem tem acesso ao repo E à passphrase. Os dois fatores são necessários.
- Quem baixar deve, ao final:
- Cifrar o resultado de qualquer extração local imediatamente.
- Apagar plaintext do disco (
shred -uou equivalente). - Nunca commitar fragmentos em PR/issue/chat.
Anti-padrões PROIBIDOS
- ❌ Upload sem cifragem ("é só um teste").
- ❌ Logar a passphrase no console do CI.
- ❌ Imprimir conteúdo do dump (
pg_restore -lestá ok,headno.dumpdecifrado não está). - ❌ Compartilhar o
.gpgou a passphrase em chat, issue, PR, e-mail não cifrado. - ❌ Restaurar em ambiente não-isolado (qualquer coisa que tenha tráfego real é não-isolada).
- ❌ Manter cópia local "para depois" sem cifragem em repouso.
Risco residual aceito
| Risco | Mitigação | Aceito |
|---|---|---|
| Vazamento do artifact cifrado | Sem passphrase, AES256 inviável por força bruta | Sim |
| Vazamento simultâneo de artifact + passphrase | Acesso a Environments é auditado pelo GitHub | Sim |
| Operador deixa plaintext local após restore | Checklist obrigatório no runbook (§8) | Sim — responsabilidade do operador |
7. Como disparar
Pré-requisitos (one-time setup)
- Secret
BACKUP_PASSPHRASEconfigurado emSettings → Environments → prod-backup → Secrets.- Passphrase gerada com
openssl rand -base64 32ou gerenciador (1Password/Bitwarden). - Armazenada fora do GitHub (gerenciador de senhas pessoal do PO).
- Passphrase gerada com
- Environment
prod-backupcriado com required reviewer (o próprio PO ou trusted dev). - Secret
PGURL_PRODjá existe (reutilizado destaging-rebuild).
Passo a passo
- GitHub → Actions → Prod Backup (manual) → Run workflow.
- Branch:
main. - Input
confirm: digitarBACKUP(literal, case-sensitive). - Input
retention_days: default7, ajustar se necessário (máx30). - Clicar Run workflow.
- Aprovar a execução em Pending deployment review (reviewer do environment).
- Aguardar 5–15 min.
- Job verde → Artifacts → baixar
prod-backup-<run_id>(formato.tar.gz.gpg). - Conferir SHA256 do artifact contra o
dump_sha256no$GITHUB_STEP_SUMMARY.
8. Como restaurar (operador humano — nunca automático)
Preparação
# 1. Decifrar (passphrase do gerenciador de senhas)
gpg --decrypt prod_backup_<ts>.tar.gz.gpg > backup.tar.gz
# 2. Extrair
tar -xzf backup.tar.gz
ls -lh prod_full_<ts>.dump prod_globals_<ts>.sql prod_manifest_<ts>.json
# 3. Conferir manifest antes de qualquer restore
cat prod_manifest_<ts>.json | jq '.timestamp_utc, .row_counts_by_table'Restauração
# PGURL_TARGET = ambiente ISOLADO (staging vazio, projeto novo Supabase, Postgres local)
# NUNCA aponta para PGURL_PROD
pg_restore \
--dbname="$PGURL_TARGET" \
--no-owner --no-privileges \
--verbose \
--jobs=4 \
prod_full_<ts>.dump
# Globals (opcional, pode falhar em host gerenciado — não-bloqueante)
psql "$PGURL_TARGET" -f prod_globals_<ts>.sql || trueChecklist anti-pé-no-tijolo
- [ ]
PGURL_TARGETconfirmado NÃO ser o ref de produção (mjvuzsizjlcalyfmbquy). - [ ] Target está vazio ou é descartável (staging recém-resetado, projeto novo, Docker local).
- [ ] Operador entende que tabelas em
auth.usersvão colidir se target não estiver vazio. - [ ] Após restore, plaintext apagado:
shred -u prod_full_<ts>.dump prod_globals_<ts>.sql backup.tar.gz. - [ ] Manifest mantido em local seguro se for evidência de incidente.
Restauração cirúrgica (uma tabela só)
# Listar o que tem no dump
pg_restore -l prod_full_<ts>.dump | grep responsaveis
# Restaurar só uma tabela em target isolado
pg_restore --dbname="$PGURL_TARGET" --table=responsaveis --data-only prod_full_<ts>.dump9. Operação e auditoria
- Onde ver execuções: GitHub → Actions → Prod Backup (manual) → histórico filtrável por ator e data.
- Trilha por execução: ator, timestamp UTC, commit SHA, run URL, retention escolhida, manifest com row counts e SHA256 — tudo no
$GITHUB_STEP_SUMMARY. - Cadência recomendada:
- Antes de toda migration de nível PROD (segue
MIGRATION_SAFETY_PROTOCOL, Fase 2). - Snapshot semanal mínimo — responsabilidade do PO.
- Sob demanda — sempre que houver suspeita de incidente que possa exigir rollback ou forense.
- Antes de toda migration de nível PROD (segue
10. Custos e limites
| Item | Estimativa | Limite |
|---|---|---|
| Tamanho atual do dump (compactado) | ~150–250 MB (base prod jun/2026) | — |
| Tempo de execução | 5–15 min | Timeout do job: 45 min |
| Artifact por arquivo | — | 5 GB (GitHub) |
| Storage de artifacts | Conta GitHub | Cota do plano |
| Custo de runner | ubuntu-latest (15 min) | Free tier suficiente para uso semanal |
Se o dump compactado passar de 2 GB, planejar migração para v2.2 (upload off-platform para R2/S3) antes de bater o teto.
11. Roadmap — v2 e follow-ups
Cada item abaixo é uma entrega independente. Ordem sugerida segue prioridade operacional.
v2.1 — Backup de objetos de Storage (prioridade alta)
- Iterar buckets via Supabase Storage API, baixar objetos para
.prod-backup/storage/<bucket>/, incluir no tarball antes dogpg. - Buckets atuais:
banners-login,curso-thumbnails,tutoriais-thumbnails,mural-imagens,email-template-assets,feature-upsell-images. - Risco:
mural-imagenspode crescer muito. Solução: split por bucket, ou pularmural-imagensse passar de limite, registrando no manifest. - Dependência: v2.2 se o tarball total passar de 5 GB.
v2.2 — Off-platform sink (AWS S3 / Cloudflare R2) (prioridade média)
- Em vez de
actions/upload-artifact, fazer upload cifrado para bucket S3/R2 dedicado. - Vantagens: retenção longa, custo previsível, sobrevive ao GitHub fora-do-ar.
- Connector AWS S3 já está disponível no projeto (
standard_connectors). - Estrutura sugerida:
s3://olp-prod-backups/YYYY/MM/prod_backup_<ts>.tar.gz.gpg+ lifecycle policy de 90 dias.
v2.3 — Cron schedule diário (prioridade média)
schedule: cron: '0 6 * * *'(03:00 BRT) em paralelo ao disparo manual.- Skip se já houve backup nas últimas 20h (idempotência).
- Alerta
ntfy(canalNTFY_TOPICjá configurado) se falhar 2 dias consecutivos. - Pré-requisito: v2.2 ativo (não vamos lotar artifacts do GitHub).
v2.4 — Backup verificável (restore canary) (prioridade alta)
- Step adicional pós-backup: spin de container Postgres efêmero (
services: postgres),pg_restoredo dump, confererow_countscontra manifest. - Garante "backup que restaura", não só "arquivo que existe".
- Custo: +5–10 min ao job. Vale.
v2.5 — Catálogo de backups (prioridade baixa)
- Tabela admin-only
prod_backups_catalog(run_id, timestamp, ator, sha256, size, retention_until, manifest_url). - Alimentada pelo manifest via API do GitHub.
- UI mínima em
/admin/sistema/backupslistando + facilitando escolha em incidente. - Não armazena o dump em si — só o catálogo.
v2.6 — Integração com MIGRATION_SAFETY_PROTOCOL (prioridade alta)
- Fase 2 do protocolo passa a exigir link para run de Prod Backup das últimas 24h antes de aplicar migration destrutiva.
- Pode ser enforced via PR template + CI check que verifica menção a
actions/runs/<id>no corpo do PR.
v2.7 — Rotação automática de BACKUP_PASSPHRASE (prioridade baixa)
- A cada N meses, alerta no
ntfylembrando rotação. - Sem automação total: rotação muda passphrase histórica, então requer ritual de re-cifragem ou aceitar que backups antigos viram inacessíveis após T+retention.
12. Referências
docs/operations/CRON_JOBS.md— Jobs agendados (contexto para v2.3)docs/operations/DATABASE_CLEANUP.md— Limpeza/reset (operação inversa)docs/operations/INCIDENT_POLICY.md— Política de incidentesdocs/development/AUDIT.md— Checklist de auditoria (seção operações)docs/development/MIGRATION_SAFETY_PROTOCOL.md— Protocolo de 3 fases (consumidor primário deste backup).github/workflows/staging-rebuild.yml— Padrão de workflow reaproveitadoscripts/staging/_pgurl-helpers.sh— Helpers de validação de PGURL