Skip to content

fix: per-user and per-tenant rate limiting on AI endpoints#2904

Open
Nancy-3012 wants to merge 1 commit into
ritesh-1918:mainfrom
Nancy-3012:fix/per-user-rate-limiting
Open

fix: per-user and per-tenant rate limiting on AI endpoints#2904
Nancy-3012 wants to merge 1 commit into
ritesh-1918:mainfrom
Nancy-3012:fix/per-user-rate-limiting

Conversation

@Nancy-3012

@Nancy-3012 Nancy-3012 commented Jun 17, 2026

Copy link
Copy Markdown
  • Replace IP-based limiter key with user: for authenticated requests
  • Fallback to IP for unauthenticated requests
  • Add get_rate_limit_key() using extract_token() + supabase.auth.get_user()
  • Add log_ai_usage() to track calls in ai_usage_log table
  • Add check_tenant_quota() reading max_ai_requests_per_hour from system_settings
  • Apply quota check + usage logging to /ai/analyze and /ai/analyze_stream

Fixes #2903

Summary by CodeRabbit

  • New Features
    • Implemented per-company hourly limits on AI requests with quota enforcement.
    • Enhanced rate limiting that prioritizes authenticated user identity when available.
    • Added AI usage tracking and monitoring.

- Replace IP-based limiter key with user:<uuid> for authenticated requests
- Fallback to IP for unauthenticated requests
- Add get_rate_limit_key() using extract_token() + supabase.auth.get_user()
- Add log_ai_usage() to track calls in ai_usage_log table
- Add check_tenant_quota() reading max_ai_requests_per_hour from system_settings
- Apply quota check + usage logging to /ai/analyze and /ai/analyze_stream

Fixes ritesh-1918#2903
@vercel

vercel Bot commented Jun 17, 2026

Copy link
Copy Markdown

@Nancy-3012 is attempting to deploy a commit to the ritesh Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

backend/main.py gains three helpers: get_system_settings() now includes max_ai_requests_per_hour; log_ai_usage() writes to ai_usage_log; check_tenant_quota() enforces a per-company hourly cap with HTTP 429. A new get_rate_limit_key() replaces the global IP-only limiter key with per-authenticated-user identity. Both /ai/analyze and /ai/analyze_stream now invoke quota checks and usage logging.

Changes

AI Rate Limiting and Quota Enforcement

Layer / File(s) Summary
System settings, quota, and usage log helpers
backend/main.py
get_system_settings() adds max_ai_requests_per_hour to defaults and the Supabase select list. New log_ai_usage() inserts rows into ai_usage_log. New check_tenant_quota() counts hourly entries per company and raises HTTP 429 when the limit is exceeded; both helpers are no-ops when Supabase is unavailable.
Per-user/IP rate limit key function
backend/main.py
get_rate_limit_key() resolves the request token via Supabase auth and returns user:<uuid> for authenticated callers, falling back to get_remote_address(). The global Limiter is rewired to use this function.
Quota enforcement wired into analyze endpoints
backend/main.py
analyze_only() and the analyze_stream() event generator each fetch tenant settings, call check_tenant_quota(), call log_ai_usage(), and then extract confidence_threshold, duplicate_sensitivity, and enable_auto_resolve from those settings.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant get_rate_limit_key
  participant check_tenant_quota
  participant log_ai_usage
  participant Supabase

  Client->>get_rate_limit_key: HTTP request (bearer/cookie token)
  get_rate_limit_key->>Supabase: auth.get_user(token)
  Supabase-->>get_rate_limit_key: user or error
  get_rate_limit_key-->>Client: "user:<uuid>" or remote IP

  Client->>check_tenant_quota: company_id, max_ai_requests_per_hour
  check_tenant_quota->>Supabase: count ai_usage_log (last hour)
  Supabase-->>check_tenant_quota: count
  check_tenant_quota-->>Client: HTTP 429 if over limit

  Client->>log_ai_usage: user_id, company_id, endpoint
  log_ai_usage->>Supabase: insert into ai_usage_log
  Supabase-->>log_ai_usage: ack

  Client->>Client: proceed with AI analysis
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • ritesh-1918/HELPDESK.AI#920: Introduced slowapi/Limiter with get_remote_address key — directly replaced by the new get_rate_limit_key() in this PR.
  • ritesh-1918/HELPDESK.AI#2383: Added current_user auth dependencies to /ai/analyze and /ai/analyze_stream, the same endpoints receiving quota/logging wiring here.
  • ritesh-1918/HELPDESK.AI#2208: Secured other Gemini AI endpoints with per-user throttling, overlapping with this PR's per-user rate limit key and quota enforcement pattern.

