Skip to content

Replace file-based webhook idempotency storage with a pluggable, multi-instance-safe store #259

Description

@Lakes41

Difficulty: Advanced
Type: Refactor

Background
.env.example documents ACTIVITY_STORAGE_MODE (memory default, or file to "persist webhook IDs across local restarts") and ACTIVITY_STORAGE_DIR (default .guildpass-activity), explicitly described as "Webhook activity idempotency storage." This is the mechanism preventing the same incoming webhook (member.joined, pass.activated, etc.) from being processed twice.

Problem
Both memory and file idempotency stores are single-instance by design: memory is lost on restart, and file writes to local disk, which does not work correctly if the dashboard is deployed across multiple server instances or serverless/ephemeral-filesystem environments — a duplicate webhook delivered to a different instance than the one that first processed it would not be recognized as a duplicate, breaking the "idempotency" guarantee the naming implies.

Expected outcome
The webhook idempotency store is defined behind an interface (e.g. IdempotencyStore with hasSeen(id)/markSeen(id, ttl)), with memory and file kept as local-dev implementations, plus a new implementation suitable for multi-instance deployments (e.g. backed by the durable Postgres storage from Issue #6, or a documented Redis-compatible option) selectable via ACTIVITY_STORAGE_MODE.

Suggested implementation

  • Extract the current idempotency logic (wherever it lives, e.g. near the webhook handler) into a small interface with pluggable backends.
  • Add a durable (or postgres) mode that stores seen webhook IDs with a TTL/expiry column, reusing the Postgres connection if Issue Add a server-wide role sync command for admins #6 is implemented, or with its own minimal table otherwise.
  • Ensure TTL-based cleanup doesn't require a background cron — expire lazily on lookup or via a scheduled cleanup query, whichever fits the existing architecture better, and document the choice.
  • Update .env.example and CONTRIBUTING.md to describe the new mode and when to use it.

Acceptance criteria

  • memory and file modes retain their exact current behavior for local development (no regression).
  • A new multi-instance-safe mode correctly rejects a duplicate webhook ID even when the "duplicate" request is simulated as coming from a second, independent process/instance.
  • Idempotency records expire (or are cleaned up) after a documented retention window rather than growing unboundedly.
  • Tests cover: first-seen acceptance, duplicate rejection, and expiry behavior for each backend.
  • pnpm --filter @guildpass/dashboard test and pnpm typecheck pass.

Likely affected files/directories
Webhook handler route (apps/dashboard/app/api/webhook*/), new idempotency-store module (e.g. apps/dashboard/lib/idempotency/), .env.example, CONTRIBUTING.md.

Metadata

Metadata

Assignees

Labels

GrantFox OSSGrantFox Open Source Sponsorship program tagMaybe RewardedIssue may qualify for a reward upon successful completion per campaign rulesOfficial Campaign | FWC26Official FWC26 campaign issue — eligible for campaign scoring and rewardsarchitectureStructural / design-level changeenhancementNew feature or requestscalabilityScale / multi-chain / load

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions