ubiquitous-invention/plans/Plan-multitenant-saas-hardening/Epic-tenant-lifecycle/Task-rate-limit-and-abuse-guardrails.md
Randall Stillwell 778fe1d321 plans: scaffold daily-driver-finish, saas-hardening, agent-coordination
Three new plan trees that fill in the gaps surfaced during repo review.
Together they map out what remains between the current scaffold-with-stubs
state and a daily-usable, multitenant, agent-coordinated app.

* Plan-daily-driver-finish (P0): turn stubs into real data. Five tasks
  covering the lint/shared-types breakage, hardcoded dashboard mocks,
  AI-page setTimeout placeholder, post-signin landing decision, and a
  cross-browser collab smoke test against the deployed Hocuspocus
  instance.

* Plan-multitenant-saas-hardening (P1): everything multitenant needs
  beyond what Plan-multitenant-cursor-sync already covers. Invites and
  role management, soft-delete + append-only audit log, rate limits on
  the auth + mutation hot paths, and a Vitest + GitHub Actions test
  foundation so PRs can't ship red.

* Plan-agent-coordination (P2): the layer that makes a Task-*.md
  runnable, not just readable. Adds workflow_prompt with task -> epic
  -> plan inheritance, an agent_runs table for auditable sessions, and
  two new MCP tools (claim_task / complete_task) that replace the
  freeform update_object composition agents do today. Includes an
  intentionally-deferred Epic-optional-orchestrator that captures the
  Symphony-shaped runner as a decision point rather than an immediate
  build.

Each task is bead-scale (one focused Cursor session) with explicit
in-scope, out-of-scope, and anti-goal sections so a future agent can
pick up a single Task-*.md and start without scrollback context.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 23:52:22 -05:00

3.4 KiB

kind slug title plan_slug epic_slug status priority tenant_id owner cursor_todo_id updated_at
task rate-limit-and-abuse-guardrails Rate-limit credentials sign-in and high-impact mutations multitenant-saas-hardening tenant-lifecycle ready P2 global unassigned null 2026-06-01

Task summary

Add per-IP rate limits to credentials sign-in and to a small set of high-impact mutation endpoints (invite, archive, create-workspace). The goal is to cap brute-force and obvious abuse, not to be a full WAF.

Description

Storage

Use the existing Redis instance on CT 102 (already used by Hocuspocus for awareness). Add a thin client in packages/shared/src/rate-limit/ — token-bucket or sliding-window, your choice; the contract is consume(key: string, opts: { capacity, refillPerSecond }): Promise<{ ok: boolean, remaining: number, retryAfterMs: number }>.

If you don't want a new shared lib, @upstash/ratelimit is a fine drop-in even though we're not on Upstash — it works against any Redis URL. Don't add a new database for this.

Where to apply

  1. Credentials sign-in (apps/web/app/api/auth/[...nextauth]/route.ts, or the authorize callback): key by IP (x-forwarded-for, falling back to remote addr). 5 attempts / 60 seconds, then 429.
  2. Invite create: key by actor_user_id. 30 invites / hour. Workspace-scoped is fine too.
  3. Workspace create: key by actor_user_id. 5 workspaces / hour.
  4. Archive/restore: key by (workspace_id, actor_user_id). 20 / hour. Cheap insurance against a script flipping state in a loop.

IP extraction

Use the same helper everywhere; don't recompute in every route. Common gotcha: x-forwarded-for is a comma-separated list when there are multiple proxies. Take the first entry. If the deployed stack is behind Cloudflare, cf-connecting-ip is more reliable — add that as a higher-priority source if present.

Response

When a limit trips, return 429 Too Many Requests with a Retry-After header in seconds and a tRPC TOO_MANY_REQUESTS error code. The client should surface a friendly message ("try again in ~30 seconds"), not a stack trace.

Audit logging

Every limit trip writes an audit_log row (action: "rate_limit.tripped", metadata: { route, key_kind, retry_after_ms }). This is how you discover whether anyone's hitting the limits in practice; without it the limits are silent.

Anti-goals

  • Don't add a CAPTCHA. If brute-forcing becomes a real problem, add Cloudflare in front of the deploy.
  • Don't write a generic rate-limit middleware that wraps every tRPC procedure. The set of high-value endpoints is short; explicit is better than universal.

Subtasks

  • Pick the Redis client and rate-limit lib (or hand-roll).
  • Add IP extraction helper.
  • Apply limits to the 4 routes above.
  • Wire audit_log writes on limit trips.
  • Verify by scripting 20 credentials sign-in attempts against a dev deploy.

Owner or assignee

Unassigned

Status

ready

Estimation

M

Acceptance criteria

  • Brute-forcing credentials sign-in trips at attempt 6 within 60 seconds.
  • Invite-spam attempt trips at invite 31 within an hour.
  • Every trip produces an audit_log row.
  • No global tRPC middleware — limits are applied per route.
  • Epic: ./Epic-tenant-lifecycle.md
  • Plan: ../Plan-multitenant-saas-hardening.md