Suggested labels

gssoc, gssoc:approved, level:critical, quality:exceptional, type:security

Poem

🐇 Hop hop, the IP trick won't do,
I check your token and your tenant too!
Each AI call is logged with care,
Quotas enforced — fair is fair.
No proxy tricks past this warren! 🛡️

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: implementing per-user and per-tenant rate limiting on AI endpoints, which is the primary objective of this PR.
Linked Issues check ✅ Passed The PR addresses all key objectives from issue #2903: custom rate limit key using user ID with IP fallback, per-tenant quota enforcement via system_settings, and usage logging to ai_usage_log.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing per-user and per-tenant rate limiting on AI endpoints as specified in issue #2903; no out-of-scope modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/main.py`:
- Around line 793-800: The line containing the
get_system_settings(request_body.company) assignment has inconsistent
indentation with only 3 spaces, while all subsequent lines (check_tenant_quota,
log_ai_usage, confidence_threshold, duplicate_sensitivity, enable_auto_resolve)
use 4 spaces. Fix this by aligning all lines to use consistent 4-space
indentation throughout the entire block to resolve the Python syntax error that
prevents module loading.
- Around line 965-971: Fix the indentation inconsistency on lines 965-966 by
ensuring all lines use the same indentation level (8 spaces). Additionally, move
the check_tenant_quota() call and log_ai_usage() call from inside the
event_generator() function to the outer analyze_stream() function, before the
StreamingResponse object is created. This ensures quota validation happens
before the response headers are sent and allows proper HTTPException handling to
return a 429 status code instead of breaking the stream.
- Around line 77-79: The SELECT query on the system_settings table is attempting
to retrieve the max_ai_requests_per_hour column, but this column is not defined
in the migration file supabase/migrations/20260531_add_company_settings.sql. To
fix this schema mismatch, either add the max_ai_requests_per_hour column
definition to the migration file with an appropriate data type and default
value, or remove max_ai_requests_per_hour from the SELECT query in the
supabase.table("system_settings").select() call if this field is intentionally
managed only through application defaults rather than stored in the database.
- Around line 87-99: The log_ai_usage function attempts to insert records into
the ai_usage_log table, but this table has not been created in any migration.
Create a new database migration file that defines the ai_usage_log table with
the following columns: id as a primary key (UUID or auto-increment integer),
user_id (nullable string or UUID), company_id (nullable string or UUID),
endpoint (string), and timestamp (timestamp with timezone). Ensure the schema
matches the fields being inserted by the log_ai_usage function to prevent
runtime errors.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c5648ee8-592a-439e-883c-5c783c1bf215

📥 Commits

Reviewing files that changed from the base of the PR and between da8faf2 and 6fc37cc.

📒 Files selected for processing (1)
  • backend/main.py

Comment thread backend/main.py
Comment on lines 77 to 79
res = supabase.table("system_settings").select(
"ai_confidence_threshold, duplicate_sensitivity, enable_auto_resolve"
"ai_confidence_threshold, duplicate_sensitivity, enable_auto_resolve, max_ai_requests_per_hour"
).eq("company_id", company_id).single().execute()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if max_ai_requests_per_hour column is defined in any migration
rg -n "max_ai_requests_per_hour" --type sql

Repository: ritesh-1918/HELPDESK.AI

Length of output: 49


🏁 Script executed:

# Find and examine the migration file mentioned in the review
fd -t f "20260531_add_company_settings.sql" .

