Skip to content

Supabase Architecture & Service Connections

This document explains what each Supabase service does, how they connect, and how the complete system works together.


Architecture Overview

┌─────────────────────────────────────────────────────────────────┐
│                         EXTERNAL ACCESS                          │
└─────────────────────────────────────────────────────────────────┘
      ┌───────────────────────┼───────────────────────┐
      │                       │                       │
      ▼                       ▼                       ▼
┌──────────┐           ┌──────────┐           ┌──────────┐
│  Browser │           │   Apps   │           │  Studio  │
│  (User)  │           │ (Next.js)│           │(Dashboard│
└──────────┘           └──────────┘           └──────────┘
      │                       │                       │
      │  ANON_KEY            │  ANON_KEY            │  SERVICE_ROLE_KEY
      │                       │                       │
      └───────────────────────┼───────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│                    Kong API Gateway (Port 8000)                  │
│  • Validates JWT tokens (checks consumers list)                 │
│  • Routes requests to appropriate services                      │
│  • Handles CORS                                                 │
└─────────────────────────────────────────────────────────────────┘
              ┌───────────────┼───────────────┬───────────────┐
              │               │               │               │
              ▼               ▼               ▼               ▼
        ┌──────────┐    ┌──────────┐   ┌──────────┐   ┌──────────┐
        │  GoTrue  │    │PostgREST │   │ Storage  │   │postgres- │
        │  (Auth)  │    │  (REST)  │   │  (Files) │   │   meta   │
        └──────────┘    └──────────┘   └──────────┘   └──────────┘
              │               │               │               │
              └───────────────┼───────────────┴───────────────┘
                    ┌────────────────────┐
                    │    PostgreSQL      │
                    │  (Database Core)   │
                    │                    │
                    │  Schemas:          │
                    │  • auth            │
                    │  • storage         │
                    │  • home_portal     │
                    │  • money_tracker   │
                    └────────────────────┘

Services Explained

1. Kong API Gateway

What it does: - Acts as the single entry point for all API requests - Validates JWT tokens before forwarding requests (first line of defense) - Routes requests to the appropriate backend service - Handles CORS (Cross-Origin Resource Sharing) for browser requests

Port: 8000 (HTTP), 8443 (HTTPS) LoadBalancer IP: 10.89.97.214 Image: kong:2.8.1

How it works: 1. Client sends request with Authorization: Bearer <JWT_TOKEN> or apikey: <JWT_TOKEN> header 2. Kong checks if token exists in its consumers list (configured in kong-config ConfigMap) 3. If valid, Kong forwards request to appropriate service (GoTrue, PostgREST, Storage) 4. If invalid, Kong returns 401 Unauthorized immediately

Configuration Location: - /root/k8s/manifests/supabase/kong.yaml - ConfigMap: kong-config contains routing rules and consumer credentials

Key Configuration:

consumers:
  - username: anon
    keyauth_credentials:
      - key: <ANON_KEY>  # Must match supabase-secrets
  - username: service_role
    keyauth_credentials:
      - key: <SERVICE_ROLE_KEY>  # Must match supabase-secrets

Routes: - /auth/v1/* → GoTrue (authentication) - /rest/v1/* → PostgREST (database REST API) - /storage/v1/* → Storage (file uploads/downloads) - /pg/* → postgres-meta (database introspection)

For detailed explanation of how Kong routes work, see Kong Routing Explained.


2. GoTrue (Authentication Service)

What it does: - Manages user authentication and authorization - Handles signup, login, password reset, email verification - Issues and validates JWT tokens - Manages user sessions - Handles OAuth providers (Google, GitHub, etc.)

Port: 9999 (internal only) Image: supabase/gotrue:v2.132.3

Database Schema: auth Key Tables: - auth.users - User accounts (shared across all apps for SSO) - auth.identities - OAuth provider linkages - auth.sessions - Active user sessions - auth.refresh_tokens - Refresh tokens for session renewal

Environment Variables (from supabase-secrets): - DATABASE_URL - PostgreSQL connection with ?search_path=auth - JWT_SECRET - Secret used to sign JWT tokens - ANON_KEY - Public API key - SERVICE_ROLE_KEY - Admin API key (bypasses RLS)

API Endpoints: - POST /auth/v1/signup - Create new user - POST /auth/v1/token?grant_type=password - Login - POST /auth/v1/logout - End session - POST /auth/v1/recover - Password reset - GET /auth/v1/user - Get current user info - POST /auth/v1/admin/users - Create user (admin, requires SERVICE_ROLE_KEY)

How it connects: 1. Client sends auth request → Kong → GoTrue 2. GoTrue queries PostgreSQL auth.* tables 3. GoTrue generates JWT token signed with JWT_SECRET 4. Returns token to client 5. Client uses token for subsequent API requests


3. PostgREST (REST API Service)

What it does: - Automatically generates a RESTful API from your PostgreSQL database - Converts HTTP requests to SQL queries - Enforces Row Level Security (RLS) policies - Supports complex queries, filtering, ordering, pagination

Port: 3000 (internal only) Image: postgrest/postgrest:v12.0.2

Database Schemas Exposed: - home_portal - Home Portal app tables - money_tracker - Money Tracker app tables - public - Public shared tables - storage - Storage metadata - graphql_public - GraphQL schema

Configuration (from configmap):

PGRST_DB_SCHEMA: "home_portal,money_tracker,public,storage,graphql_public"
PGRST_DB_ANON_ROLE: "anon"
PGRST_JWT_SECRET: "<JWT_SECRET>"

How it works: 1. Client sends request: GET /rest/v1/pages?select=* 2. Kong validates JWT → forwards to PostgREST 3. PostgREST extracts role from JWT (anon, authenticated, service_role) 4. PostgREST converts to SQL: SELECT * FROM home_portal.pages WHERE <RLS_POLICY> 5. PostgreSQL executes query with role's permissions 6. PostgREST returns JSON response

Security: - Row Level Security (RLS): PostgreSQL policies control who can see/modify what - JWT Role Extraction: PostgREST uses SET LOCAL ROLE to switch to JWT's role - Anon Role: Read-only access, public data only - Authenticated Role: Full access to user's own data (via RLS policies) - Service Role: Bypasses RLS, full database access (admin only)

Example Query:

// Client code
const { data } = await supabase
  .from('pages')
  .select('*')
  .eq('published', true)

// Becomes SQL:
// SELECT * FROM home_portal.pages WHERE published = true AND <RLS checks user can see it>


4. Storage Service

What it does: - Manages file uploads and downloads - Organizes files into "buckets" (like folders) - Generates signed URLs for secure file access - Handles image transformations (resize, crop) - Integrates with PostgreSQL for metadata and permissions

Port: 5000 (internal only) Image: supabase/storage-api:v0.43.11

Database Schema: storage Key Tables: - storage.buckets - Bucket configuration (public/private, size limits) - storage.objects - File metadata (name, size, mime type, owner)

File Storage Backend: - Files stored on disk (mounted volume) - Path: /var/lib/storage - Metadata in PostgreSQL, actual files on filesystem

How it works: 1. Client uploads file: POST /storage/v1/object/bucket-name/file.jpg 2. Kong validates JWT → forwards to Storage 3. Storage checks bucket permissions (RLS policies on storage.objects) 4. If allowed, saves file to disk + creates record in storage.objects 5. Returns file URL

Bucket Types: - Public Buckets: Files accessible without authentication - Private Buckets: Requires authentication, subject to RLS policies

Example:

// Upload file
const { data, error } = await supabase.storage
  .from('home-portal-assets')
  .upload('logo.png', file)

// Get public URL
const { data: url } = supabase.storage
  .from('home-portal-assets')
  .getPublicUrl('logo.png')


5. postgres-meta (Database Introspection)

What it does: - Provides API to inspect database schema (tables, columns, functions, policies) - Used by Studio to display database structure - Allows creating/modifying tables, columns, indexes through API - Executes raw SQL queries

Port: 8080 (internal only) Image: supabase/postgres-meta:v0.68.0

How it works: 1. Studio sends request: GET /pg/tables 2. Kong validates SERVICE_ROLE_KEY → forwards to postgres-meta 3. postgres-meta queries PostgreSQL system catalogs 4. Returns JSON description of tables

Key Endpoints: - GET /tables - List all tables - GET /columns?table=users - Get table columns - GET /policies - List RLS policies - POST /query - Execute raw SQL - POST /tables - Create new table

Security: - Requires SERVICE_ROLE_KEY - Only accessible by admins - Used exclusively by Studio dashboard - Should never be exposed to client applications


6. Studio (Admin Dashboard)

What it does: - Web-based UI for managing Supabase - View and edit database tables - Create users and manage auth - Configure storage buckets - Write and test SQL queries - View logs and monitor performance

Port: 3000 (external) LoadBalancer IP: 10.89.97.215 Image: supabase/studio:20240729-ce42139

Access: http://10.89.97.215:3000

How it connects: - Uses SERVICE_ROLE_KEY for admin operations - Calls Kong API Gateway for all backend operations - Talks to postgres-meta for schema introspection - Runs in browser (client-side React app)

Environment Variables:

SUPABASE_URL: "http://10.89.97.214:8000"  # Kong API Gateway (LoadBalancer IP)
SUPABASE_PUBLIC_URL: "http://10.89.97.214:8000"
SUPABASE_ANON_KEY: <ANON_KEY>
SUPABASE_SERVICE_KEY: <SERVICE_ROLE_KEY>

Note: Studio must use external LoadBalancer IPs because it runs in your browser (client-side), not inside the Kubernetes cluster. Browsers cannot resolve internal Kubernetes DNS names like kong.supabase.svc.cluster.local.


7. PostgreSQL (Database Core)

What it does: - Core data storage for everything - Stores user accounts, app data, file metadata - Enforces Row Level Security (RLS) policies - Provides full SQL capabilities (triggers, functions, views, etc.)

Port: 5432 (internal only) Service: postgres.supabase.svc.cluster.local Image: supabase/postgres:15.1.1.78 (PostgreSQL 15 with extensions)

Schemas: - auth - Authentication (users, sessions, tokens) - Shared across all apps - storage - File metadata (buckets, objects) - Shared - home_portal - Home Portal app tables - Isolated - money_tracker - Money Tracker app tables - Isolated - public - Default schema - Shared utilities

Storage: - PVC: postgres-data-postgres-0 - Size: 20GB - StorageClass: Longhorn (2-replica for HA) - Mount: /var/lib/postgresql - Actual Data: /var/lib/postgresql/data (created by PostgreSQL)

Key Extensions: - uuid-ossp - UUID generation - pgcrypto - Cryptographic functions - pg_stat_statements - Query performance monitoring - pg_trgm - Fuzzy text search

Connection String:

postgres://postgres:<password>@postgres.supabase.svc.cluster.local:5432/postgres?search_path=auth


Authentication Flow (Detailed)

User Login Flow

1. User submits email/password to app
2. App sends POST to Kong:
   POST http://10.89.97.214:8000/auth/v1/token?grant_type=password
   Headers: apikey: <ANON_KEY>
   Body: { "email": "user@example.com", "password": "..." }
3. Kong validates ANON_KEY in consumers list
4. Kong forwards to GoTrue
5. GoTrue queries PostgreSQL:
   SELECT * FROM auth.users WHERE email = 'user@example.com'
6. GoTrue verifies password hash
7. GoTrue generates JWT token:
   Header: { "alg": "HS256", "typ": "JWT" }
   Payload: { "sub": "<user-uuid>", "role": "authenticated", "email": "..." }
   Signature: HMACSHA256(header + payload, JWT_SECRET)
8. GoTrue creates session in auth.sessions
9. GoTrue returns to app:
   {
     "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
     "token_type": "bearer",
     "expires_in": 3600,
     "refresh_token": "...",
     "user": { ... }
   }
10. App stores access_token
11. App includes token in subsequent requests:
    Authorization: Bearer <access_token>

Authenticated API Request Flow

1. App makes request to fetch user's data:
   GET http://10.89.97.214:8000/rest/v1/transactions?select=*
   Headers:
     apikey: <ANON_KEY>
     Authorization: Bearer <access_token>
2. Kong validates ANON_KEY in consumers list
3. Kong forwards to PostgREST (passes Authorization header)
4. PostgREST validates JWT signature using JWT_SECRET
5. PostgREST extracts role from JWT: "authenticated"
6. PostgREST connects to PostgreSQL and runs:
   SET LOCAL ROLE authenticated;
   SET LOCAL request.jwt.claim.sub = '<user-uuid>';
   SELECT * FROM money_tracker.transactions WHERE <RLS_POLICY>;
7. PostgreSQL enforces RLS policy:
   CREATE POLICY "Users can see own transactions"
   ON money_tracker.transactions
   FOR SELECT
   USING (user_id = auth.uid());
8. Returns only transactions where user_id matches JWT's sub claim
9. PostgREST returns JSON to app

Service Dependencies

PostgreSQL (Core)
    ├── GoTrue (reads/writes auth schema)
    ├── PostgREST (reads/writes app schemas)
    ├── Storage (reads/writes storage schema)
    └── postgres-meta (introspects all schemas)

Kong (Gateway)
    ├── Routes to GoTrue
    ├── Routes to PostgREST
    ├── Routes to Storage
    └── Routes to postgres-meta

Studio (Dashboard)
    └── Calls all services via Kong (using SERVICE_ROLE_KEY)

Applications
    └── Call GoTrue + PostgREST + Storage via Kong (using ANON_KEY or user JWT)

Startup Order: 1. PostgreSQL (must be running first) 2. GoTrue, PostgREST, Storage, postgres-meta (need database) 3. Kong (needs services to route to) 4. Studio (needs Kong to be available)


Environment Variables & Secrets

Shared Across All Services

From supabase-secrets Secret: - JWT_SECRET - Signs all JWT tokens (must be 32+ characters) - ANON_KEY - Public API key (JWT with role: anon) - SERVICE_ROLE_KEY - Admin API key (JWT with role: service_role) - DATABASE_URL - PostgreSQL connection string - POSTGRES_PASSWORD - Database password

How to View:

# View all secrets
kubectl get secret -n supabase supabase-secrets -o yaml

# View specific secret
kubectl get secret -n supabase supabase-secrets -o jsonpath='{.data.JWT_SECRET}' | base64 -d

Service-Specific

GoTrue: - SITE_URL - Frontend URL for email links - SMTP_* - Email configuration (for password reset, verification) - DISABLE_SIGNUP - Disable public signups

PostgREST: - PGRST_DB_SCHEMA - Comma-separated list of schemas to expose - PGRST_DB_ANON_ROLE - Default role for unauthenticated requests

Storage: - FILE_SIZE_LIMIT - Max file upload size - STORAGE_BACKEND - file | s3 (we use file)

Studio: - SUPABASE_URL - Kong API Gateway URL (must be external LoadBalancer IP) - SUPABASE_PUBLIC_URL - Same as above


Network & Service Discovery

Internal Communication (within Kubernetes)

Services communicate using Kubernetes DNS: - postgres.supabase.svc.cluster.local:5432 - gotrue.supabase.svc.cluster.local:9999 - rest.supabase.svc.cluster.local:3000 - storage.supabase.svc.cluster.local:5000 - kong.supabase.svc.cluster.local:8000

External Access (from outside Kubernetes)

LoadBalancer services expose external IPs: - Kong (API): 10.89.97.214:8000 - Studio (Dashboard): 10.89.97.215:3000

All client applications and browsers use these external IPs.


Key Configuration Files

1. /root/k8s/manifests/supabase/configmap.yaml

Central configuration for all services: - API URLs (must use external LoadBalancer IPs for Studio) - Database connection settings - Service-specific settings (PGRST_DB_SCHEMA, etc.)

2. /root/k8s/manifests/supabase/kong.yaml

Kong API Gateway configuration: - Consumers list - Allowed JWT tokens (CRITICAL: must match supabase-secrets) - Routing rules - CORS settings

3. Kubernetes Secret: supabase-secrets

Sensitive credentials: - JWT_SECRET - ANON_KEY - SERVICE_ROLE_KEY - DATABASE_URL - POSTGRES_PASSWORD

NEVER commit to Git!


Troubleshooting Service Connections

Check if all services are running

kubectl get pods -n supabase

All pods should show STATUS: Running and READY: 1/1.

Test Kong → GoTrue connection

curl -I http://10.89.97.214:8000/auth/v1/health

# Should return: 200 OK
# If 401: Kong is working, but rejecting your key
# If 502/504: Kong can't reach GoTrue

Test Kong → PostgREST connection

curl http://10.89.97.214:8000/rest/v1/

# Should return: 400 (schema not specified) or schema list
# NOT 401: That means Kong rejected the request

Check PostgreSQL connectivity from GoTrue

# Exec into GoTrue pod
kubectl exec -it -n supabase <gotrue-pod> -- sh

# Try connecting to database
psql postgres://postgres:<password>@postgres.supabase.svc.cluster.local:5432/postgres

View service logs

# Kong
kubectl logs -n supabase -l app=kong --tail=50

# GoTrue
kubectl logs -n supabase -l app=gotrue --tail=50

# PostgREST
kubectl logs -n supabase -l app=rest --tail=50

# PostgreSQL
kubectl logs -n supabase postgres-0 --tail=50

Security Model Summary

1. JWT Token Roles

Role Access Level Use Case
anon Public, read-only (RLS enforced) Unauthenticated users
authenticated User data only (RLS enforced) Logged-in users
service_role Full access, bypasses RLS Backend services, admin tools

2. Token Validation Points

Kong (First Check): - Validates token exists in consumers list - Returns 401 if token not recognized

GoTrue/PostgREST (Second Check): - Validates JWT signature using JWT_SECRET - Checks expiration (exp claim) - Extracts role and applies appropriate permissions

3. Row Level Security (RLS)

PostgreSQL policies control data access:

-- Example: Users can only see their own transactions
CREATE POLICY "view_own_transactions"
ON money_tracker.transactions
FOR SELECT
USING (user_id = auth.uid());

-- auth.uid() extracts user ID from JWT

Critical: RLS is enforced by PostgreSQL, not by the application. Even if you bypass PostgREST and query directly, RLS still applies (unless using service_role).


External Resources