Skip to content

feat(supabase): Document, monitor, and rotate Upstash Redis (SMI-5769)#1972

Merged
wrsmith108 merged 3 commits into
mainfrom
chore/smi-5769-upstash-redis-hardening
Jul 20, 2026
Merged

feat(supabase): Document, monitor, and rotate Upstash Redis (SMI-5769)#1972
wrsmith108 merged 3 commits into
mainfrom
chore/smi-5769-upstash-redis-hardening

Conversation

@wrsmith108

Copy link
Copy Markdown
Member

Business Summary

What shipped: Upstash Redis (the backing store for API rate limiting, quota caching, and circuit-breaker state) is now documented — a setup/architecture/rotation runbook plus .env.schema entries that were missing since the feature originally shipped. The public /health endpoint now actually checks Redis (it claimed to in its own docstring but never did), and the new public status page gets Redis-backed rate limiting as a 7th monitored component, alongside the existing 6.

Quality bar: Implementation + adversarial security review before this PR opened. The review caught and fixed a real bug pre-merge: the health check would have permanently reported "degraded" against a perfectly healthy Redis, due to a type mismatch in how Upstash's client deserializes values. It also caught a vendor-name leak (an internal "Upstash"/"Redis" mention that would have flowed into a status-page column a future wave makes public) and a test-loading regression this change itself introduced. All fixed before commit. 89 tests pass across the touched files.

Found along the way: the ~6-month-old prod Upstash credential is stale and was excluded from a recent broader secrets rotation with no recorded reason — rotation procedure is documented in the new runbook, but the actual rotation needs a human in the Upstash console and is tracked as a follow-up step on SMI-5769, not done in this PR. Also filed separately: SMI-5770 (a CI check to catch future .env.schema gaps like this one) and SMI-5772 (now closed — a test-mock gap this PR introduced and fixed in the same commit).

Net result: Docs, health-check monitoring, and the status-page component are fully shipped. Credential rotation is documented but not yet performed — needs a human with Upstash console access.