Repository: ritesh-1918/HELPDESK.AI

Length of output: 123


🏁 Script executed:

# Also check what migration files exist in the supabase directory
fd -t f "\.sql$" supabase/migrations/ 2>/dev/null | head -20

Repository: ritesh-1918/HELPDESK.AI

Length of output: 498


🏁 Script executed:

# Examine the get_system_settings function in backend/main.py (lines 67-84)
sed -n '67,84p' backend/main.py

Repository: ritesh-1918/HELPDESK.AI

Length of output: 797


🏁 Script executed:

# Check if max_ai_requests_per_hour is referenced anywhere in Python files
rg -n "max_ai_requests_per_hour" --type py

Repository: ritesh-1918/HELPDESK.AI

Length of output: 338


🏁 Script executed:

# Examine the migration file content that was found
cat supabase/migrations/20260531_add_company_settings.sql

Repository: ritesh-1918/HELPDESK.AI

Length of output: 1808


🏁 Script executed:

# Also check if there are any other references to system_settings table definition
rg -n "CREATE TABLE.*system_settings|ALTER TABLE.*system_settings" --type sql

Repository: ritesh-1918/HELPDESK.AI

Length of output: 280


Schema mismatch: max_ai_requests_per_hour column missing from system_settings table.

The migration supabase/migrations/20260531_add_company_settings.sql does not define the max_ai_requests_per_hour column, yet the code at lines 77-79 attempts to select it. While the code mitigates this with a merge strategy ({**defaults, **res.data}) that falls back to the default value of 100, this still represents a schema integrity issue where code expectations diverge from the actual table structure.

