Skip to content

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
Client Request → Kong (10.89.97.214:8000) → Internal 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: true removes /rest/v1 from the forwarded request
  • Kong forwards to: http://rest:3000/my_table

2. Authentication Headers

Kong validates API keys from request headers:

plugins:
  - name: key-auth
    config:
      hide_credentials: true

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:

url: http://rest:3000/

This resolves to: - restrest.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

// In your Next.js app
const { data } = await supabase
  .from('bookmarks')
  .select('*')

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

10.89.97.214:8000 (External IP)
Kong Pod (10.42.2.69)

4. Kong Matches Path to Route

Path: /rest/v1/bookmarks
Matches: /rest/v1/ → rest-v1 service

5. Kong Validates API Key

apikey: eyJhbGc...
Checks consumers list: ✅ Matches "anon" consumer

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

Response flows back through Kong
CORS headers added
Client receives data

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:

# View Kong configuration
kubectl get configmap -n supabase kong-config -o yaml

The config is mounted into Kong at:

/usr/local/kong/declarative/kong.yml

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

kubectl edit configmap -n supabase kong-config

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):

Bad request → Kong → 401 (fast, no backend load)
Good request → Kong → PostgREST → PostgreSQL

Without Kong (direct to PostgREST):

Bad request → PostgREST → CPU cycles wasted validating
Good request → PostgREST → PostgreSQL

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)

type: ClusterIP

Services: - postgres - gotrue - rest - storage - postgres-meta

Accessible from: - ✅ Other pods in cluster - ❌ Outside cluster (including your LXC containers)

LoadBalancer Services (External)

type: LoadBalancer
loadBalancerIP: 10.89.97.214

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:

requests:
  cpu: 100m
  memory: 256Mi
limits:
  cpu: 500m
  memory: 512Mi

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):

plugins:
  - name: proxy-cache
    config:
      strategy: memory
      content_type: ["application/json"]



External Resources


Quick Reference

View Kong Config

kubectl get configmap -n supabase kong-config -o yaml

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/

Debug Logs

kubectl logs -n supabase -l app=kong -f

Restart Kong

kubectl rollout restart deployment -n supabase kong