Open source authentication infrastructure. Self-host on AWS for $120/month instead of $19,000.
At 1,000,000 MAU, Clerk costs ~$19,000/month. Fortis on AWS costs ~$120/month.
Same reliability. No vendor lock-in. You own your data.
- Email/password authentication
- JWT access tokens + refresh tokens
- Multi-region support (latency-based routing via Route 53)
- Regional refresh token tables (fast token ops, no cross-region reads)
- Email verification + password reset (SES, SMTP, Resend, Sendgrid)
- Rate limiting (brute force protection, no Redis required)
- Webhooks (
onUserCreated,onLogin,onPasswordReset) - Session management (list, revoke, revoke-all)
- Admin API (list users, ban, delete, impersonate)
- Hooks (
beforeLogin,afterRegister, etc.) - Drop-in middleware for Express and Next.js
- Database adapters: DynamoDB, Postgres, MongoDB
- Zero framework lock-in
npx @fortis/cli initThis creates your DynamoDB tables, configures SES, and outputs ready-to-deploy Terraform for AWS.
Or self-host locally in one command:
docker compose upUSERS (global)
β
βΌ
ROUTE 53 β latency-based routing
β β β
βΌ βΌ βΌ
CLOUDFRONT CLOUDFRONT CLOUDFRONT
(ap-southeast-2) (eu-west-2) (us-east-1)
β β β
ββ JWT valid? ββββ€ β
β βββΊ CloudFront Function (edge) β
β verify signature <1ms β
β no Lambda, no DynamoDB β
β βΊ 401 if invalid β
β βΊ pass-through if valid β
β β β
βΌ βΌ βΌ
API GATEWAY API GATEWAY API GATEWAY
β β β
ββ /register β β
β βββΊ LOCAL Lambda β
β βββΊ fortis-users-au βββββββΌβββββββββββββββββββββββ
β [Global Table replica]β auto-replicates ~1s β
β β β
ββ /login β β β
β βββΊ LOCAL Lambda β β
β βββΊ fortis-users-au β fortis-users-uk fortis-users-us
β [local read, ~5ms] β [replica] [replica]
β β
ββ /token/refreshβ β
β βββΊ LOCAL Lambda β
β βββΊ fortis-tokens-au β fortis-tokens-uk fortis-tokens-us
β [regional, ~5ms] β [regional] [regional]
β β
ββ /logout β β
β βββΊ LOCAL Lambda β
β βββΊ delete from LOCAL tokens table
β β
ββ /logout-all β β
βββΊ LOCAL Lambda
βββΊ parallel deletes across ALL 3 regional token tables
CloudFront role: JWT validation runs as a CloudFront Function at the edge β no Lambda invocation, no DynamoDB read. Invalid tokens are rejected before they ever reach your API Gateway. This is the hottest path in any auth system and costs ~$0.10/1M requests.
The following are planned but not implemented in v0.1. PRs welcome.
| Missing | Notes |
|---|---|
| MFA / TOTP | Google Authenticator style 2FA |
| Social login | OAuth2 β Google, GitHub, Apple |
| Magic link login | Passwordless email login |
| Passkeys / WebAuthn | FIDO2 hardware key support |
| Organizations / RBAC | Multi-tenant team management |
| Pre-built UI components | React login/register forms |
| Session list Lambda | List active devices per user |
| Admin UI dashboard | Web UI for user management |
| Audit logs | Who did what and when |
| SCIM provisioning | Enterprise directory sync |
fortis-users (DynamoDB Global Table β replicated in all 3 regions)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β PK SK Attributes β
β USER#email@x.com PROFILE userId, passwordHash β
β EMAIL_TOKEN#<token> VERIFY userId, expiresAt β
β EMAIL_TOKEN#<token> RESET userId, expiresAt β
β RL#login:ip ATTEMPTS count, expiresAt β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
GSI: userId-index
Replicas: us-east-1 Β· eu-west-2 Β· ap-southeast-2
Replication lag: ~1β2s (eventual consistency)
Write routing: ALL writes go to PRIMARY region (us-east-1)
to prevent duplicate email on concurrent register
fortis-tokens-{au|uk|us} (one regional table per region, NOT replicated)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β PK SK Attributes β
β TOKEN#<token> SESSION userId, sessionId, β
β expiresAt, ip, ua β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
GSI: userId-index
TTL: expiresAt (auto-delete)
| Operation | Hops | Latency |
|---|---|---|
| Register | local Lambda β primary region write | 20β50ms β once |
| Login | local Lambda β local Global Table replica | 5β20ms β once |
| Token refresh | local Lambda β local tokens table | 5β20ms β frequent |
| JWT validation | CloudFront Function (edge) | <1ms β never hits Lambda |
| Logout | local Lambda β local tokens table | 5β20ms |
| Component | Cost/month |
|---|---|
| DynamoDB users (Global Table, 3 regions) | ~$15 |
| DynamoDB tokens (3 regional tables) | ~$8 |
| Lambda (3 regions) | ~$22 |
| API Gateway | ~$20 |
| CloudFront + CloudFront Functions | ~$5 |
| Route 53 | ~$5 |
| WAF + CloudWatch + Secrets | ~$26 |
| Total | ~$101/month |
| MAU | Fortis (AWS) | Clerk | Auth0 |
|---|---|---|---|
| 10k | ~$5 | Free | Free |
| 100k | ~$25 | ~$1,800 | ~$850 |
| 500k | ~$75 | ~$9,800 | ~$3,500 |
| 1M | ~$101 | ~$19,020 | ~$7,000+ |
Fortis uses DynamoDB Global Tables for the users table (replicated across all 3 regions), regional token tables per region, and CloudFront Functions for edge JWT validation. Full breakdown in the architecture section above.
pnpm add @fortis/core @fortis/adapter-dynamodb @fortis/adapter-sesimport { createFortis } from '@fortis/core'
import { dynamodbAdapter } from '@fortis/adapter-dynamodb'
import { sesAdapter } from '@fortis/adapter-ses'
import { fortisMiddleware } from '@fortis/middleware-express'
const fortis = createFortis({
db: dynamodbAdapter({
usersTable: 'fortis-users',
tokensTable: 'fortis-tokens-us',
region: 'us-east-1',
}),
email: sesAdapter({ region: 'us-east-1' }),
jwt: {
secret: process.env.JWT_SECRET,
accessTokenTTL: '15m',
refreshTokenTTL: '30d',
},
})
app.use(fortisMiddleware(fortis))// app/api/auth/[...fortis]/route.ts
import { createFortis } from '@fortis/core'
import { nextjsHandler } from '@fortis/middleware-nextjs'
export const { GET, POST } = nextjsHandler(fortis)| Method | Endpoint | Description |
|---|---|---|
| POST | /auth/register |
Create account |
| POST | /auth/login |
Login, returns JWT + refresh token |
| POST | /auth/logout |
Invalidate current session |
| POST | /auth/logout-all |
Invalidate all sessions |
| POST | /auth/token/refresh |
Refresh access token |
| POST | /auth/email/verify |
Send verification email |
| POST | /auth/email/confirm |
Confirm email token |
| POST | /auth/password/reset |
Request password reset |
| POST | /auth/password/confirm |
Confirm password reset |
| GET | /auth/sessions |
List active sessions |
| GET | /auth/admin/users |
List users (admin) |
| POST | /auth/admin/users/:id/ban |
Ban user (admin) |
const fortis = createFortis({
hooks: {
beforeLogin: async ({ email }) => {
const banned = await db.isBanned(email)
if (banned) throw new ForbiddenError('Account suspended')
},
afterRegister: async ({ user }) => {
await crm.createContact(user)
},
},
})const fortis = createFortis({
webhooks: {
url: 'https://yourapp.com/webhooks/auth',
secret: process.env.WEBHOOK_SECRET,
events: ['user.created', 'user.login', 'password.reset'],
},
})| Adapter | Package |
|---|---|
| AWS SES | @fortis/adapter-ses |
| SMTP | @fortis/adapter-smtp |
| Resend | @fortis/adapter-resend |
| Sendgrid | @fortis/adapter-sendgrid |
| Postmark | @fortis/adapter-postmark |
Email cost is not included in Fortis cost estimates and varies significantly by provider and auth strategy. At 1M MAU, email volume depends entirely on your flows:
Scenario Emails/month SES cost Resend cost Email verify only (10% new users) ~100k ~$10 ~$90 Verify + password reset (5% MAU) ~150k ~$15 ~$135 Magic link login (every login) ~5M+ ~$500 ~$4,500+ Magic link or OTP-per-login strategies can make email your dominant cost β more than all other infrastructure combined. Choose your auth strategy with email cost in mind. SES is cheapest at scale ($0.10/1k). Resend and Postmark are more expensive but have better deliverability tooling out of the box.
| Adapter | Package |
|---|---|
| DynamoDB | @fortis/adapter-dynamodb |
| Postgres | @fortis/adapter-postgres |
| MongoDB | @fortis/adapter-mongodb |
Full multi-region setup with Terraform:
cd infra/terraform/aws
terraform init
terraform apply -var="primary_region=us-east-1" \
-var="regions=[\"us-east-1\",\"eu-west-2\",\"ap-southeast-2\"]"See self-hosting docs for full guide.
See CONTRIBUTING.md.
Writing a new adapter? See docs/writing-an-adapter.md.
Apache 2.0