Skip to content

Plano de Migração: Upstash Redis via Cloudflare Worker

Status: Em implementação — Fase 0 pendente
Última revisão: 2026-04-19
Pré-requisito: Conta Upstash + database provisionado
Documentos relacionados: WORKER_V3_REDIS_PLAN · WORKER_TELEMETRY · RATE_LIMITS


0. Estratégia de rollout (produção vs staging)

A migração para Worker v3 + Redis acontece em duas frentes simultâneas, com regras de bloqueio diferentes em cada ambiente:

AmbienteModo de operaçãoJanelaObjetivo
Produção (olp.digital)Observação total — telemetria ativa, zero bloqueio novo~4 semanas contínuasColetar baseline real de tráfego por endpoint, escola, usuário
Staging (staging.olp.digital)Bloqueio real ativo desde a Fase 1 + scripts de stress testImediatoValidar funcionamento do código, headers RFC 6585, mensagens PT-BR e os limites propostos

Após as ~4 semanas de coleta em produção, o código que está rodando em staging (já validado) é promovido para produção, substituindo os limites provisórios pelos calibrados pelos dados reais. A calibração pode (e provavelmente vai) alterar valores, mas não a estrutura do código.

Prefixo de chave Redis isola os dois ambientes no mesmo database Upstash: olp: em produção, olp-stg: em staging.


0.1 Rate limits por grupo

Tabela completa dos parâmetros aprovados. Coluna Modo (prod) indica como o Worker se comporta em produção durante a Fase 3a (observação) e Fase 3b (bloqueio real). Coluna Modo (staging) é o comportamento no ambiente de testes desde a Fase 1.

Grupo A — Auth crítico

EndpointLimiteEscopoModo (prod)Modo (staging)
send-otp5/15minusuario_idObservação → BloqueioBloqueio
send-otp20/horaipObservação → BloqueioBloqueio
verify-otp / verify-password10/15minip + lockout progressivoObservação → BloqueioBloqueio
set-password / change-password / reset-password10/horausuario_idObservação → BloqueioBloqueio

Grupo B — Leitura autenticada (apenas observação até calibração)

EndpointLimiteEscopoModo (prod)Modo (staging)
/mesem bloqueiousuario_idApenas alerta (ver WORKER_TELEMETRY.md §8)Apenas alerta
*-dashboard120/minusuario_idObservação (não bloquear até calibrar)Bloqueio
gestao-* GET, listagens300/minusuario_idObservaçãoBloqueio
eventos-calendario, escola-dados GET180/minusuario_idObservaçãoBloqueio
mural-escola GET120/minipObservaçãoBloqueio

Grupo C — Escrita autenticada

EndpointLimiteEscopoModo (prod)Modo (staging)
CRUD geral (tarefas, eventos, anotações, perfil)60/minusuario_idObservação → BloqueioBloqueio
controle.batch_update, importação10/minusuario_idObservação → BloqueioBloqueio
comunicacao-escola (broadcast)5/min + 50/horaescola_idObservação → BloqueioBloqueio
mural-escola POST30/minusuario_idObservação → BloqueioBloqueio
mural-escola POST60/minescola_idObservação → BloqueioBloqueio

Grupo D — Mural/Portal (já em produção v4, manter)

EndpointLimiteEscopoModo (prod)Modo (staging)
Burst150/5sipBloqueio (já ativo)Bloqueio
lookup_escola100/minipBloqueio (já ativo)Bloqueio
lookup_escola200/minescola_idBloqueio (já ativo)Bloqueio
login_aluno_a400/5minipBloqueio (já ativo)Bloqueio
login_aluno_a200/minescola_idBloqueio (já ativo)Bloqueio
send_otp_*50/15minipBloqueio (já ativo)Bloqueio
send_otp_*100/15minescola_idBloqueio (já ativo)Bloqueio
cadastro_responsavel30/30minipBloqueio (já ativo)Bloqueio
auto_vincular_matricula30/60minip + lockout 5/8/12 falhasBloqueio (já ativo)Bloqueio

Grupo E — Cron / Webhook

ISENTO — autenticação por CRON_SECRET e HMAC. Sem rate limit no Worker em qualquer ambiente.


0.2 Decisões arquiteturais (D1–D6) — todas aprovadas

#DecisãoStatusOpção escolhida
D1Backing store para rate limit✅ AprovadaHíbrido: Map in-memory para burst ≤10s (limitação documentada: fragmentado por PoP) + Redis para janelas ≥1min
D2Comportamento quando Redis falha✅ AprovadaFail-open no Worker + alerta ntfy crítico. Backend mantém suas próprias defesas (RLS, lockout).
D3Quota Wasender✅ Aprovada200 mensagens/escola/dia como proteção de fila compartilhada (não limitação de uso). Revisitar com dados reais.
D4Estratégia de calibração✅ AprovadaLimites estáticos calibrados pelo pico hipotético + alerta em 70% do threshold. Sem distinção de horário até pico real ser medido por 30 dias consecutivos.
D5Bypass para testes E2E✅ AprovadaHeader X-E2E-Bypass + secret — bypass total. Testes E2E precisam ser determinísticos.
D6Retenção de métricas✅ AprovadaTTL natural por janela no Redis (2h) + agregação horária no Postgres via cron, retenção 90 dias.

0.3 Regras obrigatórias de código Redis (resumo)

Detalhes e exemplos no WORKER_V3_REDIS_PLAN.md §5. As regras invioláveis:

#RegraConsequência da violação
1Toda chave Redis DEVE ter TTL — sem exceçãoMemory leak no Upstash, billing surpresa
2redis.keys() PROIBIDO — usar SCAN com cursorBloqueio do Redis em produção, gateway derruba
3Telemetria sempre via ctx.waitUntil() — nunca no caminho críticoLatência do gateway acoplada à latência do Redis
4Fail-open obrigatório — se Redis falhar, Worker continua sem rate limitAlerta ntfy crítico, mas site não cai
5Prefixo de ambiente obrigatório: olp: produção, olp-stg: stagingCross-contamination de contadores entre ambientes

1. Contexto e Motivação

Por que Redis?

OperaçãoPostgres (atual)Upstash Redis
Rate limit check~20-80ms (query + RLS)~5-15ms (REST API)
Blacklist check~15-40ms~5-10ms
Session lookup~20-50ms~5-10ms
Alert cooldown~15-30ms~3-8ms

Latência real do Upstash

  • Upstash REST API: ~5-15ms por comando (região gru1 ou us-east-1)
  • Não é zero latência — ainda há hop de rede HTTP
  • Latência mínima possível (~1ms) só com Redis TCP nativo, que Upstash REST não usa
  • Mesmo assim, 3-8x mais rápido que queries Postgres equivalentes

2. Inventário de Migração

Categoria 1 — Migração Imediata (dados efêmeros)

Tabela PostgresChave RedisMotivo
token_blacklistolp:bl:{jti}TTL natural, sem necessidade de cron cleanup
portal_alert_cooldownolp:ac:{alert_key}Cooldown puro, TTL 15min
portal_rate_metricsolp:metrics:{tipo}:{escola}:{min}Contadores efêmeros

Categoria 2 — Migração Posterior (novo recurso)

RecursoChave RedisMotivo
Sessões ativasolp:sess:{jti} + olp:user-sess:{id}Não existe hoje, Redis é ideal
Burst limit (Worker)olp:rl:burst:{ip}Substitui Map in-memory do Worker

Categoria 3 — NÃO Migrar (permanecem no Postgres)

TabelaMotivo
logs_transacoesCompliance, auditoria, queries complexas, retenção longa
portal_login_tentativasAnalytics histórico, relatórios, JOINs
senha_historicoSegurança, bcrypt hashes, retenção permanente
login_otpsCurta vida mas precisa de atomicidade com auth flow

3. Arquitetura

┌─────────────┐     ┌──────────────────┐     ┌─────────────┐
│   Browser   │────▶│ Cloudflare Worker │────▶│  Supabase   │
│             │     │                  │     │  Edge Fns   │
└─────────────┘     │  ┌────────────┐  │     └──────┬──────┘
                    │  │ Burst Rate │  │            │
                    │  │ Blacklist  │  │            │
                    │  └─────┬──────┘  │            │
                    └────────┼─────────┘            │
                             │                      │
                    ┌────────▼─────────┐            │
                    │  Upstash Redis   │◀───────────┘
                    │  (REST API)      │  Rate limits
                    │                  │  Sessions
                    │  gru1 region     │  Blacklist
                    └──────────────────┘  Cooldowns

