Zuplo
API Rate Limiting

Per-User Rate Limiting for Supabase APIs with JWT Auth

Josh TwistJosh Twist
January 9, 2023
5 min read

Learn how to add per-user rate limiting to your Supabase backend using JWT authentication and Zuplo. Protect your API from abuse with identity-aware limits in minutes.

Supabase backends are often publicly accessible — anyone can sign up, create an account, and start making requests. That openness is great for developer experience, but it creates a real risk: a single user (malicious or simply careless) can overwhelm your service with excessive requests, starving other users of resources and driving up your infrastructure costs.

The answer is per-user rate limiting — enforcing a maximum number of requests per authenticated user over a given time window. Unlike IP-based limits, which can block legitimate users behind shared networks, per-user limits tie directly to the caller’s identity. For Supabase apps, that identity comes from the JWT token issued at sign-in.

With Zuplo, you can add identity-aware rate limiting to any Supabase backend in a few minutes, without changing your backend code. The only change on the client side is updating the Supabase URL to point to your Zuplo gateway.

Why Per-User Rate Limits Matter for Supabase

Supabase provides built-in rate limiting, but those limits are primarily IP-based on authentication endpoints — they don’t tie to the identity of an authenticated user. Without per-user controls, a single bad actor can consume your resources from multiple IPs, and legitimate users behind a shared network can be unfairly blocked.

Per-user rate limits solve this by giving each authenticated user their own independent request budget. This approach:

  • Prevents resource starvation — one heavy user can’t degrade the experience for everyone else
  • Protects your costs — Supabase charges based on usage, so runaway requests directly impact your bill
  • Enables fair usage policies — you can set different limits for different user tiers or plans
  • Reduces abuse surface — automated scripts and bots that authenticate as a single user get throttled before they cause damage

How Supabase JWT Auth Maps to Rate-Limit Keys

Supabase issues a JWT (JSON Web Token) when a user signs in. That token contains a sub claim — the unique user ID — along with other claims like role, user_metadata, and app_metadata.

When you place Zuplo in front of your Supabase backend, the Supabase JWT Auth policy validates every incoming token and populates request.user on the ZuploRequest object. Specifically:

  • request.user.sub is set to the JWT’s sub claim (the Supabase user ID)
  • request.user.data contains the remaining token claims, including user_metadata and app_metadata

When the Rate Limiting policy is configured with "rateLimitBy": "user", it reads request.user.sub and uses that value as the bucket key. Each unique user ID gets its own counter, so two different users never share a rate-limit bucket.

This is why policy ordering matters: the Supabase JWT auth policy must run before the rate-limit policy in the request pipeline so that request.user is already populated when the rate limiter checks it.

Configuring Per-User Rate Limits Step by Step

There is a video walkthrough of this guide if you prefer to follow along visually:

Step 1: Create a Zuplo Project

Sign up for a free account at portal.zuplo.com and create a new project.

Step 2: Add a Wildcard Route

Add a route to your project with the following properties:

  • Path: /(.*) — a wildcard pattern that matches all incoming paths
  • Methods: all — select every HTTP method in the dropdown
  • CORS: anything goes — the simplest option for getting started (you can configure a stricter CORS policy later)
  • URL Rewrite: https://your-supabase-project.supabase.co${pathname} — replace the domain with your actual Supabase project URL

This route proxies every request through Zuplo to your Supabase backend, preserving the original path.

Step 3: Add the Supabase JWT Auth Policy

Add a policy to the request pipeline and choose the supabase-jwt-auth policy. Configure it to verify tokens using your Supabase JWT secret:

JSONjson
{
  "export": "SupabaseJwtInboundPolicy",
  "module": "$import(@zuplo/runtime)",
  "options": {
    "secret": "$env(SUPABASE_JWT_SECRET)",
    "allowUnauthenticatedRequests": false
  }
}

With allowUnauthenticatedRequests set to false, any request without a valid Supabase JWT automatically receives a 401 Unauthorized response.

Step 4: Set the JWT Secret Environment Variable

Navigate to Settings > Environment Variables in the Zuplo portal and create a new variable called SUPABASE_JWT_SECRET. Paste in the JWT secret from your Supabase project (found under Settings > API in the Supabase dashboard).

Environment Variable

This secret is stored securely and never exposed in your route configuration.

Step 5: Add the Rate-Limiting Policy

Add a second policy after the Supabase JWT auth policy in the request pipeline. Choose the rate-limit-inbound policy and configure it for per-user limiting:

JSONjson
{
  "export": "RateLimitInboundPolicy",
  "module": "$import(@zuplo/runtime)",
  "options": {
    "rateLimitBy": "user",
    "requestsAllowed": 100,
    "timeWindowMinutes": 1
  }
}

Rate Limit Policy

This configuration allows each authenticated user up to 100 requests per minute. Adjust requestsAllowed and timeWindowMinutes to match your application’s needs. When a user exceeds the limit, they receive a 429 Too Many Requests response with a Retry-After header indicating when they can try again.

Step 6: Update Your Client

Go to the Getting Started tab in the Zuplo portal and copy your gateway URL. Replace the Supabase URL in your client application with this gateway URL.

Getting Started

That’s it — all traffic now flows through Zuplo, where it’s authenticated and rate-limited on a per-user basis before being forwarded to Supabase.

Testing Your Per-User Rate Limits

After deploying, verify that rate limiting works correctly:

  1. Authenticate as a test user and send requests at your normal rate — requests should succeed with 200 responses.
  2. Exceed the limit by sending more than requestsAllowed requests within the time window. You should receive a 429 Too Many Requests response.
  3. Authenticate as a different user and confirm that their requests still succeed — each user has an independent counter.
  4. Check the response headers for Retry-After, which tells the client how long to wait before retrying.

If you set a low limit (like 2 requests per minute) for testing, you can quickly verify the behavior with a tool like curl or Postman.

Common Pitfalls

  • Policy ordering — if the rate-limit policy runs before the JWT auth policy, request.user won’t be set and the rate limiter can’t identify the caller. Always place authentication first in the pipeline.
  • Missing environment variable — if SUPABASE_JWT_SECRET isn’t set or contains the wrong value, every request will fail authentication with a 401. Double-check the value in your Supabase dashboard under Settings > API.
  • Anonymous access — with allowUnauthenticatedRequests: false, unauthenticated requests are blocked before they reach the rate limiter. If you need to allow some anonymous endpoints, consider using separate routes with different policy configurations.
  • Overly strict limits — start with generous limits and tighten them based on real usage data. Blocking legitimate users is worse than allowing a few extra requests.

Next Steps

With per-user rate limiting in place, you’ve added a critical layer of protection to your Supabase backend. From here, you can explore:

Ready to protect your Supabase API? Get started with Zuplo for free.

Try Zuplo free

Try the platform behind this post

Zuplo is a developer-first API gateway. Deploy your first API in minutes — no credit card required.

  • 100K requests/mo free
  • GitOps deploys
  • 300+ edge locations