Skip to content

feat(backend): replace in-memory challenge store with Redis-backed persistence#282

Merged
0xDeon merged 1 commit into
Suncrest-Labs:mainfrom
Salmatcre8:feature/issue-253-redis-challenge-store
Apr 23, 2026
Merged

feat(backend): replace in-memory challenge store with Redis-backed persistence#282
0xDeon merged 1 commit into
Suncrest-Labs:mainfrom
Salmatcre8:feature/issue-253-redis-challenge-store

Conversation

@Salmatcre8
Copy link
Copy Markdown
Contributor

Summary

  • Introduces a ChallengeStore interface with Set / GetAndDelete that authService delegates to — removing the sync.RWMutex + map it previously owned
  • RedisChallengeStore uses GETDEL (atomic get-and-delete) so a challenge can only be verified once even under concurrent requests; TTL is enforced by Redis natively
  • InMemoryChallengeStore is retained as a single-instance fallback (and for tests) — active when REDIS_ADDR is empty
  • main.go builds the store at startup and logs which backend is active
  • docker-compose.yml adds a Redis 7 Alpine service; the API service waits for it to be healthy before starting
  • apps/api/.env.example documents REDIS_ADDR

Why each problem is now fixed

Scenario Before After
Server restart mid-auth Challenges lost; user sees "challenge not found" Challenges survive in Redis
Load-balanced instances Instance A stores challenge; Instance B can't find it Both instances share Redis
Challenge-flood DoS Unbounded map growth → OOM Redis enforces TTL; entries evict automatically

Test plan

  • InMemoryChallengeStore: 6 unit tests (set/get, one-time use, missing key, expiry, overwrite, wallet isolation)
  • authService tests updated to pass a store; all 6 existing tests still pass logic-unchanged
  • GOOS=linux go build ./... clean
  • GOOS=linux go test -c ./internal/service/ compiles with no errors

Closes #253

Fixes three problems in the wallet auth flow:
- Server restarts wiped all pending challenges, forcing users to retry
- Horizontal scaling caused cross-instance auth failures (instance A
  generated the challenge; instance B couldn't verify it)
- Unbounded map growth allowed challenge-flood DoS by OOM

Changes:
- Introduces a ChallengeStore interface (Set / GetAndDelete) in internal/service
- RedisChallengeStore uses GETDEL (atomic get-and-delete, prevents replay)
  and lets Redis enforce the TTL natively
- InMemoryChallengeStore is the single-instance fallback when REDIS_ADDR is
  unset; also used in all unit tests
- authService no longer owns a sync.RWMutex map; it delegates to the store
- config: optional REDIS_ADDR env var (empty = in-memory)
- main.go selects the store at startup and logs which backend is active
- docker-compose.yml adds a Redis 7 service; API depends on it being healthy
- .env.example documents REDIS_ADDR

Closes Suncrest-Labs#253
@Salmatcre8 Salmatcre8 requested a review from 0xDeon as a code owner April 23, 2026 16:47
@drips-wave
Copy link
Copy Markdown

drips-wave Bot commented Apr 23, 2026

@Salmatcre8 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

Copy link
Copy Markdown
Contributor

@0xDeon 0xDeon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lgtm

@0xDeon 0xDeon merged commit 7f41396 into Suncrest-Labs:main Apr 23, 2026
6 checks passed
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.

[Backend] Auth challenge store is in-memory — lost on restart, unsafe under load balancing, no TTL eviction

2 participants