Maybe Finance API Security: API Key Authentication and Rate Limiting Explained

Maybe Finance secures its JSON API through encrypted API key storage, OAuth fallback authentication, and multi-layered rate limiting using Redis-backed sliding windows with tiered limits.

The open-source maybe-finance/maybe repository implements a defense-in-depth strategy for API security. The system combines deterministic encryption for key storage, dual authentication methods, and multiple rate limiting layers to protect both hosted and self-hosted deployments.

API Key Storage and Encryption

The ApiKey model in app/models/api_key.rb handles secure storage and validation of API credentials. Keys are never stored in plaintext; instead, the system uses Rails Active Record encryption with deterministic encryption enabled for fast lookup capabilities.

Deterministic Encryption for Fast Lookup

The model encrypts the 64-character secret key using deterministic: true, which allows the system to query keys by their encrypted value without exposing the plaintext. The find_by_value method performs this lookup efficiently:


# From app/models/api_key.rb lines 24-32

def self.find_by_value(value)
  find_by(value: value)
rescue ActiveRecord::Encryption::Errors::Decryption
  nil
end

Validation and Scope Enforcement

Each API key enforces a single active key per user and source combination (web or mobile), preventing key sprawl. The model validates scopes strictly, supporting only read or read_write permissions:


# Validation logic from app/models/api_key.rb lines 10-16

validates :scopes, presence: true, inclusion: { in: %w[read read_write] }
validates :source, presence: true, inclusion: { in: %w[web mobile] }
validate :single_active_key_per_user_and_source, on: :create

Authentication Flow in the API Controller

The Api::V1::BaseController in app/controllers/api/v1/base_controller.rb orchestrates the authentication pipeline. It attempts OAuth token authentication first, then falls back to API key extraction from the X-Api-Key header.

OAuth Token Fallback

The controller first checks for Bearer tokens in the Authorization header. If present, it validates the token against the database and populates the user context:


# OAuth flow from app/controllers/api/v1/base_controller.rb lines 48-84

def authenticate_with_oauth_token
  # Extracts Bearer token and validates against OAuthAccessToken

  # Sets @current_user on success

end

API Key Extraction and Validation

When OAuth fails or is absent, the controller extracts the X-Api-Key header and validates it against the encrypted store. Successful authentication updates the last_used_at timestamp and initializes the rate limiter:


# API key flow from app/controllers/api/v1/base_controller.rb lines 90-104

def authenticate_with_api_key
  api_key = request.headers["X-Api-Key"]
  @current_api_key = ApiKey.find_by_value(api_key)
  @current_api_key.touch(:last_used_at) if @current_api_key
  # Rate limiter initialization follows

end

User Context and Audit Logging

Every authenticated request triggers comprehensive logging in app/controllers/api/v1/base_controller.rb lines 218-232. The system records the HTTP method, request path, user email, family ID, and authentication method (OAuth vs API key), creating an audit trail for security monitoring.

Rate Limiting Architecture

The ApiRateLimiter service in app/services/api_rate_limiter.rb implements per-key rate limiting using Redis-backed sliding windows. This design allows horizontal scaling while maintaining strict request quotas.

Redis-Backed Sliding Windows

The limiter stores counters in Redis with TTL-based expiration, creating automatic sliding windows. Each API key has independent counters, preventing one user from consuming another's quota:


# Core logic from app/services/api_rate_limiter.rb lines 16-50

def increment!
  # Atomically increments Redis counter with expiration

end

def exceeded?
  # Compares current count against tier limit

end

Tiered Limit Structure

Maybe implements three rate limit tiers defined in app/services/api_rate_limiter.rb lines 4-7:

  • Standard: 100 requests per hour
  • Premium: 1,000 requests per hour
  • Enterprise: 10,000 requests per hour

The tier is determined by the user's subscription status and passed to the limiter during initialization.

Self-Hosted Noop Mode

For self-hosted installations, the NoopApiRateLimiter class disables throttling entirely. This allows private deployments to operate without Redis infrastructure or request constraints, configured via environment variables.

Controller-Level Rate Limit Enforcement

After authentication, the Api::V1::BaseController enforces rate limits through the check_api_key_rate_limit callback.

429 Response Handling

When limits are exceeded, the controller returns a structured JSON error with HTTP 429 status. The response includes detailed metadata about the violation:


# Limit exceeded response from app/controllers/api/v1/base_controller.rb lines 124-141

def render_rate_limit_exceeded
  render json: {
    error: "rate_limit_exceeded",
    message: "API rate limit exceeded",
    details: {
      limit: @rate_limiter.limit,
      remaining: 0,
      reset_in_seconds: @rate_limiter.reset_in_seconds
    }
  }, status: :too_many_requests
end

Rate Limit Headers

