Skip to content

Add per-Stellar-address rate limiting alongside existing IP rate limiting #609

@BigBen-7

Description

@BigBen-7

Description

The current rate limiting on POST /gists is IP-based only via ThrottlerGuard. Since GistPin is a Web3 app where users identify with Stellar addresses, we need a second rate limiting layer that caps how many gists a single Stellar address can post per hour — regardless of IP address.

Context

  • Current: ThrottlerGuard limits by IP at 10 requests per minute
  • Needed: additional limit per authorAddress — 20 gists per hour per Stellar address
  • Anonymous posts with no authorAddress are still limited by IP only
  • Use an in-memory Map for tracking — no Redis required

Requirements

  • Create a custom StellarAddressThrottlerGuard implementing CanActivate
  • Extract authorAddress from the request body
  • Track post count per address using an in-memory Map
  • Reset counters every hour using Date.now()
  • Skip throttling if authorAddress is not present — anonymous posts pass through
  • Return 429 with message: Rate limit exceeded for this Stellar address
  • Make the limit configurable via STELLAR_RATE_LIMIT_PER_HOUR env var, default 20
  • Log when rate limit is hit — log a hash of the address, not the full address

Files to Touch

  • Backend/src/common/guards/stellar-throttler.guard.ts — create custom guard
  • Backend/src/gists/gists.controller.ts — apply @UseGuards on the POST route
  • Backend/src/config/configuration.ts — add STELLAR_RATE_LIMIT_PER_HOUR
  • Backend/.env.example — add STELLAR_RATE_LIMIT_PER_HOUR=20

Guard Structure Reference

@Injectable()
export class StellarAddressThrottlerGuard implements CanActivate {
  private readonly counts = new Map<string, { count: number; resetAt: number }>();

  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    const address = request.body?.authorAddress;
    if (!address) return true;

    const limit = parseInt(process.env.STELLAR_RATE_LIMIT_PER_HOUR ?? '20');
    const now = Date.now();
    const entry = this.counts.get(address);

    if (!entry || now > entry.resetAt) {
      this.counts.set(address, { count: 1, resetAt: now + 3_600_000 });
      return true;
    }
    if (entry.count >= limit) return false;
    entry.count++;
    return true;
  }
}

Acceptance Criteria

  • 21st post from the same Stellar address within 1 hour returns 429
  • Anonymous posts are unaffected by this guard
  • IP-based throttling still works independently
  • Rate limit resets after 1 hour
  • 429 response includes time until reset in minutes
  • Unit tests cover limit hit, anonymous, and reset scenarios

Complexity: 250 points

Metadata

Metadata

Assignees

No one assigned

    Labels

    BackendBackend issues

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions