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ção | Postgres (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 Postgres | Chave Redis | Motivo |
|---|---|---|
token_blacklist | olp:bl:{jti} | TTL natural, sem necessidade de cron cleanup |
portal_alert_cooldown | olp:ac:{alert_key} | Cooldown puro, TTL 15min |
portal_rate_metrics | olp:metrics:{tipo}:{escola}:{min} | Contadores efêmeros |
Categoria 2 — Migração Posterior (novo recurso)
| Recurso | Chave Redis | Motivo |
|---|---|---|
| Sessões ativas | olp: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)
| Tabela | Motivo |
|---|---|
logs_transacoes | Compliance, auditoria, queries complexas, retenção longa |
portal_login_tentativas | Analytics histórico, relatórios, JOINs |
senha_historico | Segurança, bcrypt hashes, retenção permanente |
login_otps | Curta 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
└──────────────────┘ CooldownsFluxo de um request autenticado:
- Browser → Worker: burst check via Redis (
olp:rl:burst:{ip}) - Worker: decode JWT, blacklist check via Redis (
olp:bl:{jti}) - Worker → Edge Function: proxy com headers
- Edge Function: rate limit via Redis (
olp:rl:{tipo}:{id}) - Edge Function: lógica de negócio via Supabase Postgres
- Edge Function: log via Postgres (
logs_transacoes)
4. Key Schema Redis
# ── 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:
# 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
// ❌ 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:
// 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:
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)
| # | Tarefa | Arquivo |
|---|---|---|
| A.1 | Criar database Upstash (gru1 ou us-east-1) | Dashboard Upstash |
| A.2 | Adicionar secrets no Cloudflare | UPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKEN |
| A.3 | Worker: importar @upstash/redis | cloudflare-worker/src/index.ts |
| A.4 | Worker: burst rate limit via Redis INCR+EXPIRE | substitui Map in-memory |
| A.5 | Worker: 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
| # | Tarefa | Arquivo |
|---|---|---|
| B.1 | Criar redis-client.ts wrapper | supabase/functions/_shared/redis-client.ts |
| B.2 | Adicionar secrets Supabase | UPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKEN |
| B.3 | checkRateLimit() → Redis INCR (fallback Postgres) | portal-security.ts |
| B.4 | registrarTentativa() → dual-write | portal-security.ts |
| B.5 | checkLockout() → Redis INCR + TTL 24h | portal-security.ts |
| B.6 | shouldSendAlert() → Redis SET + TTL 15min | portal-security.ts |
Nota: portal-security.ts (224 linhas) será refatorado nesta fase, separando:
portal-security-constants.ts— constantes e thresholdsportal-security-redis.ts— implementações Redis com fallbackportal-security.ts— re-exports (backward compat)
Fase C — Token Blacklist em Redis
| # | Tarefa | Arquivo |
|---|---|---|
| C.1 | Logout: SET olp:bl:{jti} + Postgres INSERT | logout/index.ts |
| C.2 | Auth check: blacklist via Redis GET (fallback Postgres) | _shared/auth-helpers.ts |
| C.3 | Portal auth: blacklist via Redis | _shared/jwt-portal.ts |
| C.4 | Após 2 semanas: drop tabela token_blacklist | Migration SQL |
| C.5 | Remover cleanup de blacklist do maintenance-cron | maintenance-cron/index.ts |
Fase D — Sessões Ativas em Redis
| # | Tarefa | Arquivo |
|---|---|---|
| D.1 | Login: HSET olp:sess:{jti} + SADD olp:user-sess:{id} | verify-otp/index.ts, select-role/index.ts |
| D.2 | Logout: DEL sess + SREM user-sess | logout/index.ts |
| D.3 | Admin list: SCAN olp:sess:* + HGETALL | Nova Edge Function |
| D.4 | Admin revoke: DEL sess + SET bl + SREM user-sess | Nova Edge Function |
| D.5 | Invalidação pós-senha: SMEMBERS user-sess → revoke all | change-password/index.ts |
| D.6 | Frontend: painel de sessões ativas | src/components/admin-sessoes.tsx |
Fase E — Cleanup
| # | Tarefa | Condição |
|---|---|---|
| E.1 | Drop portal_alert_cooldown | Fase B validada (2 sem) |
| E.2 | Drop token_blacklist | Fase C validada (2 sem) |
| E.3 | Simplificar maintenance-cron | Remover tarefas migradas |
| E.4 | portal_login_tentativas → write-only | Manter para analytics, remover reads |
| E.5 | Avaliar drop portal_rate_metrics | Se métricas Redis suficientes |
7. Dívidas Técnicas — Inventário
Prioridade 1 — Resolvidas pelo Redis
| ID | Dívida | Solução Redis |
|---|---|---|
| P1 | Invalidação de sessão pós-troca-senha não revoga todas | olp:user-sess:{id} → revoke all |
| P2 | Burst rate limit usa Map in-memory (perde entre deploys) | Redis INCR persistente |
| P3 | Alert cooldown por isolate (não cross-instance) | Redis SET com TTL |
| P4 | Token blacklist precisa de cron cleanup | TTL automático |
Prioridade 2 — Melhorias com Redis
| ID | Dívida | Benefício |
|---|---|---|
| P5 | Rate limit queries pesadas no pico (12h BRT) | Redis ~5ms vs Postgres ~40ms |
| P6 | Sem visibilidade de sessões ativas | Painel admin com SCAN |
| P7 | Lockout check faz query em 24h de dados | Redis counter com TTL |
Prioridade 3 — Independentes do Redis
| ID | Dívida | Status |
|---|---|---|
| P8 | Papéis bloqueados (pedagógico, professor, marketing) precisam de UI | Backlog |
| P9 | Portal: abandonment rate alto (324 lookups vs 185 logins) | Investigar UX |
| P10 | WhatsApp (Wasender) retry/dead-letter queue | Backlog |
8. Estimativa de Custo Upstash
Comandos por request autenticado
| Operação | Comandos |
|---|---|
| Blacklist GET | 1 |
| Rate limit (INCR + EXPIRE) | 2-3 |
| Session check | 1 |
| Métricas INCR | 1-2 |
| Total por request | 5-8 |
Projeção de custo
| Escala | Requests/dia | Comandos/dia | Custo/dia | Custo/mês |
|---|---|---|---|---|
| Atual (2 escolas, 93 alunos) | ~500-1.5k | ~2.5k-12k | Free 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
| Risco | Impacto | Mitigação |
|---|---|---|
| Upstash indisponível | Rate limits e blacklist falham | Fallback Postgres (§5.3) |
| TTL não configurado em chave | Memory leak no Redis | Lint rule: toda chave DEVE ter EXPIRE |
redis.keys() usado acidentalmente | Bloqueio em produção | Code review + grep CI |
| Dual-write inconsistência | Dados divergem Redis/Postgres | Monitorar contagem por 2 semanas |
| Custo escala inesperada | Billing surpresa | Alert no Upstash dashboard > 500k cmd/dia |
10. Documentação a Atualizar
| Documento | Mudança | Fase |
|---|---|---|
docs/architecture/CLOUDFLARE_WORKER_GATEWAY.md | Seção Redis, variáveis, fluxo burst | A |
docs/architecture/CLOUDFLARE_WORKER_CODE.md | Imports Upstash, burst Redis | A |
docs/security/RATE_LIMITS.md | Reescrever — Redis substitui Postgres | B |
docs/security/AUTHENTICATION.md | Blacklist via Redis, sessões | C |
docs/operations/CRON_JOBS.md | Remover tarefas migradas | E |
docs/operations/DATABASE_CLEANUP.md | Tabelas removidas | E |
docs/operations/SESSION_MANAGEMENT_ALERTS.md | Substituído por Redis | D |
docs/operations/ANOMALY_DETECTION.md | Dependência Redis | B |
docs/README.md | Upstash no stack | A |
NOVO: docs/architecture/UPSTASH_REDIS.md | Arquitetura, key schema, TTLs, fallback, SCAN policy | A |
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.tscriado 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-sesstestado
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