Skip to content

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

Status: Planejado — Aguardando provisionamento Upstash
Última revisão: 2026-03-30
Pré-requisito: Conta Upstash + database provisionado


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