Overview
Authenticated rate limits now use a server-side reputation layer. The layer scores users, sessions, API keys, IPs, CIDRs, and user-location pairs from auditable signals, then returns a coarse risk tier and rate-limit multiplier to the API and PostgREST guards. The policy is intentionally server-only. The repository is open source, so do not move scoring thresholds, bypass rules, or detailed reason codes into client code, response headers, or public docs. User-agent, missing-header, and request-shape checks are weak signals; they can raise caution but cannot earn trust by themselves.Trust Tiers
| Tier | Operator meaning | Rate-limit behavior |
|---|---|---|
trusted | Sustained low-risk, organic usage with no recent abuse | Higher authenticated budgets |
standard | Normal authenticated activity or fail-safe default | Current authenticated budgets |
watch | Suspicious but not high-confidence abuse | Standard or slightly lower budgets |
challenge_required | Medium-risk browser mutation activity | Browser mutation routes require Turnstile step-up |
restricted | High-risk activity or manual restriction | Stricter budgets and normal abuse blocks still apply |
API Abuse IP Blocks
Session-authenticated routes may defer proxy-sideapi_abuse IP blocks only
long enough to validate route authentication. Do not treat bearer-shaped tokens,
ttr_ strings, Supabase auth cookie names, or app-session cookie presence as
proof of an authenticated session. If the route auth check fails, return the
original block response before invoking the handler or recording normal auth
failure side effects.
Signals
The layer records rolling signals for:- rate-limit hits, failed auth, repeated
4xx,401,403, and429 - payload abuse and automation-like clients
- missing or scripted browser headers as weak negative signals
- older accounts, stable sessions, successful organic usage, and passed challenges as positive signals
- admin overrides and override revocations
Backend 429 Cascades
Supabase Auth and PostgREST/backend429 responses are availability signals,
not user-scoped abuse proof. Handlers may return 429 to the caller and seed a
bounded proxy/IP throttle when the client IP is known, but a single backend rate
limit must not create or extend user_suspensions for the authenticated user.
User suspension requires manual operator action or repeated, user-attributable
abuse counters, and automated suspensions should be expiry-bound unless a
separate policy explicitly justifies permanence.
Shared-IP Login Traffic
English centers, classrooms, and offices often put many legitimate teachers behind one public NAT IP. The proxy guard therefore keeps password login, OTP send, and OTP verify on separate auth buckets instead of sharing the generic mutation budget:| Policy | Default minute/hour/day budget | Override variables |
|---|---|---|
password-login | 60 / 600 / 4000 | API_PROXY_PASSWORD_LOGIN_LIMIT_MINUTE, API_PROXY_PASSWORD_LOGIN_LIMIT_HOUR, API_PROXY_PASSWORD_LOGIN_LIMIT_DAY |
otp-send | 30 / 180 / 300 | API_PROXY_OTP_SEND_LIMIT_MINUTE, API_PROXY_OTP_SEND_LIMIT_HOUR, API_PROXY_OTP_SEND_LIMIT_DAY |
otp-verify | 60 / 600 / 4000 | API_PROXY_OTP_VERIFY_LIMIT_MINUTE, API_PROXY_OTP_VERIFY_LIMIT_HOUR, API_PROXY_OTP_VERIFY_LIMIT_DAY |
ABUSE_OTP_SEND_IP_LIMIT_MINUTE, ABUSE_OTP_SEND_IP_LIMIT_HOUR, and
ABUSE_OTP_SEND_IP_LIMIT_DAY. Keep the per-email cooldown/hour/day limits in
place; they are the primary protection against repeated sends to the same
mailbox. Human auth route 429 responses should return Retry-After to the
caller and must not be converted into api_abuse IP blocks by the proxy
escalation path. Generic anonymous API route-limit hits can still escalate.
Step-Up Challenges
Browser mutations with medium risk should require Turnstile through the existing server verification path. The API returns a generic challenge-required response without exposing the exact scoring rules. API keys, CLI calls, cron jobs, webhooks, and native clients do not receive browser challenges; they rely on token, workspace, and API-key reputation instead.Local E2E Isolation
The Playwright rate-limit suite intentionally exhausts budgets and records 429 signals. ItsresetDbRateLimits() helper must clear both PostgREST rate-limit
counters and generated adaptive abuse state (abuse_activity_signals,
abuse_step_up_challenges, and abuse_reputation_subjects) before each spec.
The local dev-session endpoint also supports resetRateLimits: true to clear
the app process’s in-memory rate-limit fallback during E2E setup. Specs that
intentionally hit authenticated route limits should also use a fresh local
dev-session account per test/retry so Redis-backed user keys and asynchronous
adaptive reputation writes cannot bleed into the next case.
Admin Investigation
Use Infrastructure -> Abuse Intelligence to inspect:- trusted, watched, and restricted subject counts
- recent signals and coarse reason codes
- challenge pass/fail trends
- risky users, sessions, API keys, IPs, and CIDRs
- active manual overrides
- Check whether the subject has recent rate-limit, auth-failure, or payload abuse signals.
- Compare the subject with related location and user-location entries.
- Review challenge outcomes and recent route diversity before granting trust.
- Add a time-bound override only when the activity is clearly organic.
- Revoke trust immediately if the subject later shows scripted behavior, account takeover indicators, or noisy API-key traffic.
api_abuse or password_login_failed. If the activity is confirmed
organic, clear the active false-positive block or reset the affected counters;
do not relax the per-email OTP cooldown or failed-attempt protections.
Manual overrides require a reason and are written to the audit signal stream.
Prefer expiry-bound overrides for trust and watch decisions so stale operational
judgment does not become permanent policy.