Multi-tenant SaaS API Gateway with authentication, rate limiting, caching, and reverse proxy capabilities.
- ✅ API Key authentication via
Authorization: Bearer <key>header - ✅ In-memory cache for API keys (15-minute TTL, <1ms lookups)
- ✅ PostgreSQL integration with automatic fallback on cache miss
- ✅ Background cache refresh every 15 minutes
- ✅ Redis-backed rate limiting with token bucket algorithm
- ✅ Reverse proxy to configurable backend services
- ✅ Structured JSON logging with request tracing
- ✅ Panic recovery middleware
- ✅ Health check endpoints (Kubernetes-ready)
- ✅ Request context propagation to backends
- ✅ Multi-backend support with service routing
- ✅ Burst traffic handling with configurable allowances
Client Request
↓
[Recovery Middleware] → Catches panics
↓
[Logging Middleware] → Structured JSON logs
↓
[Auth Middleware] → Validates API key (cache-first, PostgreSQL fallback)
↓
[Rate Limit Middleware] → Redis token bucket (Phase 2)
↓
[Proxy Handler] → Routes to backend service
↓
Backend Response
↓
Backend Service
cd ../../db/
docker-compose up -d
# Run migrations (Windows)
./scripts/setup.ps1
# Or on Linux/macOS
# bash scripts/setup.shcd ../services/gateway/
docker-compose up -d redisCreate a .env file:
cp .env.example .envEdit .env with your configuration:
GATEWAY_PORT=8080
LOG_LEVEL=info
BACKEND_URLS=api-service=http://localhost:3000
# PostgreSQL connection (required for Phase 2+)
DATABASE_URL=postgresql://gateway_user:dev_password_change_in_prod@localhost:5432/saas_gateway?sslmode=disable
# Redis configuration (optional - graceful degradation)
REDIS_ADDR=localhost:6379
REDIS_PASSWORD=
REDIS_DB=0
# Legacy API keys (will be replaced by database keys)
VALID_API_KEYS=sk_test_abc123:org_1:premiumcd ../../tools/keygen/
go build -o keygen.exe
# Set database URL
$env:DATABASE_URL="postgresql://gateway_user:dev_password_change_in_prod@localhost:5432/saas_gateway?sslmode=disable"
# Create a test key
./keygen.exe create --org-id=00000000-0000-0000-0000-000000000001 --name="Test API Key"
# Save the generated key (e.g., sk_test_abc123...)cd ../../services/gateway/
# Run with environment variables
$env:DATABASE_URL="postgresql://..."
go run cmd/server/main.goExpected startup logs:
✅ Connected to PostgreSQL
✅ Initialized API key cache (TTL: 15m)
[RefreshManager] Starting background cache refresh (interval: 15m0s)
✅ Connected to Redis for rate limiting
🚀 Gateway server starting on http://localhost:8080
Health Check:
curl http://localhost:8080/healthAuthenticated Request:
curl -H "Authorization: Bearer sk_test_abc123" \
http://localhost:8080/api/usersCheck Rate Limit Headers:
curl -i -H "Authorization: Bearer sk_test_abc123" \
http://localhost:8080/api/test | grep X-RateLimitTrigger Rate Limit (automated test):
# Run test suite
bash scripts/test-ratelimit.sh # Linux/macOS
./scripts/test-ratelimit.ps1 # Windows PowerShell| Endpoint | Description |
|---|---|
GET /health |
General health status |
GET /health/ready |
Readiness probe (Kubernetes) |
GET /health/live |
Liveness probe (Kubernetes) |
All other routes require a valid API key in the Authorization header:
Authorization: Bearer <your_api_key>
The gateway will proxy requests to the configured backend service.
| Variable | Required | Description | Example |
|---|---|---|---|
GATEWAY_PORT |
No | Server port (default: 8080) | 8080 |
LOG_LEVEL |
No | Logging level (default: info) | info, debug, warn, error |
REDIS_ADDR |
No | Redis server address | localhost:6379 |
REDIS_PASSWORD |
No | Redis password (if auth enabled) | your_password |
REDIS_DB |
No | Redis database number (default: 0) | 0 |
DATABASE_URL |
No | PostgreSQL connection (Phase 2) | postgresql://user:pass@localhost/db |
BACKEND_URLS |
Yes | Backend services (comma-separated) | api=http://localhost:3000 |
VALID_API_KEYS |
Yes | Temporary API keys (key:org_id:tier) | sk_test_abc:org1:premium |
Temporary hardcoded keys use this format:
key:organization_id:plan_tier
Example:
sk_test_abc123:org_1:premium
Plan Tiers:
basic- 100 req/min, 10K req/daypremium- 1000 req/min, 100K req/dayenterprise- 10K req/min, 1M req/day
The gateway adds these headers to backend requests:
X-Request-ID- Unique request identifierX-Organization-ID- Customer organization IDX-Plan-Tier- Customer subscription tierX-Forwarded-Proto- Original protocolX-Forwarded-For- Original client IP
All requests are logged in JSON format:
{
"timestamp": "2026-01-25T10:30:00.123Z",
"level": "info",
"method": "GET",
"path": "/api/users",
"status": 200,
"duration_ms": 45,
"bytes": 1024,
"client_ip": "192.168.1.1",
"request_id": "550e8400-e29b-41d4-a716-446655440000",
"organization_id": "org_1",
"plan_tier": "premium"
}All errors return JSON:
{
"error": {
"code": 401,
"message": "missing Authorization header"
},
"timestamp": "2026-01-25T10:30:00Z"
}Common Status Codes:
401- Missing or malformed Authorization header403- Invalid, revoked, or expired API key404- Service not found429- Rate limit exceeded (see details in response)500- Internal server error502- Backend service unavailable504- Backend service timeout
The gateway enforces two types of limits per organization:
- Per-Minute Limit - Sustained rate (e.g., 1000 req/min)
- Per-Day Limit - Daily quota (e.g., 100,000 req/day)
- Burst Allowance - Extra capacity for spikes (e.g., +500)
When rate limited, the gateway returns 429 Too Many Requests:
{
"error": {
"code": 429,
"message": "Rate limit exceeded: minute limit reached",
"details": {
"limit_type": "minute",
"daily_used": 1234,
"minute_used": 1500,
"reset_at": "2026-01-25T14:32:00Z",
"retry_after": 45
}
},
"timestamp": "2026-01-25T14:31:15Z",
"request_id": "550e8400-e29b-41d4-a716-446655440000"
}All responses include rate limit information:
X-RateLimit-Limit-Minute: 1000
X-RateLimit-Limit-Day: 100000
X-RateLimit-Remaining-Minute: 847
X-RateLimit-Remaining-Day: 95234
X-RateLimit-Reset-Minute: 2026-01-25T14:32:00Z
X-RateLimit-Reset-Day: 2026-01-26T00:00:00ZWhen rate limited:
Retry-After: 45 (seconds until reset)# Automated test suite
bash scripts/test-ratelimit.sh # Linux/macOS
./scripts/test-ratelimit.ps1 # Windows PowerShell
# Manual testing with loop
for i in {1..150}; do
curl -H "Authorization: Bearer sk_test_abc123" \
http://localhost:8080/api/test \
-w "\nStatus: %{http_code}\n"
sleep 0.1
done401- Missing or malformed Authorization header403- Invalid, revoked, or expired API key404- Service not found500- Internal server error502- Backend service unavailable504- Backend service timeout
Start a simple mock backend:
# In a new terminal
python -m http.server 3000Or use a simple Node.js server:
// mock-backend.js
const http = require("http");
const server = http.createServer((req, res) => {
console.log(`${req.method} ${req.url}`);
console.log("Headers:", req.headers);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
message: "Hello from mock backend",
organization: req.headers["x-organization-id"],
tier: req.headers["x-plan-tier"],
})
);
});
server.listen(3000, () => {
console.log("Mock backend running on http://localhost:3000");
});Run it:
node mock-backend.jsgateway/
├── cmd/
│ └── server/
│ └── main.go # Application entry point
├── internal/
│ ├── config/
│ │ └── config.go # Configuration loader
│ ├── handler/
│ │ ├── health.go # Health check handlers
│ │ └── proxy.go # Reverse proxy handler
│ └── middleware/
│ ├── auth.go # API key authentication
│ ├── logging.go # Structured logging
│ └── recovery.go # Panic recovery
├── pkg/
│ └── models/
│ └── apikey.go # Domain models
├── .env.example # Environment template
├── go.mod # Go module definition
└── README.md # This file
# Build binary
go build -o bin/gateway cmd/server/main.go
# Run binary
./bin/gateway# Unit tests
go test ./...
# With coverage
go test -cover ./...
# Verbose output
go test -v ./...Phase 1 Remaining:
- ✅ Module 1.1: Core Gateway (COMPLETE)
- 🔄 Module 1.2: PostgreSQL Schema (Next)
- 🔄 Module 1.3: API Key CLI
Phase 2:
- Redis-based rate limiting
- API key caching
Phase 3:
- Kafka event streaming
- TimescaleDB usage analytics
"Failed to load configuration" error:
- Ensure all required environment variables are set
- Check
.envfile format (no quotes around values)
"Backend service unavailable" error:
- Verify backend service is running
- Check
BACKEND_URLSconfiguration - Test backend directly:
curl http://localhost:3000
"Invalid API key" error:
- Verify API key format in
VALID_API_KEYS - Check Authorization header format:
Bearer <key> - Ensure no extra spaces or characters
MIT License - Copyright (c) 2026