Back to Blog
Engineering

Building a Secure Public API: Keys, OAuth, and Rate Limiting

Opening a public API is a trust exercise. You're giving external developers programmatic access to your users' data, and any security lapse can have catastrophic consequences. When we built Velocity's developer platform, we focused on three pillars: API keys with vel_ prefix and SHA-256 hashing, OAuth 2.0 with PKCE, and a sliding window rate limiter with tier-based limits.

API keys are the simplest auth method: generate a random 32-byte token, hash it with SHA-256, store the hash in the database, and return the plaintext key once. The vel_ prefix makes keys instantly recognizable and prevents accidental leaks (GitHub can scan for them). When a request arrives with an Authorization: Bearer vel_... header, we hash the incoming key and look up the hash. No plaintext keys ever touch the database.

OAuth 2.0 is the standard for third-party integrations. We implemented the authorization code flow with PKCE (Proof Key for Code Exchange) to protect against interception attacks. The flow is: (1) client redirects user to /oauth/authorize with code_challenge, (2) user consents and we generate an authorization code, (3) client exchanges code + code_verifier for access/refresh tokens. PKCE ensures only the original client can redeem the code.

Scopes control what each token can access. We support 15+ scopes like issues:read, issues:write, projects:read, etc. The GraphQL context includes a scopes array, and resolvers call requireScope('issues:write') to enforce permissions. Wildcard scopes (issues:*) and admin scopes (admin) are also supported for internal tooling.

Rate limiting prevents abuse and ensures fair usage. We built a sliding window rate limiter using Redis (or in-memory Map for development) that tracks request counts per identifier (IP, API key, or OAuth token) over a rolling window. Limits are tier-based: public endpoints get 60 req/min, API keys get 300 req/min, OAuth tokens get 300 req/min, and authenticated sessions get 100 req/min. Exceeded limits return 429 Too Many Requests with Retry-After headers.

The rate limiter runs as middleware before GraphQL execution. It increments the counter, checks the limit, and either allows the request or rejects it. The sliding window algorithm is more forgiving than fixed windows—it smooths out bursts and avoids the "thundering herd" problem at window boundaries. We use the current timestamp divided by window size as the bucket key.

These three layers—API keys, OAuth, and rate limiting—form a defense in depth. No single mechanism is perfect, but together they provide robust security for our public API. We also log every API request (identifier, scopes, query, latency) for auditing and abuse detection. If you're building a public API, don't skimp on auth and rate limiting—they're the foundation of trust.