Global WatchGlobal Watch Docs
Architecture

Arquitetura do Rate Limiting

Arquitetura: API Rate Limiting & Security Headers

O sistema de rate limiting segue a arquitetura hexagonal do ForestWatch, com separação clara entre port (interface RateLimitStore) e adapter (InMemoryRateLimitStore). O pacote @fw/rate-limit é independente de framework e se integra ao Next.js através dos wrappers enhanceAction e enhanceRouteHandler do pacote @kit/next.

Diagrama de Arquitetura

┌─────────────────────────────────────────────────────────────────────┐
│                        Camada de Apresentação                       │
│                                                                     │
│  ┌──────────────┐  ┌──────────────────┐  ┌───────────────────────┐ │
│  │  API Routes   │  │  Server Actions   │  │  proxy.ts (middleware)│ │
│  │  /api/*       │  │  'use server'     │  │  Security Headers     │ │
│  └──────┬───────┘  └────────┬──────────┘  └───────────┬───────────┘ │
└─────────┼───────────────────┼─────────────────────────┼─────────────┘
          │                   │                         │
┌─────────┼───────────────────┼─────────────────────────┼─────────────┐
│         ▼                   ▼                         │             │
│  ┌──────────────────────────────────────┐             │             │
│  │        @kit/next (Wrappers)          │             │             │
│  │  enhanceRouteHandler  enhanceAction  │             │             │
│  │  ┌─────────────────────────────────┐ │             │             │
│  │  │ 1. Rate Limit Check             │ │             │             │
│  │  │ 2. CAPTCHA Verification         │ │             │             │
│  │  │ 3. Auth Check                   │ │             │             │
│  │  │ 4. Schema Validation            │ │             │             │
│  │  │ 5. Handler Execution            │ │             │             │
│  │  └─────────────────────────────────┘ │             │             │
│  └──────────────┬───────────────────────┘             │             │
└─────────────────┼─────────────────────────────────────┼─────────────┘
                  │                                     │
┌─────────────────┼─────────────────────────────────────┼─────────────┐
│                 ▼                                     ▼             │
│  ┌──────────────────────────────────────────────────────────────┐   │
│  │                    @fw/rate-limit                             │   │
│  │                                                              │   │
│  │  ┌─────────────────┐  ┌──────────────┐  ┌────────────────┐  │   │
│  │  │  rate-limiter.ts │  │  headers.ts  │  │ security-      │  │   │
│  │  │  (Core Logic)    │  │  (RL Headers)│  │ headers.ts     │  │   │
│  │  └────────┬─────────┘  └──────────────┘  └────────────────┘  │   │
│  │           │                                                   │   │
│  │  ┌────────▼──────────────────────────────────────────────┐   │   │
│  │  │  RateLimitStore (PORT - Interface)                     │   │   │
│  │  │  check(key, limit, windowMs) → RateLimitResult         │   │   │
│  │  └────────┬──────────────────────────────────────────────┘   │   │
│  │           │                                                   │   │
│  │  ┌────────▼──────────────────────────────────────────────┐   │   │
│  │  │  InMemoryRateLimitStore (ADAPTER)                      │   │   │
│  │  │  Map<string, WindowEntry>                              │   │   │
│  │  │  Cleanup automático a cada 60s, TTL de 5 minutos       │   │   │
│  │  └───────────────────────────────────────────────────────┘   │   │
│  └──────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────┘

Estrutura do Pacote

packages/fw/rate-limit/
├── src/
│   ├── index.ts                  # API pública (re-exports)
│   ├── rate-limit-store.ts       # Port: interface RateLimitStore
│   ├── in-memory-store.ts        # Adapter: InMemoryRateLimitStore
│   ├── rate-limiter.ts           # Core: tiers, resolveConfig, checkRateLimit
│   ├── client-identifier.ts      # Utilitário: extração de IP
│   ├── headers.ts                # Utilitário: headers de rate limit
│   ├── security-headers.ts       # Constantes e aplicação de security headers
│   ├── store-instance.ts         # Singleton do store
│   └── __tests__/                # 56 testes (PBT + unit + integration)
├── package.json
├── tsconfig.json
└── vitest.config.ts

Padrão Hexagonal: Port & Adapter

Port: RateLimitStore

A interface define o contrato para qualquer implementação de armazenamento de contadores:

export interface RateLimitResult {
  allowed: boolean;
  limit: number;
  remaining: number;
  resetAt: number;       // Unix timestamp (segundos)
  retryAfter?: number;   // Segundos até reset (só quando bloqueado)
}

export interface RateLimitStore {
  check(key: string, limit: number, windowMs: number): Promise<RateLimitResult>;
  cleanup(): void;
  destroy(): void;
}

Adapter: InMemoryRateLimitStore

Implementação baseada em Map com sliding window:

export class InMemoryRateLimitStore implements RateLimitStore {
  private windows: Map<string, WindowEntry> = new Map();
  // Cleanup automático a cada 60s, TTL de 5 min
}

Extensibilidade

Para adicionar um adapter Redis/Upstash no futuro, basta implementar a interface RateLimitStore:

// Exemplo futuro (não implementado)
export class UpstashRateLimitStore implements RateLimitStore {
  constructor(private redis: Redis) {}
  async check(key, limit, windowMs) { /* Redis INCR + EXPIRE */ }
  cleanup() { /* Redis TTL cuida disso */ }
  destroy() { /* Fecha conexão */ }
}

