Kong API Gateway Routing Explained¶
Last Updated: 2025-11-13 Applies To: Supabase deployment on k3s
This document explains how Kong routes requests to Supabase services, how to understand the routing configuration, and how to debug routing issues.
Overview¶
Kong acts as a reverse proxy and API gateway for all Supabase services. It's the single entry point that:
- Routes requests to the correct backend service based on URL path
- Validates API keys and JWT tokens
- Handles CORS for browser requests
- Provides a single external IP instead of exposing all services
How Routing Works¶
Kong uses path-based routing configured via a declarative YAML file stored in a Kubernetes ConfigMap.
The Three Components¶
1. Path Matching (Primary Routing Logic)¶
Kong looks at the URL path of incoming requests and matches it against configured routes:
services:
- name: rest-v1
url: http://rest:3000/ # Target service (internal DNS)
routes:
- name: rest-v1-all
strip_path: true
paths:
- /rest/v1/ # Incoming path to match
How it works:
- Request comes in:
http://10.89.97.214:8000/rest/v1/my_table - Kong matches path
/rest/v1/to the route strip_path: trueremoves/rest/v1from the forwarded request- Kong forwards to:
http://rest:3000/my_table
2. Authentication Headers¶
Kong validates API keys from request headers:
Kong checks for keys in:
- Authorization: Bearer <your-key>
- apikey: <your-key> header
Keys are validated against the consumers section:
consumers:
- username: anon
keyauth_credentials:
- key: eyJhbGc... # Your ANON_KEY
- username: service_role
keyauth_credentials:
- key: eyJhbGc... # Service role key
3. Service DNS Resolution¶
Target URLs use Kubernetes internal DNS:
This resolves to:
- rest → rest.supabase.svc.cluster.local → Service ClusterIP 10.43.45.237:3000
Complete Request Flow¶
Let's trace a real request from your Next.js app querying data:
1. Client Makes Request¶
2. Supabase Client Generates HTTP Request¶
GET http://10.89.97.214:8000/rest/v1/bookmarks
Headers:
apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
3. Request Hits Kong LoadBalancer¶
4. Kong Matches Path to Route¶
5. Kong Validates API Key¶
6. Kong Applies Plugins¶
plugins:
- name: cors # Adds CORS headers
- name: key-auth # Validates key (already done)
config:
hide_credentials: true # Removes apikey header before forwarding
7. Kong Strips Path and Forwards¶
Original: GET /rest/v1/bookmarks
strip_path: true
Forwarded: GET /bookmarks
Target: http://rest:3000/bookmarks
Resolves to: 10.43.45.237:3000
8. PostgREST Processes Request¶
PostgREST (rest pod):
- Validates JWT signature
- Extracts role: "authenticated"
- Queries PostgreSQL with RLS policies
- Returns JSON
9. Kong Returns Response to Client¶
Routing Table¶
Here are all the configured routes in your Supabase deployment:
| Incoming Path | Target Service | Internal URL | Purpose | Auth Required |
|---|---|---|---|---|
/auth/v1/verify |
GoTrue | http://gotrue:9999/verify |
Email verification | No |
/auth/v1/callback |
GoTrue | http://gotrue:9999/callback |
OAuth callback | No |
/auth/v1/authorize |
GoTrue | http://gotrue:9999/authorize |
OAuth authorize | No |
/auth/v1/* |
GoTrue | http://gotrue:9999/ |
Authentication API | Yes |
/rest/v1/* |
PostgREST | http://rest:3000/ |
Database REST API | Yes |
/graphql/v1/* |
PostgREST | http://rest:3000/rpc/graphql |
GraphQL endpoint | Yes |
/storage/v1/* |
Storage | http://storage:5000/ |
File storage API | Depends |
/pg/* |
postgres-meta | http://postgres-meta:8080/ |
DB introspection | Yes |
Configuration Location¶
Kong's routing configuration is stored in a Kubernetes ConfigMap:
The config is mounted into Kong at:
Kong runs in DB-less mode (no database needed):
env:
- name: KONG_DATABASE
value: "off"
- name: KONG_DECLARATIVE_CONFIG
value: /usr/local/kong/declarative/kong.yml
Understanding strip_path¶
The strip_path: true setting is crucial for routing:
With strip_path: true (default)¶
Incoming: GET /rest/v1/bookmarks
Matched: /rest/v1/
Stripped: /rest/v1/
Forwarded: GET /bookmarks → http://rest:3000/bookmarks
Without strip_path (if false)¶
Incoming: GET /rest/v1/bookmarks
Matched: /rest/v1/
Forwarded: GET /rest/v1/bookmarks → http://rest:3000/rest/v1/bookmarks
↑ Would cause 404!
Why strip? Backend services don't know about the /rest/v1/ prefix - that's just for Kong routing.
Why Path-Based Routing?¶
Kong could route based on other criteria, but path-based routing is superior:
Advantages¶
✅ Simple to understand - URL clearly shows which service handles it ✅ Easy to debug - Just look at the path to know the route ✅ RESTful - Aligns with REST API conventions ✅ No DNS changes needed - All on one domain/IP ✅ Browser-friendly - Works with CORS, no preflight complexity
Alternative Approaches (Not Used)¶
❌ Header-based routing - More complex, harder to debug ❌ Hostname-based routing - Would need multiple DNS entries ❌ Port-based routing - Would need multiple LoadBalancer IPs
Updating Routes¶
To add a new service or modify routing:
Step 1: Edit Kong ConfigMap¶
Step 2: Add Your Service¶
# Add under services:
- name: my-new-service
_comment: "My Service: /my-service/v1/* -> http://my-service:8080/*"
url: http://my-service:8080/
routes:
- name: my-service-v1-all
strip_path: true
paths:
- /my-service/v1/
plugins:
- name: cors
- name: key-auth
config:
hide_credentials: true
Step 3: Restart Kong¶
# Kong needs restart to reload declarative config
kubectl rollout restart deployment -n supabase kong
# Wait for rollout
kubectl rollout status deployment -n supabase kong
# Verify new route works
curl http://10.89.97.214:8000/my-service/v1/health
Debugging Routing Issues¶
Check if Request Reaches Kong¶
# Test Kong directly
curl -I http://10.89.97.214:8000/rest/v1/
# Should return: HTTP/1.1 (some response, not connection refused)
Check Kong Logs¶
# View Kong logs
kubectl logs -n supabase -l app=kong --tail=100
# Follow logs in real-time
kubectl logs -n supabase -l app=kong -f
Look for:
- "upstream": "rest:3000" - Shows where Kong forwarded request
- "status": 401 - Authentication failure
- "status": 502 - Backend service unreachable
- "status": 404 - Route not found
Test Specific Route¶
# Get ANON_KEY
ANON_KEY=$(kubectl get secret -n supabase supabase-secrets -o jsonpath='{.data.ANON_KEY}' | base64 -d)
# Test auth endpoint (no key needed)
curl http://10.89.97.214:8000/auth/v1/health
# Test REST API (needs key)
curl -H "apikey: $ANON_KEY" \
http://10.89.97.214:8000/rest/v1/
# Test with verbose output
curl -v -H "apikey: $ANON_KEY" \
http://10.89.97.214:8000/rest/v1/
Common Error Codes¶
| Status | Meaning | Likely Cause |
|---|---|---|
401 |
Unauthorized | Invalid/missing API key |
404 |
Not Found | Route not configured or wrong path |
502 |
Bad Gateway | Backend service down or unreachable |
503 |
Service Unavailable | Kong can't reach service (DNS issue) |
504 |
Gateway Timeout | Backend service too slow |
Check Backend Service is Running¶
# Check all Supabase services
kubectl get pods -n supabase
# Check specific service
kubectl get service -n supabase rest
# Test backend directly (from another pod)
kubectl run -it --rm debug --image=curlimages/curl --restart=Never -- \
curl http://rest.supabase.svc.cluster.local:3000/
Verify Kong Configuration Loaded¶
# Check Kong has correct config
kubectl exec -n supabase -l app=kong -- \
cat /usr/local/kong/declarative/kong.yml | head -50
# Check consumers are loaded
kubectl exec -n supabase -l app=kong -- \
cat /usr/local/kong/declarative/kong.yml | grep -A 5 "consumers:"
Security Implications¶
Two-Layer Authentication¶
Kong provides defense in depth with two validation layers:
Layer 1: Kong validates API key exists
Request with apikey: xyz123
Kong checks: Is xyz123 in consumers list?
If NO → 401 Unauthorized (immediate)
If YES → Forward to backend
Layer 2: Backend validates JWT signature
PostgREST receives JWT
Validates signature with JWT_SECRET
Validates expiration
Extracts role and applies permissions
Why Not Just Backend Auth?¶
With Kong (current setup):
Without Kong (direct to PostgREST):
Kong acts as a gatekeeper, preventing invalid requests from reaching backend services.
What Kong DOESN'T Validate¶
Kong does NOT: - ❌ Check JWT expiration (backend does this) - ❌ Validate JWT claims (role, sub, etc.) - backend does this - ❌ Enforce Row Level Security - PostgreSQL does this
Kong only checks if the key exists in the consumers list.
Service Types and Exposure¶
ClusterIP Services (Internal Only)¶
Services: - postgres - gotrue - rest - storage - postgres-meta
Accessible from: - ✅ Other pods in cluster - ❌ Outside cluster (including your LXC containers)
LoadBalancer Services (External)¶
Services: - kong (API gateway) - studio (admin dashboard)
Accessible from: - ✅ Other pods in cluster - ✅ Outside cluster (your apps, browsers)
Why This Design?¶
Security: Internal services hidden from network Single entry point: Easier to secure one gateway than many services Flexibility: Can change internal services without affecting clients
Architecture Diagram¶
┌─────────────────────────────────────────────────────────────┐
│ External Network │
│ (Your apps, browsers, LXC containers) │
└─────────────────────────────────────────────────────────────┘
│
│ http://10.89.97.214:8000
▼
┌─────────────────────────────────────────────────────────────┐
│ Kong API Gateway (LoadBalancer) │
│ │
│ Path Matching: │
│ /rest/v1/* → rest:3000 │
│ /auth/v1/* → gotrue:9999 │
│ /storage/v1/* → storage:5000 │
│ │
│ Authentication: Validates API keys │
│ CORS: Adds headers for browsers │
└─────────────────────────────────────────────────────────────┘
│
┌───────────────┼───────────────┬───────────────┐
│ │ │ │
▼ ▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ GoTrue │ │PostgREST │ │ Storage │ │postgres- │
│ (Auth) │ │ (REST) │ │ (Files) │ │ meta │
│ ClusterIP│ │ ClusterIP│ │ ClusterIP│ │ ClusterIP│
└──────────┘ └──────────┘ └──────────┘ └──────────┘
│ │ │ │
└───────────────┼───────────────┴───────────────┘
│
▼
┌────────────────────┐
│ PostgreSQL │
│ (Database) │
│ ClusterIP │
└────────────────────┘
Performance Considerations¶
Kong is Lightweight¶
Resources:
Kong adds minimal latency (~1-2ms) for routing and validation.
Caching¶
Kong does NOT cache responses by default. Every request goes to backend.
To add caching (advanced):
Related Documentation¶
- Supabase Architecture - Overall service connections
- Supabase Kubernetes - Complete deployment guide
- JWT Token Management - Managing API keys
- Troubleshooting - Common issues
External Resources¶
Quick Reference¶
View Kong Config¶
Test Routes¶
# Health check
curl http://10.89.97.214:8000/auth/v1/health
# With auth
ANON_KEY=$(kubectl get secret -n supabase supabase-secrets -o jsonpath='{.data.ANON_KEY}' | base64 -d)
curl -H "apikey: $ANON_KEY" http://10.89.97.214:8000/rest/v1/