Rate Limiting

The "depends" project implements rate limiting to protect the API from abuse and provides usage quotas through a dedicated endpoint for monitoring resource consumption. Rate limiting applies globally to API requests, keyed by client IP address, while usage statistics offer namespace-specific metrics for billing, auditing, and self-regulation. These features distinguish between self-hosted and hosted modes, with rate limiting bypassed for local development.

Rate Limiting Mechanism

Rate limiting uses a simple in-memory sliding-window counter per IP address. Each window spans 1 minute (WINDOW_MS = 60_000), allowing up to 120 requests (MAX_REQUESTS = 120) before rejection. Exceeding the limit returns a 429 "Too Many Requests" response with a Retry-After header indicating seconds until the window resets.

Stale windows (expired beyond resetAt) are pruned every 5 minutes via a background setInterval. The implementation stores windows in a Map, where keys are IP addresses and Window tracks count and resetAt.

interface Window {
  count: number;
  resetAt: number;
}

const windows = new Map<string, Window>();

const WINDOW_MS = 60_000; // 1 minute
const MAX_REQUESTS = 120; // per window

/** Prune stale entries every 5 minutes */
setInterval(() => {
  const now = Date.now();
  for (const [key, w] of windows) {
    if (now > w.resetAt) windows.delete(key);
  }
}, 5 * 60_000);

/**
 * Returns a 429 Response if the IP has exceeded the limit, or null if allowed.
 */
export function rateLimit(ip: string): Response | null {
  const now = Date.now();
  let w = windows.get(ip);

  if (!w || now > w.resetAt) {
    w = { count: 0, resetAt: now + WINDOW_MS };
    windows.set(ip, w);
  }

  w.count++;

  if (w.count > MAX_REQUESTS) {
    return Response.json(
      { error: "Rate limit exceeded. Try again shortly." },
      {
        status: 429,
        headers: { "Retry-After": String(Math.ceil((w.resetAt - now) / 1000)) },
      },
    );
  }

  return null;
}

This design prioritizes simplicity and low overhead, suitable for the project's lightweight server. Limits are hardcoded, with no external configuration, reflecting a focus on default protection without user tuning.

Application in the Server

Rate limiting hooks into Elysia's onBeforeHandle middleware in src/server.ts, applied to all routes after public static files and before authentication. It extracts the client IP from X-Forwarded-For (first address) or the server's requestIP method (e.g., Cloudflare-specific).

Local requests—detected via self-hosted-mode.md or localhost IPs (127.0.0.1, ::1, etc.)—bypass limiting to support development and cli-commands.md usage.

.onBeforeHandle(({ request, server }) => {
  if (isLocalRequest(request, server)) return;
  const forwarded = request.headers.get("X-Forwarded-For");
  const ip = forwarded
    ? forwarded.split(",")[0].trim()
    : ((
        server as { requestIP?(req: Request): { address: string } | null }
      )?.requestIP?.(request)?.address ?? "unknown");
  return rateLimit(ip) ?? undefined;
});

In hosted-mode.md, this protects public endpoints like /v1/signup and namespace-scoped routes under /v1/namespaces/:namespace.

Usage Quotas and Statistics

Usage tracking provides monthly quotas via the GET /v1/usage/:namespace endpoint, accessible after authentication.md. It computes statistics from the database-schema.md:

The period uses the current UTC month-year (e.g., "2024-10").

export function handleGetUsage(
  db: Database,
  nsId: number,
  namespace: string,
  tokenId: number,
): Response {
  const now = new Date();
  const period = `${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, "0")}`;

  const activeNodes = db
    .query(
      `SELECT COUNT(DISTINCT node_id) as c FROM events WHERE ns_id = ? AND created_at >= datetime('now', 'start of month')`,
    )
    .get(nsId) as { c: number };

  // ... similar queries for totalEvents, totalNodes, webhookDeliveries, emailsSent

  const token = db
    .query("SELECT email FROM tokens WHERE id = ?")
    .get(tokenId) as { email: string | null } | null;

  return Response.json({
    email: token?.email ?? null,
    namespace,
    period,
    nodes: totalNodes.c,
    active_nodes: activeNodes.c,
    total_events: totalEvents.c,
    webhook_deliveries: webhookDeliveries.c,
    emails_sent: emailsSent.c,
  });
}

In hosted-mode.md, these metrics integrate with Legendum billing (via legendum_token in tokens table). Self-hosted users see stats for monitoring without enforcement.

CLI Integration

The depends usage command fetches and formats stats, supporting JSON output:

depends usage [namespace]

It displays totals like "Nodes: 5 total, 2 active this month" and conditional lines for webhooks/emails. See cli-commands.md for details.

Design Decisions

These mechanisms ensure API stability while providing transparency into quotas, fitting the project's dual self-hosted/hosted architecture. For full endpoints, see api-reference.md.

Recent changes