Fluxo de um request autenticado:

  1. Browser → Worker: burst check via Redis (olp:rl:burst:{ip})
  2. Worker: decode JWT, blacklist check via Redis (olp:bl:{jti})
  3. Worker → Edge Function: proxy com headers
  4. Edge Function: rate limit via Redis (olp:rl:{tipo}:{id})
  5. Edge Function: lógica de negócio via Supabase Postgres
  6. Edge Function: log via Postgres (logs_transacoes)

4. Key Schema Redis

text
# ── BLACKLIST ──
olp:bl:{jti}                         → "logout" | "admin_revoke"         TTL=token_expiry_remaining

# ── SESSÕES ──
olp:sess:{jti}                       → HASH {user_id, nome, papel, ip, criado_em}   TTL=8h
olp:user-sess:{usuario_id}           → SET de JTIs ativos                            TTL=8h (renovado a cada login)

# ── RATE LIMITS ──
olp:rl:burst:{ip}                    → counter                           TTL=5s
olp:rl:lookup:ip:{ip}                → counter                           TTL=1min
olp:rl:lookup:escola:{escola_id}     → counter                           TTL=1min
olp:rl:login-a:escola:{escola_id}    → counter                           TTL=1min
olp:rl:login-a:ip:{ip}              → counter                           TTL=5min
olp:rl:otp:ip:{ip}                  → counter                           TTL=15min
olp:rl:otp:escola:{escola_id}       → counter                           TTL=15min
olp:rl:cadastro:ip:{ip}             → counter                           TTL=30min
olp:rl:vinculo:ip:{ip}              → counter                           TTL=60min

# ── LOCKOUT ──
olp:lo:{identificador}               → counter (falhas consecutivas)     TTL=24h

# ── ALERT COOLDOWN ──
olp:ac:{alert_key}                   → "1"                               TTL=15min

# ── MÉTRICAS ──
olp:metrics:{tipo}:{escola_id}:{min} → counter                           TTL=48h

Índice Reverso — olp:user-sess:{usuario_id}

Resolve o problema de invalidação pós-troca-de-senha sem O(N) scan:

text
# Login
HSET olp:sess:{jti} user_id {id} nome {nome} papel {papel} ip {ip}
EXPIRE olp:sess:{jti} 28800
SADD olp:user-sess:{usuario_id} {jti}
EXPIRE olp:user-sess:{usuario_id} 28800

# Logout
DEL olp:sess:{jti}
SREM olp:user-sess:{usuario_id} {jti}

# Invalidação pós-senha (revogar TODAS as sessões do usuário)
jtis = SMEMBERS olp:user-sess:{usuario_id}
for jti in jtis:
    DEL olp:sess:{jti}
    SET olp:bl:{jti} "senha_alterada" EX {ttl_restante}
DEL olp:user-sess:{usuario_id}

# Complexidade: O(M) onde M = sessões do usuário (tipicamente 1-3)

5. Regras Obrigatórias

5.1 — redis.keys() PROIBIDO

typescript
// ❌ NUNCA — bloqueante, varre todas as chaves
const keys = await redis.keys("olp:sess:*");

// ✅ CORRETO — iterativo, não bloqueante
let cursor = 0;
const results: string[] = [];
do {
  const [nextCursor, keys] = await redis.scan(cursor, { match: "olp:sess:*", count: 100 });
  cursor = nextCursor;
  results.push(...keys);
} while (cursor !== 0);

5.2 — Dual-Write na Transição

Durante a migração, toda operação escrita deve gravar em Redis E Postgres:

typescript
// Rate limit: gravar nos dois
await redis.incr(`olp:rl:login-a:ip:${ip}`);        // Redis (leitura primária)
await supabase.from("portal_login_tentativas").insert({...}); // Postgres (backup)

Período mínimo de dual-write: 2 semanas antes de drop de qualquer tabela.

5.3 — Fallback Postgres

Se Redis falhar (timeout, erro de rede), o sistema DEVE cair para Postgres:

typescript
async function checkRateLimitRedis(key: string, max: number, ttlSeconds: number): Promise<boolean> {
  try {
    const count = await redis.incr(key);
    if (count === 1) await redis.expire(key, ttlSeconds);
    return count <= max;
  } catch (err) {
    console.error("Redis fallback para Postgres:", err);
    return await checkRateLimitPostgres(/* params */); // fallback
  }
}

5.4 — TTLs Explícitos

Toda chave Redis DEVE ter TTL. Sem exceções. Chaves sem TTL = memory leak.


6. Fases de Implementação

Fase A — Infra Redis no Cloudflare Worker

Pré-requisito: Conta Upstash criada, database provisionado (região gru1)

#TarefaArquivo
A.1Criar database Upstash (gru1 ou us-east-1)Dashboard Upstash
A.2Adicionar secrets no CloudflareUPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKEN
A.3Worker: importar @upstash/rediscloudflare-worker/src/index.ts
A.4Worker: burst rate limit via Redis INCR+EXPIREsubstitui Map in-memory
A.5Worker: blacklist check — GET olp:bl:{jti}decode JWT sem verify (jti público)

Validação: Monitorar latência de burst check no Worker por 1-2 dias antes de avançar.

Fase B — Rate Limit nas Edge Functions

#TarefaArquivo
B.1Criar redis-client.ts wrappersupabase/functions/_shared/redis-client.ts
B.2Adicionar secrets SupabaseUPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKEN
B.3checkRateLimit() → Redis INCR (fallback Postgres)portal-security.ts
B.4registrarTentativa() → dual-writeportal-security.ts
B.5checkLockout() → Redis INCR + TTL 24hportal-security.ts
B.6shouldSendAlert() → Redis SET + TTL 15minportal-security.ts

Nota: portal-security.ts (224 linhas) será refatorado nesta fase, separando:

  • portal-security-constants.ts — constantes e thresholds
  • portal-security-redis.ts — implementações Redis com fallback
  • portal-security.ts — re-exports (backward compat)

Fase C — Token Blacklist em Redis

#TarefaArquivo
C.1Logout: SET olp:bl:{jti} + Postgres INSERTlogout/index.ts
C.2Auth check: blacklist via Redis GET (fallback Postgres)_shared/auth-helpers.ts
C.3Portal auth: blacklist via Redis_shared/jwt-portal.ts
C.4Após 2 semanas: drop tabela token_blacklistMigration SQL
C.5Remover cleanup de blacklist do maintenance-cronmaintenance-cron/index.ts

Fase D — Sessões Ativas em Redis

#TarefaArquivo
D.1Login: HSET olp:sess:{jti} + SADD olp:user-sess:{id}verify-otp/index.ts, select-role/index.ts
D.2Logout: DEL sess + SREM user-sesslogout/index.ts
D.3Admin list: SCAN olp:sess:* + HGETALLNova Edge Function
D.4Admin revoke: DEL sess + SET bl + SREM user-sessNova Edge Function
D.5Invalidação pós-senha: SMEMBERS user-sess → revoke allchange-password/index.ts
D.6Frontend: painel de sessões ativassrc/components/admin-sessoes.tsx

Fase E — Cleanup

#TarefaCondição
E.1Drop portal_alert_cooldownFase B validada (2 sem)
E.2Drop token_blacklistFase C validada (2 sem)
E.3Simplificar maintenance-cronRemover tarefas migradas
E.4portal_login_tentativas → write-onlyManter para analytics, remover reads
E.5Avaliar drop portal_rate_metricsSe métricas Redis suficientes

7. Dívidas Técnicas — Inventário

Prioridade 1 — Resolvidas pelo Redis

IDDívidaSolução Redis
P1Invalidação de sessão pós-troca-senha não revoga todasolp:user-sess:{id} → revoke all
P2Burst rate limit usa Map in-memory (perde entre deploys)Redis INCR persistente
P3Alert cooldown por isolate (não cross-instance)Redis SET com TTL
P4Token blacklist precisa de cron cleanupTTL automático

Prioridade 2 — Melhorias com Redis

IDDívidaBenefício
P5Rate limit queries pesadas no pico (12h BRT)Redis ~5ms vs Postgres ~40ms
P6Sem visibilidade de sessões ativasPainel admin com SCAN
P7Lockout check faz query em 24h de dadosRedis counter com TTL

Prioridade 3 — Independentes do Redis

IDDívidaStatus
P8Papéis bloqueados (pedagógico, professor, marketing) precisam de UIBacklog
P9Portal: abandonment rate alto (324 lookups vs 185 logins)Investigar UX
P10WhatsApp (Wasender) retry/dead-letter queueBacklog

8. Estimativa de Custo Upstash

Comandos por request autenticado

OperaçãoComandos
Blacklist GET1
Rate limit (INCR + EXPIRE)2-3
Session check1
Métricas INCR1-2
Total por request5-8

Projeção de custo

EscalaRequests/diaComandos/diaCusto/diaCusto/mês
Atual (2 escolas, 93 alunos)~500-1.5k~2.5k-12kFree tier$0
10 escolas~5k~30k~$0.04~$1.20
50 escolas~50k~300k~$0.60~$18
200 escolas~200k~1.2M~$2.40~$72
Pico matrícula~500k~3M~$6.00

Pricing Upstash: $0.2 por 100k comandos após free tier (10k/dia grátis).


9. Riscos e Mitigações

RiscoImpactoMitigação
Upstash indisponívelRate limits e blacklist falhamFallback Postgres (§5.3)
TTL não configurado em chaveMemory leak no RedisLint rule: toda chave DEVE ter EXPIRE
redis.keys() usado acidentalmenteBloqueio em produçãoCode review + grep CI
Dual-write inconsistênciaDados divergem Redis/PostgresMonitorar contagem por 2 semanas
Custo escala inesperadaBilling surpresaAlert no Upstash dashboard > 500k cmd/dia

10. Documentação a Atualizar

DocumentoMudançaFase
docs/architecture/CLOUDFLARE_WORKER_GATEWAY.mdSeção Redis, variáveis, fluxo burstA
docs/architecture/CLOUDFLARE_WORKER_CODE.mdImports Upstash, burst RedisA
docs/security/RATE_LIMITS.mdReescrever — Redis substitui PostgresB
docs/security/AUTHENTICATION.mdBlacklist via Redis, sessõesC
docs/operations/CRON_JOBS.mdRemover tarefas migradasE
docs/operations/DATABASE_CLEANUP.mdTabelas removidasE
docs/operations/SESSION_MANAGEMENT_ALERTS.mdSubstituído por RedisD
docs/operations/ANOMALY_DETECTION.mdDependência RedisB
docs/README.mdUpstash no stackA
NOVO: docs/architecture/UPSTASH_REDIS.mdArquitetura, key schema, TTLs, fallback, SCAN policyA

11. Dados Reais de Uso (Coletados 2026-03-30)

Volume

  • 93 alunos únicos ativos no portal
  • 2 escolas ativas
  • 5-30 logins/dia (normal), 280 logins/dia (pico)
  • ~5 logins/aluno/mês (re-login frequente, não sessão persistente)

Distribuição horária

08h: ░░░░░ 5%
09h: ░░░░░░ 6%
10h: ░░░░░░░ 8%
11h: ░░░░░░░░░ 10%
12h: ██████████████████████████████████████ 37%  ← PICO (almoço)
13h: ███████████████ 15%
14h: ░░░░░░░░ 8%
15h: ░░░░░ 5%
16h+: ░░░░░░ 6%
  • 52% do tráfego concentrado entre 12h-13h BRT
  • Uso claramente escolar (intervalo de almoço)

Implicação para Redis

  • Volume atual cabe no Free Tier do Upstash
  • Pico de 12h gera ~50-100 comandos/minuto — trivial para Redis
  • Scaling para 50+ escolas requer tier pago (~$18/mês)

12. Checklist de Prontidão por Fase

Fase A — Pronto para iniciar quando:

  • [ ] Conta Upstash criada
  • [ ] Database provisionado (região gru1)
  • [ ] Secrets adicionados no Cloudflare Dashboard
  • [ ] Worker local testado com Upstash

Fase B — Pronto quando:

  • [ ] Fase A validada em produção (1-2 dias)
  • [ ] Secrets adicionados no Supabase Dashboard
  • [ ] redis-client.ts criado e testado

Fase C — Pronto quando:

  • [ ] Fase B validada (rate limits funcionando via Redis)
  • [ ] Dual-write blacklist implementado

Fase D — Pronto quando:

  • [ ] Fase C validada (blacklist via Redis)
  • [ ] Índice reverso olp:user-sess testado

Fase E — Pronto quando:

  • [ ] Todas as fases anteriores validadas por ≥2 semanas
  • [ ] Zero discrepância Redis vs Postgres no período de dual-write
  • [ ] Backup das tabelas antes do drop