Add max_ai_requests_per_hour to the migration, or remove it from the SELECT query if intentionally managed only via defaults.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/main.py` around lines 77 - 79, The SELECT query on the
system_settings table is attempting to retrieve the max_ai_requests_per_hour
column, but this column is not defined in the migration file
supabase/migrations/20260531_add_company_settings.sql. To fix this schema
mismatch, either add the max_ai_requests_per_hour column definition to the
migration file with an appropriate data type and default value, or remove
max_ai_requests_per_hour from the SELECT query in the
supabase.table("system_settings").select() call if this field is intentionally
managed only through application defaults rather than stored in the database.

Comment thread backend/main.py
Comment on lines +87 to +99
def log_ai_usage(user_id: str | None, company_id: str | None, endpoint: str) -> None:
"""Log each AI call to ai_usage_log for audit and billing."""
if not supabase:
return
try:
supabase.table("ai_usage_log").insert({
"user_id": user_id,
"company_id": company_id,
"endpoint": endpoint,
"timestamp": datetime.datetime.utcnow().isoformat() + "Z"
}).execute()
except Exception as e:
print(f"[WARNING] ai_usage_log insert failed: {e}")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for ai_usage_log table migration
rg -n "ai_usage_log" --type sql

Repository: ritesh-1918/HELPDESK.AI

Length of output: 49


🏁 Script executed:

# Find migration files in any format
find . -type f \( -name "*migration*" -o -name "*schema*" \) | head -20

Repository: ritesh-1918/HELPDESK.AI

Length of output: 157


🏁 Script executed:

# Check for Supabase-specific files
find . -type f -name "supabase*" -o -name "*supabase*" | head -20

Repository: ritesh-1918/HELPDESK.AI

Length of output: 193


🏁 Script executed:

# Search more broadly for ai_usage_log references
rg -i "ai_usage_log" --type-list | head -10
rg -i "ai_usage_log" -l

Repository: ritesh-1918/HELPDESK.AI

Length of output: 491


🏁 Script executed:

# Look for database initialization or setup documentation
find . -type f \( -name "*.md" -o -name "README*" -o -name "SETUP*" \) -exec grep -l "database\|table\|migration\|ai_usage_log" {} \;

Repository: ritesh-1918/HELPDESK.AI

Length of output: 652


🏁 Script executed:

# Check the migration file
cat -n ./supabase/migrations/20260330131213_remote_schema.sql | head -100

Repository: ritesh-1918/HELPDESK.AI

Length of output: 49


🏁 Script executed:

# Check if ai_usage_log is in the migration file
rg "ai_usage_log" ./supabase/migrations/

Repository: ritesh-1918/HELPDESK.AI

Length of output: 49


🏁 Script executed:

# Check Supabase directory structure
ls -la ./supabase/

Repository: ritesh-1918/HELPDESK.AI

Length of output: 352


🏁 Script executed:

# Get file size and content
wc -l ./supabase/migrations/20260330131213_remote_schema.sql

Repository: ritesh-1918/HELPDESK.AI

Length of output: 124


🏁 Script executed:

# Read the file
cat ./supabase/migrations/20260330131213_remote_schema.sql | grep -i "table\|create" | head -30

Repository: ritesh-1918/HELPDESK.AI

Length of output: 49


🏁 Script executed:

# Search for any table creation in all migration files
rg "CREATE TABLE" ./supabase/migrations/

Repository: ritesh-1918/HELPDESK.AI

Length of output: 168


🏁 Script executed:

# List all migration files
ls -1 ./supabase/migrations/

Repository: ritesh-1918/HELPDESK.AI

Length of output: 338


🏁 Script executed:

# Check all migration files for ai_usage_log
for file in ./supabase/migrations/*.sql; do echo "=== $file ==="; cat "$file" | grep -i "ai_usage_log" || echo "NOT FOUND"; done

Repository: ritesh-1918/HELPDESK.AI

Length of output: 658


🏁 Script executed:

# Check if table creation happens in Python code or elsewhere
rg -i "create.*table\|alter.*table" --type py backend/

Repository: ritesh-1918/HELPDESK.AI

Length of output: 49


Create migration for ai_usage_log table.

The log_ai_usage() function inserts into the ai_usage_log table, but this table is not defined in any migration. Add a migration to create the table with columns: id (primary key), user_id, company_id, endpoint, and timestamp.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/main.py` around lines 87 - 99, The log_ai_usage function attempts to
insert records into the ai_usage_log table, but this table has not been created
in any migration. Create a new database migration file that defines the
ai_usage_log table with the following columns: id as a primary key (UUID or
auto-increment integer), user_id (nullable string or UUID), company_id (nullable
string or UUID), endpoint (string), and timestamp (timestamp with timezone).
Ensure the schema matches the fields being inserted by the log_ai_usage function
to prevent runtime errors.

Comment thread backend/main.py
Comment on lines +793 to +800