Technical detail

  • .env.schema: UPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKEN, RATE_LIMIT_FAIL_CLOSED — previously read in code (since SMI-1231) but never declared.
  • supabase/functions/_shared/rate-limiter.ts / new rate-limiter.health.ts: checkRedisHealth() — pipelined SET+GET+DEL round trip, never throws, never worse than degraded. Split into a sibling file to stay under the 500-line convention (mirrors the existing rate-limiter.types.ts split). New rate-limiter.health.test.ts covers the not-configured, healthy, mismatch, and error-redaction paths, plus a regression guard for the numeric-sentinel bug caught in review.
  • supabase/functions/health/index.ts: wires checkRedisHealth() into the live checks array.
  • New migration 20260719030000_status_component_rate_limiting.sql (+ rollback): seeds a 7th status_components row, public name "API Rate Limiting" (capability-framed, matching the existing 6 — never "Upstash"/"Redis" in public-facing columns).
  • supabase/functions/_shared/health-checks.probes.ts: checkRateLimitingStatus(), reusing checkRedisHealth() as its raw probe; message is translated to vendor-neutral text (raw detail kept in metadata.detail for engineer debugging).
  • supabase/functions/status-check/index.ts: wires the 7th component into COMPONENT_SLUGS + checkers.
  • health-checks.{helpers,probes,readers}.test.ts: added the vi.mock('https://esm.sh/@upstash/...') treatment these needed once the barrel (health-checks.ts) started transitively loading rate-limiter.ts's real esm.sh imports.
  • Docs: new docs/internal/runbooks/upstash-redis-operations.md (docs/internal submodule, already merged as skillsmith-docs#485), guards-and-opt-outs.md backfill row for RATE_LIMIT_FAIL_CLOSED, smoke-prod-guide.md health-row update, CLAUDE.md sub-doc table row.
  • Linear: SMI-5769 (main tracker), SMI-5768 (status-page component), SMI-5770 (audit-standards follow-up, not implemented here — ADR-109-gated), SMI-5772 (test-mock gap, fixed in this same commit).
  • ADR-109 note: a follow-up piece (wiring the Redis check into scripts/smoke-prod/website.sh/surfaces.json's automated pass/fail) is intentionally not in this PR — that's a CI/dev-tooling script change requiring SPARC + plan-review first, per this repo's Infrastructure Change Policy.

Upstash Redis backs rate-limiter.ts, quota-enforcer.ts, tier-cache.ts, and
circuit-breaker.ts but was undocumented in .env.schema and had zero
monitoring — health/index.ts's own docstring claimed to check 'Rate
limiter status' but never did. Verified directly against prod that the
credentials are also ~6 months stale, excluded from a broader Supabase
secrets rotation with no recorded rationale.

- Add UPSTASH_REDIS_REST_URL/TOKEN and RATE_LIMIT_FAIL_CLOSED to
  .env.schema, following the existing per-vendor block convention.
- Add checkRedisHealth() (pipelined Upstash SET+GET+DEL round trip,
  never throws, never worse than 'degraded') and wire it into the
  live /health endpoint's checks array, closing the docstring/code
  mismatch. Split into a new rate-limiter.health.ts sibling file to
  keep rate-limiter.ts under the 500-line convention, mirroring the
  existing rate-limiter.types.ts split.
- Add Redis-backed rate limiting as a 7th public status-page
  component (SMI-5768): new migration seeding a 'rate-limiting' row
  with a capability-framed public name ('API Rate Limiting', never
  'Upstash'/'Redis' — matches the existing 6 components' naming),
  checkRateLimitingStatus() in health-checks.probes.ts reusing
  checkRedisHealth() as its raw probe, wired into status-check's
  COMPONENT_SLUGS + checkers.
- Update smoke-prod-guide.md's health surface + failure-triage
  Classify section, and CLAUDE.md's sub-doc table.
- Bump the docs/internal submodule pointer for the new
  upstash-redis-operations.md runbook (architecture, why a scoped
  read-only token isn't viable, credential rotation procedure and
  180-day cadence) and a guards-and-opt-outs.md backfill row for the
  pre-existing RATE_LIMIT_FAIL_CLOSED guard (skillsmith-docs#485).

Adversarial review (before this commit) caught and fixed two issues:
a numeric health-check sentinel round-tripped through Upstash's
JSON.parse-based deserialization as a number, not the string that was
written, so the round trip would ALWAYS report 'degraded' against a
healthy Redis; and checkRateLimitingStatus() was passing Upstash's raw
message (containing 'Redis'/'Upstash') into status_checks.message,
which Wave 4/5 will eventually expose on the public status page,
undoing the migration's own vendor-neutral naming discipline. Both
fixed; regression test added in rate-limiter.health.test.ts.

Also fixes a test-loading regression this change itself introduced:
importing checkRedisHealth into health-checks.probes.ts pulled rate-
limiter.ts's real (non-type-only) esm.sh imports into the
health-checks.ts barrel, which health-checks.{helpers,probes,readers}
.test.ts all import through — none had the vi.mock() treatment for
those esm.sh specifiers, so all three failed to load. Added the same
mock pattern already used in rate-limiter.test.ts/health/index.test.ts.

Refs SMI-5769, SMI-5768
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

E2E Test Results

E2E Test Results - July 20, 2026

Summary

  • Status: ✅ PASSED
  • Total Duration: 0.00s
  • Generated: 2026-07-20T06:00:18.542Z

Test Results

Phase Status Duration
CLI E2E ⏭️ Skipped -
MCP E2E ⏭️ Skipped -

Generated by skillsmith E2E test suite

@github-actions

Copy link
Copy Markdown

E2E Test Results

E2E Test Results - July 20, 2026

Summary

  • Status: ✅ PASSED
  • Total Duration: 0.00s
  • Generated: 2026-07-20T18:01:23.706Z

Test Results

Phase Status Duration
CLI E2E ⏭️ Skipped -
MCP E2E ⏭️ Skipped -

Generated by skillsmith E2E test suite

@wrsmith108
wrsmith108 merged commit 1823a18 into main Jul 20, 2026
43 checks passed
@wrsmith108
wrsmith108 deleted the chore/smi-5769-upstash-redis-hardening branch July 20, 2026 18:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant