|
| 1 | +# Redis Architecture — NiffyInsure Backend |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +Redis underpins three operational concerns: |
| 6 | + |
| 7 | +| Concern | Implementation | Fail behaviour | |
| 8 | +|---------|---------------|----------------| |
| 9 | +| Job queues | BullMQ (`claim-events`, `claim-payouts`) | **Fail closed** — job not enqueued returns error | |
| 10 | +| Wallet-auth nonces | `setNonce` / `consumeNonce` in `cache.ts` | **Fail closed** — auth rejected if Redis is down | |
| 11 | +| Rate limiting | `incrementRateLimit` in `cache.ts` | **Fail open** — request allowed, warning logged | |
| 12 | +| Response caching | `cacheGet` / `cacheSet` in `cache.ts` | **Degrade gracefully** — cache miss falls through to DB | |
| 13 | + |
| 14 | +**Redis is never the authoritative store for financial data. Postgres is.** |
| 15 | + |
| 16 | +--- |
| 17 | + |
| 18 | +## Key Naming Conventions |
| 19 | + |
| 20 | +All keys are prefixed with `{NODE_ENV}:niffyinsure:` (set as `keyPrefix` in ioredis). |
| 21 | + |
| 22 | +``` |
| 23 | +{env}:niffyinsure:cache:policy:{holder}:{policy_id} — policy read cache (30 s TTL) |
| 24 | +{env}:niffyinsure:cache:claim:{claim_id} — claim read cache (10 s TTL) |
| 25 | +{env}:niffyinsure:nonce:{address} — wallet-auth nonce (5 min TTL) |
| 26 | +{env}:niffyinsure:ratelimit:{identifier} — rate-limit counter (60 s TTL) |
| 27 | +{env}:niffyinsure:bull:claim-events:* — BullMQ internal keys |
| 28 | +{env}:niffyinsure:bull:claim-payouts:* — BullMQ internal keys |
| 29 | +``` |
| 30 | + |
| 31 | +Segments: |
| 32 | +- `{env}` — `NODE_ENV` value (`development` | `staging` | `production`) |
| 33 | +- `niffyinsure` — service constant; prevents collisions in shared Redis |
| 34 | +- `{area}` — `cache` | `nonce` | `ratelimit` | `bull` |
| 35 | +- `{id}` — resource-specific identifier |
| 36 | + |
| 37 | +--- |
| 38 | + |
| 39 | +## TTL Conventions |
| 40 | + |
| 41 | +Defined in `src/redis/config.ts` as `TTL` — single source of truth. |
| 42 | + |
| 43 | +| Key area | TTL | Rationale | |
| 44 | +|----------|-----|-----------| |
| 45 | +| Nonce | 5 min | Challenge must be used before wallet session expires | |
| 46 | +| Rate limit | 60 s | Sliding window; resets each minute | |
| 47 | +| Policy cache | 30 s | Stale-while-revalidate acceptable; policies change infrequently | |
| 48 | +| Claim cache | 10 s | Lower TTL; claim status changes on every vote | |
| 49 | + |
| 50 | +--- |
| 51 | + |
| 52 | +## Queue Configuration |
| 53 | + |
| 54 | +### `claim-events` |
| 55 | + |
| 56 | +Processes Soroban contract events (ClaimFiled, VoteLogged, ClaimSettled) and writes to Postgres. |
| 57 | + |
| 58 | +| Setting | Value | Rationale | |
| 59 | +|---------|-------|-----------| |
| 60 | +| `attempts` | 5 | Retry transient failures (network, DB lock) | |
| 61 | +| `backoff` | exponential, 1 s base | Avoid thundering herd on DB recovery | |
| 62 | +| `concurrency` | 5 per worker | Balance throughput vs DB connection pool | |
| 63 | +| `stalledInterval` | 30 s | Redeliver if worker crashes mid-job | |
| 64 | +| `maxStalledCount` | 2 | Move to failed after 2 stall cycles | |
| 65 | +| `removeOnComplete` | last 100 | Keep for debugging without unbounded growth | |
| 66 | +| `removeOnFail` | last 500 | Keep for alerting and manual replay | |
| 67 | + |
| 68 | +**Idempotency requirement**: The Postgres writer must use `INSERT … ON CONFLICT DO NOTHING` keyed on `(ledger, event_index)` — stalled jobs will be redelivered. |
| 69 | + |
| 70 | +### `claim-payouts` |
| 71 | + |
| 72 | +Triggers token transfer for approved claims. Not yet implemented — queue name reserved. |
| 73 | + |
| 74 | +--- |
| 75 | + |
| 76 | +## Outage Behaviour |
| 77 | + |
| 78 | +### Redis completely unavailable |
| 79 | + |
| 80 | +| Feature | Behaviour | User impact | |
| 81 | +|---------|-----------|-------------| |
| 82 | +| Wallet auth (nonce) | **Rejected** — `RedisUnavailableError` thrown | User cannot log in; must retry when Redis recovers | |
| 83 | +| Rate limiting | **Allowed** — warning logged | Temporary rate-limit bypass; acceptable short-term risk | |
| 84 | +| Policy/claim reads | **DB fallback** — cache miss | Slightly higher DB load; no user-visible impact | |
| 85 | +| Job enqueue | **Error returned** — caller must handle | Async processing delayed; no data loss if caller retries | |
| 86 | +| `/health/ready` | Returns `503 { redis: "down" }` | Load balancer can route away from degraded instance | |
| 87 | + |
| 88 | +### Redis slow (high latency) |
| 89 | + |
| 90 | +- `checkRedisHealth` has a 2 s timeout — returns `false` if exceeded. |
| 91 | +- Cache operations have no explicit timeout; they will block the request. Consider adding per-operation timeouts in production if Redis latency is a concern. |
| 92 | + |
| 93 | +--- |
| 94 | + |
| 95 | +## Metrics and Alerting |
| 96 | + |
| 97 | +`GET /metrics/redis` returns: |
| 98 | + |
| 99 | +```json |
| 100 | +{ |
| 101 | + "connected": true, |
| 102 | + "memory_used_bytes": 1234567, |
| 103 | + "memory_used_mb": 1, |
| 104 | + "queues": { |
| 105 | + "claim-events": { |
| 106 | + "waiting": 0, |
| 107 | + "active": 1, |
| 108 | + "completed": 42, |
| 109 | + "failed": 0, |
| 110 | + "delayed": 0, |
| 111 | + "depth": 1 |
| 112 | + } |
| 113 | + } |
| 114 | +} |
| 115 | +``` |
| 116 | + |
| 117 | +Recommended alert thresholds: |
| 118 | + |
| 119 | +| Metric | Threshold | Action | |
| 120 | +|--------|-----------|--------| |
| 121 | +| `queues["claim-events"].depth` | > 1000 | Scale worker replicas | |
| 122 | +| `queues["claim-events"].failed` | > 10 | Investigate; replay failed jobs | |
| 123 | +| `memory_used_mb` | > 200 (of 256 limit) | Increase `maxmemory` or scale Redis | |
| 124 | +| `connected: false` | any | Page on-call; wallet auth is down | |
| 125 | + |
| 126 | +--- |
| 127 | + |
| 128 | +## Local Development |
| 129 | + |
| 130 | +```bash |
| 131 | +# Start Redis |
| 132 | +docker compose up -d redis |
| 133 | + |
| 134 | +# Set env vars (copy from .env.example) |
| 135 | +cp .env.example .env |
| 136 | + |
| 137 | +# Run backend |
| 138 | +npm run build && npm start |
| 139 | + |
| 140 | +# Run tests (Redis must be running) |
| 141 | +REDIS_HOST=127.0.0.1 npm test |
| 142 | +``` |
| 143 | + |
| 144 | +--- |
| 145 | + |
| 146 | +## Production Security Checklist |
| 147 | + |
| 148 | +- [ ] `REDIS_PASSWORD` set to ≥ 32 random characters |
| 149 | +- [ ] `REDIS_TLS=true` with valid CA cert for managed Redis (e.g. AWS ElastiCache, Upstash) |
| 150 | +- [ ] Redis not exposed on public network interface |
| 151 | +- [ ] `maxmemory` and `maxmemory-policy` configured (`allkeys-lru` recommended) |
| 152 | +- [ ] Separate Redis instance (or logical DB) per environment |
| 153 | +- [ ] Alerts wired on queue depth and memory usage |
| 154 | +- [ ] Redis password rotated on any suspected compromise |
0 commit comments