Why Self-Host Your Authentication?
Authentication is the single most security-critical piece of infrastructure in any application. It controls who gets in, what they can access, and how their identity is verified. Yet most companies outsource this entirely to SaaS providers like Auth0, Clerk, or Firebase Auth.
That works until it doesn't. Here's when it breaks down:
Data Sovereignty
When you use a hosted auth provider, every phone number, email address, session token, and authentication event flows through their servers. For companies subject to GDPR, HIPAA, SOC 2, or FedRAMP, this creates a compliance liability. You're trusting a third party with PII that regulators hold you accountable for.
Self-hosting means your user data never leaves your infrastructure. Phone numbers used for OTP verification stay in your database. Session tokens are generated and validated on your servers. Audit logs live in your monitoring stack. When a regulator asks "where does this data live?", the answer is simple: your VPC, your region, your control.
Compliance Requirements
Many industries have strict requirements about data residency and processing:
- Healthcare (HIPAA) — patient identity data must be stored in compliant infrastructure with BAA agreements. Most auth SaaS providers don't sign BAAs.
- Finance (PCI DSS, SOX) — authentication systems are in scope for PCI compliance. Self-hosting lets you control the boundary.
- Government (FedRAMP) — federal workloads require FedRAMP-authorized services or self-hosted alternatives in GovCloud.
- EU (GDPR Art. 28) — data processor agreements are required. Self-hosting eliminates the processor relationship entirely.
Cost Control
Auth0's pricing starts reasonable but scales aggressively. At 10,000 MAU, you're paying $240/month. At 50,000 MAU, it's $1,150/month. At 100,000 MAU, you're well past $2,000/month. These are authentication costs alone — before any business logic.
Self-hosted Auth1 runs on a single t3.medium instance ($30/month) handling up to
500,000 MAU. Add a managed PostgreSQL ($15/month) and Redis ($15/month) and you're looking
at $60/month total for half a million users. That's a 97% cost reduction
compared to Auth0 at the same scale.
No Vendor Lock-in
SaaS auth providers store your users in their database. Migrating away means exporting user data (if they even allow it), re-hashing passwords (if they expose hashes — most don't), and rewriting every integration point. Auth0 migrations routinely take 3-6 months.
With self-hosted auth, your user table is a PostgreSQL table you own. Want to switch to a different auth system? Export the table. Want to add custom fields? Alter the table. Want to run custom queries against auth data? Connect directly.
To be fair: if you have fewer than 5,000 users, no compliance requirements, and no engineering team to maintain infrastructure, hosted auth is perfectly fine. Auth0 and Clerk are excellent products. This guide is for teams that have outgrown that model.
Self-Hosted Options Compared
There are several self-hosted authentication systems available today. Here's an honest comparison of the major options:
| Feature | Auth0 | Keycloak | Authelia | Auth1 |
|---|---|---|---|---|
| Self-hostable | No (SaaS only) | Yes | Yes | Yes |
| Setup complexity | N/A | High (Java, realm config) | Medium (YAML config) | Low (Docker one-liner) |
| SMS OTP built-in | Yes | Plugin required | No | Yes (Twilio + SNS) |
| Fraud detection | Bot detection only | No | No | VOIP blocking, risk scoring |
| Multi-tenant | Yes | Yes (realms) | No | Yes (API key per tenant) |
| White-label | Enterprise only | Theme system | No | Yes (per-tenant branding) |
| Memory usage | N/A | 512MB - 2GB (JVM) | ~50MB | ~80MB |
| Language | N/A | Java | Go | Node.js |
Why Not Keycloak?
Keycloak is the most commonly recommended self-hosted auth solution, and it's genuinely powerful. It supports SAML, OIDC, LDAP federation, and complex realm hierarchies. But for most teams, it's massive overkill.
Keycloak requires a JVM (512MB minimum heap), a complex realm/client/role configuration, and deep understanding of SAML vs OIDC protocol flows. The admin console alone has hundreds of settings. Most startups need phone/email OTP, session management, and webhook callbacks. Keycloak can do that, but you'll spend weeks configuring what should take minutes.
Why Not Authelia?
Authelia is excellent as a reverse-proxy authentication layer. It's lightweight (Go binary), supports TOTP/WebAuthn, and integrates well with Traefik and Nginx. But it's designed as a gateway auth layer, not a user management API. It doesn't provide SMS OTP, user signup flows, API key management, or multi-tenant support. If you need a full authentication API (not just a proxy), Authelia isn't the right tool.
Deploy Auth1 with Docker
Auth1 ships as a single Docker image with all dependencies built in. Here's the fastest path to a working self-hosted auth system.
Prerequisites
- Docker and Docker Compose installed
- A PostgreSQL database (v13+)
- A Redis instance (v6+)
- A Twilio account (for SMS OTP) — optional, can use AWS SNS instead
- A domain with HTTPS (for production)
Step 1: Docker Compose File
Create a docker-compose.yml that includes Auth1, PostgreSQL, and Redis.
This gives you a complete auth stack in one file:
version: "3.8" services: auth1: image: auth1/auth1-server:latest ports: - "3000:3000" environment: - DATABASE_URL=postgresql://auth1:secret@postgres:5432/auth1_db - REDIS_URL=redis://redis:6379 - JWT_SECRET=your-256-bit-secret-here - TWILIO_ACCOUNT_SID=your_twilio_sid - TWILIO_AUTH_TOKEN=your_twilio_token - TWILIO_MESSAGING_SERVICE_SID=your_messaging_sid - APP_URL=https://auth.yourdomain.com - NODE_ENV=production depends_on: - postgres - redis restart: always postgres: image: postgres:16-alpine environment: - POSTGRES_USER=auth1 - POSTGRES_PASSWORD=secret - POSTGRES_DB=auth1_db volumes: - pgdata:/var/lib/postgresql/data restart: always redis: image: redis:7-alpine command: redis-server --maxmemory 128mb --maxmemory-policy allkeys-lru restart: always volumes: pgdata:
Step 2: Start the Stack
# Start everything docker compose up -d # Check logs docker compose logs -f auth1 # You should see: # Auth1 server running on port 3000 # Database connected: auth1_db # Redis connected: redis://redis:6379 # Migrations applied: 85 migrations
Step 3: Create Your First Tenant
Auth1 is multi-tenant by default. Each tenant gets its own API key, user pool, and branding configuration. Create your first tenant:
curl -X POST http://localhost:3000/api/admin/tenants \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $ADMIN_TOKEN" \ -d '{ "name": "My App", "slug": "myapp", "settings": { "otp_length": 6, "otp_expiry_minutes": 10, "max_otp_attempts": 5, "rate_limit_per_hour": 5, "voip_blocking": true, "sms_branding": "MyApp" } }' # Response: # { # "tenant_id": "tn_abc123", # "api_key": "auth1_pk_myapp_d1617d338c62...", # "name": "My App" # }
Step 4: Test Authentication
# Request an OTP curl -X POST http://localhost:3000/api/auth/request \ -H "Content-Type: application/json" \ -H "x-api-key: auth1_pk_myapp_d1617d338c62..." \ -d '{"phone": "+15551234567"}' # Verify the OTP curl -X POST http://localhost:3000/api/auth/verify \ -H "Content-Type: application/json" \ -H "x-api-key: auth1_pk_myapp_d1617d338c62..." \ -d '{"phone": "+15551234567", "code": "482913"}' # Response: # { # "verified": true, # "token": "eyJhbGciOiJIUzI1NiIs...", # "user_id": "usr_xyz789" # }
Environment Variables Reference
Auth1 is configured entirely through environment variables. No config files, no admin panels for core settings. Here's the complete reference:
| Variable | Description | Default |
|---|---|---|
DATABASE_URL |
PostgreSQL connection string | required |
REDIS_URL |
Redis connection string for session storage and rate limiting | required |
JWT_SECRET |
256-bit secret for signing JWTs. Generate with openssl rand -hex 32 |
required |
PORT |
Server listen port | 3000 |
TWILIO_ACCOUNT_SID |
Twilio account SID for SMS OTP | optional |
TWILIO_AUTH_TOKEN |
Twilio auth token | optional |
TWILIO_MESSAGING_SERVICE_SID |
Twilio Messaging Service for branded SMS | optional |
AWS_SNS_REGION |
AWS region for SNS failover SMS | us-east-1 |
SNS_ORIGINATION_NUMBER |
AWS SNS origination phone number | optional |
APP_URL |
Public URL for callbacks and email links | http://localhost:3000 |
CORS_ORIGINS |
Comma-separated allowed origins | * |
LOG_LEVEL |
Logging verbosity: debug, info, warn, error | info |
RATE_LIMIT_WINDOW_MS |
Rate limit window in milliseconds | 3600000 |
RATE_LIMIT_MAX |
Max requests per window per IP | 100 |
Configure PostgreSQL
Auth1 uses PostgreSQL for all persistent data: users, tenants, sessions, audit logs, and OTP records. The Docker Compose setup above includes PostgreSQL, but for production you'll want a managed service or a properly tuned instance.
Production PostgreSQL Settings
-- Connection pooling max_connections = 200 shared_buffers = '256MB' -- Write performance wal_buffers = '16MB' checkpoint_completion_target = 0.9 -- Query planning effective_cache_size = '768MB' random_page_cost = 1.1 -- SSD storage -- Logging (for audit compliance) log_statement = 'mod' log_connections = on log_disconnections = on
Using AWS RDS
For most production deployments, AWS RDS or Google Cloud SQL is the simplest option.
Use a db.t3.medium instance ($60/month) with 20GB SSD storage. Enable
automatic backups and set retention to 7 days. Your connection string:
DATABASE_URL=postgresql://auth1:strong_password@your-rds-instance.region.rds.amazonaws.com:5432/auth1_db?sslmode=require
Configure Redis
Redis handles three critical functions in Auth1:
- Session storage — JWT sessions with configurable TTL
- Rate limiting — sliding window rate limits per phone number and IP
- OTP storage — temporary OTP codes with automatic expiry
For production, use AWS ElastiCache or a managed Redis service. A cache.t3.micro
instance ($12/month) handles up to 100,000 concurrent sessions comfortably.
Auth1 uses Redis as a cache, not as primary storage. All critical data is in PostgreSQL.
If Redis restarts, active OTPs will expire (users just request a new one) and rate
limit counters reset. This is by design — Redis should be configured with
maxmemory-policy allkeys-lru for optimal performance.
Configure SMS (Twilio)
Auth1 supports two SMS providers: Twilio (primary) and AWS SNS (failover). For most deployments, Twilio is recommended because it supports Messaging Services, which enable toll-free numbers and automatic carrier compliance.
Step 1: Create a Twilio Messaging Service
- Log in to the Twilio Console
- Go to Messaging → Services → Create Messaging Service
- Name it (e.g., "Auth1-OTP")
- Add a toll-free phone number as a sender
- Copy the Messaging Service SID (starts with
MG)
Step 2: Set Environment Variables
TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx TWILIO_AUTH_TOKEN=your_auth_token_here TWILIO_MESSAGING_SERVICE_SID=MGxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Auth1 automatically sends branded SMS messages: "Your {TenantName} verification code is: 482913. Expires in 10 minutes."
The tenant name is pulled from your tenant configuration, so each app using your
Auth1 instance gets its own branding.
Set Up HTTPS
Never run authentication over HTTP in production. Use a reverse proxy with TLS termination. Here's a minimal Nginx configuration with Let's Encrypt:
server { listen 443 ssl http2; server_name auth.yourdomain.com; ssl_certificate /etc/letsencrypt/live/auth.yourdomain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/auth.yourdomain.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; location / { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; 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; } } server { listen 80; server_name auth.yourdomain.com; return 301 https://$host$request_uri; }
# Install certbot sudo apt install certbot python3-certbot-nginx # Get certificate sudo certbot --nginx -d auth.yourdomain.com # Auto-renewal is configured automatically
Kubernetes Deployment
For larger deployments or teams already running Kubernetes, Auth1 deploys cleanly as a Deployment with a Service and Ingress. Here's a production-ready manifest:
apiVersion: apps/v1 kind: Deployment metadata: name: auth1 labels: app: auth1 spec: replicas: 3 selector: matchLabels: app: auth1 template: metadata: labels: app: auth1 spec: containers: - name: auth1 image: auth1/auth1-server:latest ports: - containerPort: 3000 env: - name: DATABASE_URL valueFrom: secretKeyRef: name: auth1-secrets key: database-url - name: REDIS_URL valueFrom: secretKeyRef: name: auth1-secrets key: redis-url - name: JWT_SECRET valueFrom: secretKeyRef: name: auth1-secrets key: jwt-secret resources: requests: memory: "128Mi" cpu: "100m" limits: memory: "256Mi" cpu: "500m" livenessProbe: httpGet: path: /health port: 3000 initialDelaySeconds: 10 periodSeconds: 30 readinessProbe: httpGet: path: /health port: 3000 initialDelaySeconds: 5 periodSeconds: 10 --- apiVersion: v1 kind: Service metadata: name: auth1-service spec: selector: app: auth1 ports: - port: 80 targetPort: 3000 type: ClusterIP --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: auth1-ingress annotations: cert-manager.io/cluster-issuer: letsencrypt-prod spec: tls: - hosts: - auth.yourdomain.com secretName: auth1-tls rules: - host: auth.yourdomain.com http: paths: - path: / pathType: Prefix backend: service: name: auth1-service port: number: 80
# Create secrets kubectl create secret generic auth1-secrets \ --from-literal=database-url="postgresql://auth1:pass@db:5432/auth1_db" \ --from-literal=redis-url="redis://redis:6379" \ --from-literal=jwt-secret="$(openssl rand -hex 32)" # Apply manifests kubectl apply -f auth1-k8s.yaml # Check status kubectl get pods -l app=auth1
Monitoring and Scaling
Health Check Endpoint
Auth1 exposes a /health endpoint that returns the status of all
dependencies:
{
"status": "healthy",
"uptime": 847293,
"database": "connected",
"redis": "connected",
"version": "2.4.1",
"memory_mb": 78
}
Key Metrics to Monitor
Authentication Rate
Track OTP requests and verifications per minute. A sudden spike could indicate a brute-force attack. Auth1 logs these to stdout for ingestion by Datadog, CloudWatch, or Prometheus.
Fraud Detection Rate
Monitor the percentage of requests blocked by VOIP detection and risk scoring. Healthy applications typically see 2-5% block rates. Higher suggests you're under attack.
SMS Delivery Rate
Track Twilio delivery callbacks. Delivery rates below 95% indicate carrier issues or invalid phone numbers. Auth1 stores delivery status per OTP request.
Database Connection Pool
Monitor active PostgreSQL connections. Auth1 uses a connection pool of 20 by default. If you're consistently above 80% utilization, increase the pool or scale horizontally.
Horizontal Scaling
Auth1 is stateless — all state lives in PostgreSQL and Redis. This means you can run multiple instances behind a load balancer without sticky sessions. For most applications:
- 1 instance — handles up to 500 auth requests/second
- 3 instances — handles up to 1,500 auth requests/second with redundancy
- 10 instances — handles up to 5,000 auth requests/second (enterprise scale)
The bottleneck is almost always the database, not Auth1 itself. Use PgBouncer for connection pooling and read replicas for audit log queries if you're at enterprise scale.
Before going live: (1) Set a strong JWT_SECRET with openssl rand -hex 32.
(2) Enable HTTPS with a valid TLS certificate. (3) Set CORS_ORIGINS to your
specific domains (not *). (4) Configure automated PostgreSQL backups.
(5) Set up monitoring alerts on the /health endpoint.
(6) Review rate limit settings for your expected traffic volume.