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:
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¶
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).
Related Documentation¶
- Supabase JWT Token Management - Token generation and rotation
- Supabase Multi-App Architecture - Schema-based isolation
- Supabase Deployment Lessons - Initial setup challenges