Fluxo de Dados: Route Handler

1. Requisição HTTP chega ao Next.js
2. proxy.ts aplica SECURITY_HEADERS
3. enhanceRouteHandler verifica se rateLimit está configurado
4. Resolve config: tier defaults + overrides
   → resolveRateLimitConfig({ tier: 'strict' }, 'route:/api/auth/check-email')
   → { maxRequests: 5, windowMs: 60000, keyPrefix: 'route:/api/auth/check-email' }
5. Extrai IP: x-forwarded-for → x-real-ip → '127.0.0.1'
6. Verifica rate limit no store (singleton)
   → store.check('route:/api/auth/check-email:10.0.0.1', 5, 60000)
7a. Bloqueado → 429 + headers RL
7b. Erro no store → Fail-open (continua)
7c. Permitido → Fluxo existente: CAPTCHA → Auth → Schema → Handler
8. Resposta com headers RL anexados

Fluxo de Dados: Server Action

1. Client invoca server action
2. enhanceAction verifica se rateLimit está configurado
3. Resolve config com tier defaults
4. Extrai IP via next/headers
5. Verifica rate limit
   → Bloqueado: throw Error('Too many requests...')
   → Erro no store: Fail-open (continua)
   → Permitido: Fluxo existente: Schema → CAPTCHA → Auth → Action

Padrão Singleton

O store é compartilhado entre todas as requisições:

let instance: RateLimitStore | null = null;

export function getRateLimitStore(): RateLimitStore {
  if (!instance) {
    instance = new InMemoryRateLimitStore();
  }
  return instance;
}

Garante que todos os endpoints compartilham o mesmo Map de contadores e o cleanup interval roda uma única vez.

Mecanismo de Cleanup

O InMemoryRateLimitStore executa limpeza automática para evitar vazamento de memória:

  • Intervalo: 60 segundos (configurável no construtor)
  • TTL: 5 minutos (generoso para cobrir qualquer janela)
  • Complexidade: O(n) onde n = número de entradas no Map
  • Comportamento: Remove entradas com now - startTime > 5 min

Pontos de Integração

PontoArquivoMudança
enhanceRouteHandlerpackages/next/src/routes/index.tsRate limit como primeiro passo
enhanceActionpackages/next/src/actions/index.tsRate limit como primeiro passo
proxy.tsapps/web/proxy.tsSecurity headers globais
api.config.tsapps/web/config/api.config.tsConfiguração centralizada de tiers

Decisões Arquiteturais

1. In-Memory vs Redis

CritérioIn-Memory (atual)Redis/Upstash (futuro)
Latência< 0.01ms~1-5ms
PersistênciaNão (reseta no deploy)Sim
Multi-instanceNão (por processo)Sim (compartilhado)
DependênciasNenhumaRedis client
CustoZeroVariável

Decisão: In-memory primeiro. O Vercel roda single-instance por função serverless, então o Map funciona bem.

2. Fail-Open vs Fail-Closed

Decisão: Fail-open. Disponibilidade é mais importante que enforcement estrito. Um rate limiter quebrado não deve derrubar a aplicação.

3. Opt-in vs Global

Decisão: Opt-in por endpoint. Preserva compatibilidade retroativa e permite controle granular.

4. Rate Limit antes de Auth

Decisão: Rate limit é verificado antes de autenticação. Protege contra ataques de força bruta que tentam sobrecarregar o sistema de auth.

5. Identificação por IP

Decisão: IP como identificador primário via headers. Simples e eficaz. User ID pode ser adicionado como identificador secundário no futuro.

Performance

OperaçãoComplexidadeLatência
store.check()O(1)< 0.01ms
getClientIdentifier()O(1)< 0.01ms
resolveRateLimitConfig()O(1)< 0.01ms
rateLimitHeaders()O(1)< 0.01ms
cleanup()O(n)Depende do Map
Total por requisiçãoO(1)< 0.1ms

Documentação Relacionada

On this page