FraiseQL Authentication Deployment Guide¶
Production deployment guide for FraiseQL's authentication system.
FraiseQL is a Python runtime GraphQL framework served over FastAPI. Authentication
runs inside the FastAPI app (pure Python) — there is no separate auth server and
no compile step. You deploy a standard Python ASGI application and configure auth
through FraiseQLConfig / FRAISEQL_-prefixed environment variables.
Prerequisites¶
Required Knowledge:
- OAuth 2.0 / OIDC and JWT validation
- Containerization (Docker) and your orchestrator (Kubernetes, ECS, etc.)
- Python application packaging and ASGI servers (uvicorn / gunicorn)
- SSL/TLS certificate management
- PostgreSQL administration and backups
- Load balancing and reverse proxy configuration
- Security best practices and compliance
Required Software:
- FraiseQL (latest stable release) and Python 3.13+
uvicorn(and optionallygunicornfor multi-worker process management)- Docker 20.10+ (and Docker Compose 2+) or your container runtime
- Kubernetes 1.24+ (kubectl configured), if deploying to Kubernetes
- PostgreSQL 14+ database
- OpenSSL or a certificate management tool
- Nginx or another reverse proxy (optional)
Required Infrastructure:
- A container host or orchestrator (for deployment)
- PostgreSQL 14+ database (primary + replica for HA)
- An OIDC/JWT issuer for production auth (Auth0, or any issuer you front with Auth0
or validate with a custom
AuthProvider) - Domain with DNS setup
- SSL/TLS certificate (Let's Encrypt, commercial CA, or internal)
- Load balancer or Ingress controller
- Persistent storage for the database
- Backup storage system
Optional but Recommended:
- Kubernetes cert-manager for automatic certificate renewal
- Container registry (Docker Hub, ECR, GCR, etc.)
- A secrets management system (HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager)
- Monitoring, alerting, and log aggregation infrastructure
- Disaster recovery and backup testing
- Autoscaling configuration
Time Estimate: 2-4 hours for a Kubernetes deployment, 1-2 hours for Docker Compose.
Pre-Deployment Checklist¶
- Auth provider (Auth0 or custom JWT issuer) configured
- Database schema and migrations applied
- SSL/TLS certificates installed
-
FRAISEQL_environment variables configured (secrets via secret manager) - Token revocation store selected (
PostgreSQLRevocationStorefor production) - Monitoring and logging configured
- Backup strategy defined
- Security audit completed
- Load testing performed
- Runbook created
Environment Configuration¶
All FraiseQL settings can be supplied as environment variables prefixed with
FRAISEQL_. They map directly onto fields of FraiseQLConfig
(src/fraiseql/fastapi/config.py), e.g. FRAISEQL_AUTH_PROVIDER →
auth_provider, FRAISEQL_AUTH0_DOMAIN → auth0_domain.
Production Environment Variables¶
# Database (PostgreSQL only)
FRAISEQL_DATABASE_URL=postgresql://fraiseql_app:strong_password@prod-db.internal:5432/fraiseql
FRAISEQL_DATABASE_POOL_SIZE=20
FRAISEQL_DATABASE_POOL_RECYCLE=1800
# Environment (disables playground/introspection automatically)
FRAISEQL_ENVIRONMENT=production
# Authentication
FRAISEQL_AUTH_ENABLED=true
FRAISEQL_AUTH_PROVIDER=auth0 # auth0 | custom | none
# Auth0 settings (when FRAISEQL_AUTH_PROVIDER=auth0)
FRAISEQL_AUTH0_DOMAIN=myapp.auth0.com
FRAISEQL_AUTH0_API_IDENTIFIER=https://api.yourdomain.com
# FRAISEQL_AUTH0_ALGORITHMS=["RS256"] # defaults to RS256
# Token revocation
FRAISEQL_REVOCATION_ENABLED=true
FRAISEQL_REVOCATION_CHECK_ENABLED=true
FRAISEQL_REVOCATION_TTL=86400
# Server (uvicorn / gunicorn) — standard ASGI server settings
PORT=8000
When FRAISEQL_AUTH_PROVIDER=auth0, FraiseQL auto-creates an Auth0Provider from
auth0_domain / auth0_api_identifier at startup — you do not write provider code.
For any other OIDC issuer (Google, Keycloak, etc.), either front it with Auth0 or set
FRAISEQL_AUTH_PROVIDER=custom and supply an AuthProvider subclass that validates
that issuer's JWTs via its JWKS/issuer/audience.
Secrets: never bake credentials into the image. Inject
FRAISEQL_DATABASE_URLand any provider secrets at runtime from your platform's secret manager (Kubernetes Secrets, Vault, AWS/GCP Secrets Manager). The values above show shape, not real secrets.
.env file¶
For non-container hosts, FraiseQL loads a .env file automatically (pydantic-settings):
# Load deployment secrets into the process environment
source /etc/fraiseql/auth.env
# Verify critical variables (avoid printing secret values)
echo "Auth provider: $FRAISEQL_AUTH_PROVIDER"
echo "Auth0 domain: $FRAISEQL_AUTH0_DOMAIN"
echo "Environment: $FRAISEQL_ENVIRONMENT"
Database Setup¶
1. Create Database¶
# On the PostgreSQL server
sudo -u postgres psql
CREATE DATABASE fraiseql;
CREATE USER fraiseql_app WITH PASSWORD 'strong_password_here';
ALTER ROLE fraiseql_app SET client_encoding TO 'utf8';
ALTER ROLE fraiseql_app SET default_transaction_isolation TO 'read committed';
ALTER ROLE fraiseql_app SET timezone TO 'UTC';
GRANT ALL PRIVILEGES ON DATABASE fraiseql TO fraiseql_app;
\c fraiseql
GRANT ALL PRIVILEGES ON SCHEMA public TO fraiseql_app;
2. Token Revocation Table¶
In production, use PostgreSQLRevocationStore
(src/fraiseql/auth/token_revocation.py) so revocations survive restarts and are
shared across instances. The store creates and manages its own table
(default name tb_token_revocation) on first use:
from psycopg_pool import AsyncConnectionPool
from fraiseql.auth.token_revocation import PostgreSQLRevocationStore
pool = AsyncConnectionPool(conninfo="postgresql://...")
revocation_store = PostgreSQLRevocationStore(pool, table_name="tb_token_revocation")
# The table is created lazily on first revocation/check.
The in-memory store (InMemoryRevocationStore) is for development/testing only —
revocations are lost on restart and are not shared between processes.
3. Verify Connection¶
export FRAISEQL_DATABASE_URL="postgresql://fraiseql_app:strong_password_here@prod-db.internal:5432/fraiseql"
psql "$FRAISEQL_DATABASE_URL" -c "SELECT 1;"
Docker Deployment¶
FraiseQL is a Python package — the image installs fraiseql and runs the FastAPI
app with an ASGI server. The optional fraiseql-rs acceleration ships as a prebuilt
wheel, so no Rust toolchain and no compile step are needed.
Dockerfile¶
FROM python:3.13-slim
# libpq is needed by psycopg at runtime
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq5 \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Install FraiseQL (prebuilt wheels — no build step)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Your application code (types, queries, mutations, the FastAPI app object)
COPY app/ ./app/
EXPOSE 8000
# Run the ASGI app with multiple workers via gunicorn + uvicorn workers.
CMD ["gunicorn", "app.main:app", \
"-k", "uvicorn.workers.UvicornWorker", \
"-w", "4", "-b", "0.0.0.0:8000"]
requirements.txt pins fraiseql (and fraiseql-rs for the acceleration wheel).
For a single worker (or local development) you can run uvicorn app.main:app --host 0.0.0.0 --port 8000 instead.
app.main:apprefers to the FastAPI app returned bycreate_fraiseql_app(...)in yourapp/main.py. Auth is configured there (or viaFRAISEQL_env vars).
Docker Compose Production¶
services:
fraiseql:
build: .
container_name: fraiseql-auth
restart: always
environment:
FRAISEQL_DATABASE_URL: ${FRAISEQL_DATABASE_URL}
FRAISEQL_ENVIRONMENT: production
FRAISEQL_AUTH_ENABLED: "true"
FRAISEQL_AUTH_PROVIDER: auth0
FRAISEQL_AUTH0_DOMAIN: ${FRAISEQL_AUTH0_DOMAIN}
FRAISEQL_AUTH0_API_IDENTIFIER: ${FRAISEQL_AUTH0_API_IDENTIFIER}
ports:
- "8000:8000"
depends_on:
postgres:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
postgres:
image: postgres:15-alpine
container_name: fraiseql-db
restart: always
environment:
POSTGRES_DB: fraiseql
POSTGRES_USER: fraiseql_app
POSTGRES_PASSWORD: ${DATABASE_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U fraiseql_app"]
interval: 10s
timeout: 5s
retries: 5
nginx:
image: nginx:alpine
restart: always
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- /etc/letsencrypt:/etc/letsencrypt:ro
depends_on:
- fraiseql
volumes:
postgres_data:
Nginx Configuration¶
FraiseQL serves GraphQL at /graphql and exposes /health (liveness) and
/ready (readiness) probes. Put TLS termination and rate limiting at the proxy.
upstream fraiseql {
server fraiseql:8000;
}
# Rate-limit zones (declared at http context in your nginx.conf)
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=auth_limit:10m rate=1r/s;
server {
listen 80;
server_name api.yourdomain.com;
# Redirect HTTP to HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name api.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
# Security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
location /graphql {
limit_req zone=api_limit burst=20;
proxy_pass http://fraiseql;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location ~ ^/(health|ready)$ {
access_log off;
proxy_pass http://fraiseql;
}
}
Apply a stricter
auth_limitzone to any native-auth login routes you expose (fromfraiseql.auth.native's FastAPI router) if you use the native provider.
SSL/TLS Setup¶
Using Let's Encrypt¶
# Install certbot
sudo apt-get install certbot python3-certbot-nginx
# Get certificate
sudo certbot certonly --standalone -d api.yourdomain.com
# Auto-renewal
sudo systemctl enable certbot.timer
sudo systemctl start certbot.timer
# Verify renewal
sudo certbot renew --dry-run
For PostgreSQL connection security (TLS + SCRAM-SHA-256), configure pg_hba.conf
and add ?sslmode=require to your FRAISEQL_DATABASE_URL.
Kubernetes Deployment¶
apiVersion: apps/v1
kind: Deployment
metadata:
name: fraiseql
labels:
app: fraiseql
spec:
replicas: 3
selector:
matchLabels:
app: fraiseql
template:
metadata:
labels:
app: fraiseql
spec:
containers:
- name: fraiseql
image: your-registry/fraiseql:latest
ports:
- containerPort: 8000
env:
- name: FRAISEQL_ENVIRONMENT
value: production
- name: FRAISEQL_AUTH_PROVIDER
value: auth0
- name: FRAISEQL_DATABASE_URL
valueFrom:
secretKeyRef:
name: fraiseql-secrets
key: database-url
- name: FRAISEQL_AUTH0_DOMAIN
valueFrom:
secretKeyRef:
name: fraiseql-secrets
key: auth0-domain
- name: FRAISEQL_AUTH0_API_IDENTIFIER
valueFrom:
secretKeyRef:
name: fraiseql-secrets
key: auth0-api-identifier
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet:
path: /ready
port: 8000
initialDelaySeconds: 5
periodSeconds: 10
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
---
apiVersion: v1
kind: Service
metadata:
name: fraiseql
spec:
selector:
app: fraiseql
type: ClusterIP
ports:
- protocol: TCP
port: 80
targetPort: 8000
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: fraiseql
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: fraiseql
minReplicas: 3
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
Monitoring Setup¶
Prometheus Configuration¶
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'fraiseql'
static_configs:
- targets: ['localhost:8000']
metrics_path: '/metrics'
Use the /health and /ready endpoints for liveness/readiness, and your standard
ASGI/FastAPI metrics for latency and error-rate dashboards.
Backup Strategy¶
Database Backups¶
#!/bin/bash
# backup.sh
BACKUP_DIR="/backups/fraiseql"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
DB_NAME="fraiseql"
mkdir -p "$BACKUP_DIR"
# Full backup
pg_dump -h "$DB_HOST" -U fraiseql_app "$DB_NAME" | \
gzip > "$BACKUP_DIR/fraiseql_$TIMESTAMP.sql.gz"
# Keep only last 30 days
find "$BACKUP_DIR" -name "fraiseql_*.sql.gz" -mtime +30 -delete
# Upload to S3
aws s3 cp "$BACKUP_DIR/fraiseql_$TIMESTAMP.sql.gz" \
s3://fraiseql-backups/
Schedule with cron:
# Run daily at 2 AM
0 2 * * * /scripts/backup.sh
Restore from Backup¶
gunzip -c fraiseql_20260121_020000.sql.gz | \
psql -h prod-db.internal -U fraiseql_app fraiseql
Scaling¶
Horizontal Scaling¶
- Run multiple FraiseQL app instances behind a load balancer.
- Each instance connects to the same PostgreSQL database.
- Use
PostgreSQLRevocationStoreso token revocations are shared across instances. - The app is stateless (auth state lives in JWTs + PostgreSQL), so scaling is simple.
- Within a single instance, run multiple worker processes via
gunicorn -k uvicorn.workers.UvicornWorker -w N.
Vertical Scaling¶
Adjust resource limits, for example in Kubernetes:
kubectl set resources deployment fraiseql \
--limits=memory=1Gi,cpu=1000m \
--requests=memory=512Mi,cpu=500m
Performance Tuning¶
PostgreSQL Connection Pool¶
Tune the pool via FraiseQLConfig / env vars:
FRAISEQL_DATABASE_POOL_SIZE=50
FRAISEQL_DATABASE_MAX_OVERFLOW=20
FRAISEQL_DATABASE_POOL_RECYCLE=1800
JWKS Caching¶
When using Auth0 (or any JWKS-based issuer), the provider fetches and caches the
issuer's signing keys (JWKS) so per-request token validation does not hit the issuer
every time. Keep the app reachable to the issuer's .well-known/jwks.json endpoint,
and allow outbound egress from your pods/containers to the issuer. Cached keys are
refreshed automatically when an unknown key ID is seen (e.g. after key rotation).
High Availability¶
Multi-Region Setup¶
Region 1: Primary database
Region 2: Read replica
Region 3: Standby replica
Failover: managed PostgreSQL service or your replication tooling
Because auth runs inside each stateless app instance and revocations live in PostgreSQL, app-tier HA is just "run more replicas across zones". Database HA is the critical piece — use a primary + replica with automated failover.
Disaster Recovery¶
- RPO (Recovery Point Objective): 5 minutes
- RTO (Recovery Time Objective): 15 minutes
- Test failover monthly
Cost Optimization¶
Development:
- Single app instance / single worker
- Shared database
- In-memory revocation store
Production:
- 3x app instances (HA), multiple workers each
- Managed PostgreSQL (primary + replica)
PostgreSQLRevocationStore, monitoring, and backups
Monitoring Dashboard¶
Key metrics to monitor:
- Availability: % uptime (target: 99.9%)
- Latency: p50, p95, p99 (target: <100ms)
- Errors: error rate (target: <1%)
- Capacity: CPU, memory, database connections
Troubleshooting¶
App Won't Start¶
# Check logs
docker logs fraiseql
# Check database connection
psql "$FRAISEQL_DATABASE_URL" -c "SELECT 1"
# Check the auth issuer is reachable (Auth0 example)
curl "https://${FRAISEQL_AUTH0_DOMAIN}/.well-known/openid-configuration"
A missing auth0_domain/auth0_api_identifier with FRAISEQL_AUTH_PROVIDER=auth0
raises a configuration error at startup — set both env vars.
High Latency¶
-- Check database slow queries (requires pg_stat_statements)
SELECT * FROM pg_stat_statements ORDER BY total_exec_time DESC;
# Check auth issuer latency (Auth0 example)
time curl "https://${FRAISEQL_AUTH0_DOMAIN}/.well-known/jwks.json"
Database Connection Pool Exhausted¶
# Increase pool size
FRAISEQL_DATABASE_POOL_SIZE=100
-- Check active connections
SELECT count(*) FROM pg_stat_activity;
Access Denied¶
Denied operations surface as a GraphQL error with
extensions.code = "FORBIDDEN" (not an HTTP 4xx). Check the user's roles/permissions
in the JWT and the @requires_permission / @requires_role / Authorizer rules on
the affected operations.
See Also¶
Next Step: Deploy to production and monitor performance.