settings = get_system_settings(request_body.company)
check_tenant_quota(request_body.company, settings)
log_ai_usage(request_body.user_id, request_body.company, "/ai/analyze")
confidence_threshold = settings["ai_confidence_threshold"]
duplicate_sensitivity = settings["duplicate_sensitivity"]
enable_auto_resolve = settings["enable_auto_resolve"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: Indentation syntax error breaks module loading.

Lines 794-795 have inconsistent indentation (3 spaces vs 4 spaces), causing Python syntax errors as flagged by static analysis. This will prevent the entire module from loading.

Proposed fix
-
-   settings = get_system_settings(request_body.company)
-    check_tenant_quota(request_body.company, settings)
-    log_ai_usage(request_body.user_id, request_body.company, "/ai/analyze")
+    settings = get_system_settings(request_body.company)
+    check_tenant_quota(request_body.company, settings)
+    log_ai_usage(request_body.user_id, request_body.company, "/ai/analyze")
     confidence_threshold = settings["ai_confidence_threshold"]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
settings = get_system_settings(request_body.company)
check_tenant_quota(request_body.company, settings)
log_ai_usage(request_body.user_id, request_body.company, "/ai/analyze")
confidence_threshold = settings["ai_confidence_threshold"]
duplicate_sensitivity = settings["duplicate_sensitivity"]
enable_auto_resolve = settings["enable_auto_resolve"]
settings = get_system_settings(request_body.company)
check_tenant_quota(request_body.company, settings)
log_ai_usage(request_body.user_id, request_body.company, "/ai/analyze")
confidence_threshold = settings["ai_confidence_threshold"]
duplicate_sensitivity = settings["duplicate_sensitivity"]
enable_auto_resolve = settings["enable_auto_resolve"]
🧰 Tools
🪛 Ruff (0.15.17)

[warning] 794-794: unindent does not match any outer indentation level

(invalid-syntax)


[warning] 795-795: Unexpected indentation

(invalid-syntax)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/main.py` around lines 793 - 800, The line containing the
get_system_settings(request_body.company) assignment has inconsistent
indentation with only 3 spaces, while all subsequent lines (check_tenant_quota,
log_ai_usage, confidence_threshold, duplicate_sensitivity, enable_auto_resolve)
use 4 spaces. Fix this by aligning all lines to use consistent 4-space
indentation throughout the entire block to resolve the Python syntax error that
prevents module loading.

Source: Linters/SAST tools

Comment thread backend/main.py
Comment on lines +965 to +971
settings = get_system_settings(request_body.company)
check_tenant_quota(request_body.company, settings)
log_ai_usage(request_body.user_id, request_body.company, "/ai/analyze_stream")
confidence_threshold = settings["ai_confidence_threshold"]
duplicate_sensitivity = settings["duplicate_sensitivity"]
enable_auto_resolve = settings["enable_auto_resolve"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: Indentation syntax error and HTTPException inside generator won't return 429.

Two issues here:

  1. Syntax error: Lines 965-966 have inconsistent indentation (7 vs 8 spaces), same issue as in analyze_only().

  2. HTTPException in streaming context: check_tenant_quota() is called inside event_generator() after StreamingResponse has started. By that point, response headers (200 OK) are already sent. Raising HTTPException will break the stream rather than returning a proper 429 response to the client.

Move quota checks and usage logging to the outer analyze_stream() function, before creating the StreamingResponse.

Proposed fix
 `@app.post`("/ai/analyze_stream")
 async def analyze_stream(request_body: TicketRequest):
     """
     REAL-TIME SSE ENDPOINT: Streams the AI progress to the frontend dynamically.
     """
     import datetime
     def get_now_ist():
         return datetime.datetime.utcnow().isoformat() + "Z"
+
+    # Quota enforcement BEFORE starting the stream
+    settings = get_system_settings(request_body.company)
+    check_tenant_quota(request_body.company, settings)
+    log_ai_usage(request_body.user_id, request_body.company, "/ai/analyze_stream")

     async def event_generator():
         text = request_body.text
         env_metadata = {
             "timestamp": get_now_ist(),
             "model_version": "3.0.0-PRO",
             "api_endpoint": "/ai/analyze_stream"
         }
         timeline = {"received": get_now_ist()}
-       settings = get_system_settings(request_body.company)
-        check_tenant_quota(request_body.company, settings)
-        log_ai_usage(request_body.user_id, request_body.company, "/ai/analyze_stream")
         confidence_threshold = settings["ai_confidence_threshold"]
         duplicate_sensitivity = settings["duplicate_sensitivity"]
         enable_auto_resolve = settings["enable_auto_resolve"]
🧰 Tools
🪛 Ruff (0.15.17)

[warning] 965-965: unindent does not match any outer indentation level

(invalid-syntax)


[warning] 966-966: Unexpected indentation

(invalid-syntax)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/main.py` around lines 965 - 971, Fix the indentation inconsistency on
lines 965-966 by ensuring all lines use the same indentation level (8 spaces).
Additionally, move the check_tenant_quota() call and log_ai_usage() call from
inside the event_generator() function to the outer analyze_stream() function,
before the StreamingResponse object is created. This ensures quota validation
happens before the response headers are sent and allows proper HTTPException
handling to return a 429 status code instead of breaking the stream.

Source: Linters/SAST tools

@Aryanbansal-05

Copy link
Copy Markdown

Good work buddy!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BOUNTY] [level:advanced] Per-user and per-tenant rate limiting missing on AI endpoints — IP-only throttling bypassable via proxies

2 participants