Skip to content

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.sh com o runbook ao lado em scripts/backup/PROD_BACKUP_LOCAL.md. Este documento descreve o workflow CI futuro (v2.x), ainda não implementado.


1. Resumo executivo

ItemValor
O que éBackup completo e cifrado do banco de produção (mjvuzsizjlcalyfmbquy)
Quem disparaPO ou desenvolvedor autorizado, via GitHub Actions → Prod Backup (manual)
CadênciaSob demanda (manual). Antes de migrations destrutivas e snapshots semanais mínimos
Onde ficaArtifact do GitHub Actions, cifrado AES256 (gpg --symmetric)
Retenção7 dias (default), configurável até 30
RestauraçãoManual, 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:

  1. 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.
  2. 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.
  3. 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.
  4. 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_PROTOCOL quer um snapshot antes de ALTER TABLE/DROP COLUMN/UPDATE em massa.
  • (b) Pré-incidente. Detectou anomalia em produção (ex.: corrupção em escola_faturas, perda de mural_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_transacoes de 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.

SchemaConteúdoPor que incluir
publicTodas as 86 tabelas de domínioCore do app
authauth.users, auth.identities, auth.sessions etc.Sem isso o restore não vincula usuários a papéis
storagestorage.buckets, storage.objects (metadata)Necessário para reconstruir referências; objetos binários ficam para v2.1
realtimeConfiguração de canaisSem isso, postgres_changes não funciona pós-restore
vaultSecrets gerenciados pelo SupabaseNecessário para reconstrução completa
extensionsExtensões instaladaspg_cron, pg_net, pgcrypto etc.
graphql, graphql_publicSchema do PostgREST/GraphQLGeraçã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ê)

ItemPor que foraOnde vai
Objetos binários em buckets de Storagepg_dump só pega metadadosRoadmap v2.1
Edge Function codeVersionado no GitNão é "backup de banco"
Edge Function secretsGerenciados pelo Supabase dashboardNão é "backup de banco"
Estado do worker CloudflareEfêmero por designN/A
Cache do Upstash RedisEfêmero por designN/A
Logs do Supabase Analytics (BigQuery)Acesso via API separadaFora de escopo

4. Arquitetura do workflow

text
┌─────────────────────────────────────────────────────────────────┐
│  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

GarantiaComo é enforced
Read-only contra prodpg_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 prodScript aborta se PGURL_PROD_REF != mjvuzsizjlcalyfmbquy
Cifragem obrigatóriaUpload falha se BACKUP_PASSPHRASE ausente; plaintext nunca sai do runner
Plaintext destruídoStep cleanup com if: always() apaga .dump/.sql/.json/.tar.gz (mantém só .gpg)
AuditávelManifest + 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 + environment com reviewer — espelha staging-rebuild.yml.

5. Conteúdo do tarball cifrado

Artifact final: prod_backup_<UTC_TS>.tar.gz.gpg.

Após gpg --decrypt + tar -xzf:

ArquivoConteúdoGerado por
prod_full_<ts>.dumpDump custom-format com schema + dados de todos os schemas listados em §3pg_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>.sqlRoles e grants globais (best-effort — Supabase pode bloquear roles internos)pg_dumpall --globals-only --no-role-passwords
prod_manifest_<ts>.jsonMetadados estruturados (ver abaixo)generate-manifest.sh

Especificação do prod_manifest_<ts>.json

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

  1. Cifragem AES256 antes do upload. gpg --batch --symmetric --cipher-algo AES256 --passphrase "$BACKUP_PASSPHRASE". Sem isso, o job falha.
  2. Passphrase no environment, não em repo secrets. BACKUP_PASSPHRASE mora em Settings → Environments → prod-backup → Secrets — só visível após approval do reviewer.
  3. Plaintext apagado em if: always() — mesmo se o job falhar, os arquivos .dump/.sql/.json/.tar.gz são removidos. Só o .gpg sobe.
  4. Retention curta por default (7d). Operador pode escolher 1–30, nunca mais. Manual cleanup via UI permitido.
  5. Acesso ao artifact = quem tem acesso ao repo E à passphrase. Os dois fatores são necessários.
  6. Quem baixar deve, ao final:
    • Cifrar o resultado de qualquer extração local imediatamente.
    • Apagar plaintext do disco (shred -u ou 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 -l está ok, head no .dump decifrado não está).
  • ❌ Compartilhar o .gpg ou 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

RiscoMitigaçãoAceito
Vazamento do artifact cifradoSem passphrase, AES256 inviável por força brutaSim
Vazamento simultâneo de artifact + passphraseAcesso a Environments é auditado pelo GitHubSim
Operador deixa plaintext local após restoreChecklist obrigatório no runbook (§8)Sim — responsabilidade do operador

7. Como disparar

Pré-requisitos (one-time setup)

  1. Secret BACKUP_PASSPHRASE configurado em Settings → Environments → prod-backup → Secrets.
    • Passphrase gerada com openssl rand -base64 32 ou gerenciador (1Password/Bitwarden).
    • Armazenada fora do GitHub (gerenciador de senhas pessoal do PO).
  2. Environment prod-backup criado com required reviewer (o próprio PO ou trusted dev).
  3. Secret PGURL_PROD já existe (reutilizado de staging-rebuild).

Passo a passo

  1. GitHub → ActionsProd Backup (manual)Run workflow.
  2. Branch: main.
  3. Input confirm: digitar BACKUP (literal, case-sensitive).
  4. Input retention_days: default 7, ajustar se necessário (máx 30).
  5. Clicar Run workflow.
  6. Aprovar a execução em Pending deployment review (reviewer do environment).
  7. Aguardar 5–15 min.
  8. Job verde → Artifacts → baixar prod-backup-<run_id> (formato .tar.gz.gpg).
  9. Conferir SHA256 do artifact contra o dump_sha256 no $GITHUB_STEP_SUMMARY.

8. Como restaurar (operador humano — nunca automático)

Preparação

bash
# 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

bash
# 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 || true

Checklist anti-pé-no-tijolo

  • [ ] PGURL_TARGET confirmado 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.users vã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ó)

bash
# 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>.dump

9. 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.

10. Custos e limites

ItemEstimativaLimite
Tamanho atual do dump (compactado)~150–250 MB (base prod jun/2026)
Tempo de execução5–15 minTimeout do job: 45 min
Artifact por arquivo5 GB (GitHub)
Storage de artifactsConta GitHubCota do plano
Custo de runnerubuntu-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 do gpg.
  • Buckets atuais: banners-login, curso-thumbnails, tutoriais-thumbnails, mural-imagens, email-template-assets, feature-upsell-images.
  • Risco: mural-imagens pode crescer muito. Solução: split por bucket, ou pular mural-imagens se 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 (canal NTFY_TOPIC já 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_restore do dump, confere row_counts contra 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/backups listando + 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 ntfy lembrando 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