My first post about Asstgr covered the "what" — a self-hosted gateway that lets you register third-party APIs and call them through one unified interface. This time I want to zoom into the "how": specifically, two problems that are deceptively annoying to get right — OAuth2 token lifecycle management and per-user quota enforcement.
If you're building anything that proxies calls to multiple third-party APIs on behalf of your users, you'll hit both of these sooner or later.
Problem 1: OAuth2 tokens expire at the worst possible time
When you integrate a single OAuth2-protected API, refreshing a token by hand is annoying but manageable. When you're proxying an arbitrary number of APIs — each with its own grant type, scopes, and expiry — you need a system, not a snippet.
Asstgr models this with one OAuthConfig per API:
Field Purpose
grant_type client_credentials, authorization_code, or password
token_url Where to fetch/refresh tokens
client_id / client_secret_encrypted Credentials, secret stored encrypted at rest
scope Space-separated scopes
access_token / refresh_token Cached values
token_expires_at Expiry timestamp — null means the token never expires
The actual work happens in a dedicated OAuthService, kept separate from the views. Its job boils down to three responsibilities:
Fetch a token the first time an API is used, based on its grant_type.
Check expiry before every call — if token_expires_at is in the past (or close to it), refresh proactively instead of waiting for a 401 from the upstream API.
Persist the new access_token / refresh_token / token_expires_at back onto the OAuthConfig so the next call reuses it.
The detail that matters most here: refresh happens at the gateway level, once, and every caller benefits from it. Without a shared gateway, every single client integrating that API has to reimplement this same refresh logic — and inevitably some of them get it wrong (usually: reacting to a 401 instead of checking expiry ahead of time, which causes the first request after expiry to fail).
Encrypting client_secret at rest is non-negotiable once you're storing credentials for APIs you don't own. It's a small thing, but it's the difference between "gateway" and "liability."
Problem 2: quotas need to be atomic, not eventually-consistent
The second problem is subtler. A naive quota check looks like this:
python
if quota.call_count + api.quota_cost > quota.monthly_limit:
raise QuotaExceeded()
quota.call_count += api.quota_cost
quota.save()
This works fine until two requests from the same user hit the gateway at nearly the same time — which, with a burst limit of 30 requests/second, is not a rare edge case, it's Tuesday. Both requests read the same call_count, both pass the check, both increment, and you've let the user go over budget.
Asstgr's APICallQuota model tracks call_count, monthly_limit, and the current month/year, and the increment is wrapped so the check-and-increment happens atomically at the database level rather than in Python. Two practical rules came out of building this:
Never trust an in-memory read for a value you're about to write back. Use F() expressions or select_for_update() so the increment happens where the data lives, not in application code that can race.
Reset by period, not by cron job. Rather than a scheduled task that zeroes out counters on the 1st of the month (and inevitably fails silently once), the quota check itself looks up-or-creates the APICallQuota row for the current month/year. If it doesn't exist yet, it's created fresh. No cron, no midnight job that can fail, no stale counter.
HasSufficientQuota is a DRF permission class, checked before execute/ even reaches the view logic — which means a user who's out of credits gets a clean 403 before the gateway wastes a call to the upstream API on their behalf.
Where this leaves the request lifecycle
Putting the two together, a single call to /api/v1/apis/{id}/endpoints/{id}/execute/ goes through:
- Authenticate the caller (API key)
- Check burst + sustained rate limits (DRF throttling)
- Check quota (HasSufficientQuota) — atomic check against APICallQuota
- Resolve auth for the target API:
- static API key → attach directly
- OAuth2 → OAuthService checks expiry, refreshes if needed
- Build the request from registered Endpoint/Parameter/Header/Method
- Call the upstream API
- Format the response (JSONCleaner: json/compact/standard/verbose)
- Log the call (APILog) + increment quota
- Return the formatted response + quota status to the caller
Nothing here is exotic — it's mostly about making sure steps 3 and 4 don't have races, and that a failure in step 6 doesn't leave the quota incremented for a call that never actually succeeded.
Takeaways if you're building something similar
Centralize token refresh. One place that owns "is this token still good" saves every downstream integration from reimplementing (and getting wrong) the same logic.
Treat quota checks as a database problem, not an application problem. Anything read-then-write under concurrency needs to happen atomically where the data lives.
Derive state from time, don't schedule it. A quota keyed by (user, month, year) that's lazily created is more robust than a monthly reset job.
Fail before you spend. Check quota and rate limits before making the upstream call, not after — so a rejected request costs you nothing.
Asstgr is open source (Django + DRF) if you want to see the full implementation: github.com/asstgr/asstgropensource. Happy to dig into any of these pieces further in the comments.
Top comments (0)