Successful responses include standard X-RateLimit-* headers to inform clients of their current quota status. The add_rate_limit_headers method in app/controllers/api/v1/base_controller.rb lines 144-148 injects:

  • X-RateLimit-Limit: Total requests allowed per window
  • X-RateLimit-Remaining: Requests remaining in current window
  • X-RateLimit-Reset: Unix timestamp when the window resets

Global HTTP Throttling with Rack Attack

Maybe employs Rack::Attack as a secondary defense layer in config/initializers/rack_attack.rb. This middleware operates before Rails routing, providing IP-level and token-level protection.

The configuration implements two primary throttles:

  1. OAuth Token Endpoint Protection: Limits requests to /oauth/token to prevent brute-force attacks on authentication
  2. Generic API Throttling: Caps requests per IP address and per OAuth bearer token across all /api/ routes

For development and testing environments, the initializer includes permissive IP throttling that prevents local development from hitting limits while maintaining the production code paths.

Practical Code Examples

Creating and Using an API Key

Generate a new key through the Rails console:


# Generate a new key for the current user (web source)

key = current_user.api_keys.create!(
  name: "My script",
  scopes: ["read_write"],
  source: "web"
)

puts "Save this once – it will never be shown again:"
puts key.plain_key   # => e.g. "a3f9c2d4…"

Authenticating API Requests

Include the key in the X-Api-Key header:

curl -H "X-Api-Key: a3f9c2d4…" \
     https://api.maybe.com/api/v1/accounts

The response includes rate limit headers:


X-RateLimit-Limit: 100
X-RateLimit-Remaining: 97
X-RateLimit-Reset: 3540

Handling Rate Limit Exceeded (429) Responses

Implement exponential backoff when receiving HTTP 429:

#!/usr/bin/env ruby
require 'net/http'
require 'json'

url = URI('https://api.maybe.com/api/v1/transactions')
key = ENV['MAYBE_API_KEY']

loop do
  resp = Net::HTTP.start(url.host, url.port, use_ssl: true) do |http|
    req = Net::HTTP::Get.new(url)
    req['X-Api-Key'] = key
    http.request(req)
  end

  if resp.code == '429'
    body = JSON.parse(resp.body)
    wait = resp['Retry-After'] || body['details']['reset_in_seconds'] || 60
    puts "Rate limited – sleeping #{wait}s"
    sleep(wait.to_i)
  else
    puts "Success #{resp.code}"
    break
  end
end

Revoking API Keys

Invalidate a key immediately through the console:

key = current_user.api_keys.find_by(name: "My script")
key.revoke!    # sets revoked_at, making it instantly invalid

All subsequent requests with that key will receive a 401 Unauthorized response.

Summary

  • Encrypted Storage: API keys use Rails Active Record deterministic encryption in app/models/api_key.rb, enabling secure storage with fast lookup capabilities.
  • Dual Authentication: The Api::V1::BaseController supports both OAuth tokens and API keys via the X-Api-Key header, with comprehensive audit logging.
  • Tiered Rate Limiting: The ApiRateLimiter service implements sliding window counters in Redis with standard (100/hr), premium (1,000/hr), and enterprise (10,000/hr) tiers.
  • Graceful Degradation: Rate limit responses include structured JSON errors, Retry-After headers, and standard X-RateLimit-* headers for client-side backoff handling.
  • Defense in Depth: Rack::Attack provides IP-level and token-level throttling at the middleware layer, protecting against brute-force attacks on the OAuth token endpoint.

Frequently Asked Questions

How are API keys stored securely in Maybe Finance?

API keys are encrypted at rest using Rails Active Record encryption with the deterministic: true option, as implemented in app/models/api_key.rb. This approach ensures that the 64-character secret is never stored in plaintext, while still allowing the system to look up keys by their encrypted value using the find_by_value method.

What happens when I exceed the API rate limit?

When you exceed your tier's request quota (100 requests per hour for standard accounts), the API returns an HTTP 429 status code with a JSON error body containing the limit details and reset time. The response includes Retry-After and X-RateLimit-Reset headers that indicate when you can resume making requests, allowing your client to implement automatic backoff strategies.

Can I disable rate limiting for my self-hosted Maybe instance?

Yes, self-hosted installations can disable API rate limiting entirely by using the NoopApiRateLimiter class instead of the standard Redis-backed limiter. This configuration bypasses all request throttling, allowing private deployments to operate without Redis infrastructure or request constraints, which is useful for internal integrations or development environments.

How does Maybe prevent brute-force attacks against API authentication?

Maybe implements defense-in-depth through Rack::Attack middleware configured in config/initializers/rack_attack.rb. This adds IP-level throttling for the OAuth token endpoint (/oauth/token) and generic API routes, limiting requests per IP address and per OAuth bearer token before they reach the application layer, effectively blocking brute-force attempts against authentication endpoints.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →