Introduction
RapidAuth is a pure backend authentication framework for FastAPI. It exposes a clean JSON REST API — no HTML pages, no server-side form rendering. You plug it into your FastAPI application and immediately get a complete, production-grade auth system.
Your frontend (React, Vue, Next.js, mobile app) talks to the endpoints over HTTP. RapidAuth handles everything else.
Philosophy
- Backend only. Every endpoint returns JSON.
- Zero configuration to start. One line mounts the full system.
- Scales from dev to production. Sensible defaults during development; explicit configuration for production.
- ORM-agnostic. Tortoise ORM 1.x and SQLAlchemy (async & sync) — auto-detected.
- Secure by default. bcrypt/argon2, refresh rotation, rate limiting, token blacklisting out of the box.
What you get
One app.include_router(auth.router) call mounts 14 production-ready endpoints:
| Method | Endpoint | Description |
|---|---|---|
| POST | /auth/register | Create account, return token pair |
| POST | /auth/login | Login with username or email + password |
| GET | /auth/me | Return current authenticated user |
| POST | /auth/refresh | Issue new token pair, revoke old refresh |
| POST | /auth/logout | Blacklist current access token |
| POST | /auth/change-password | Change password (old password required) |
| POST | /auth/reset-password | Request password reset email |
| POST | /auth/reset-password/confirm | Confirm reset with token + new password |
| GET | /auth/verify-email | Verify email — returns JSON |
| POST | /auth/verify-email | Verify email with token in body |
| POST | /auth/resend-verification | Resend verification email |
| POST | /auth/revoke-all | Revoke all refresh tokens for user |
| GET | /auth/{provider}/login | Redirect to OAuth2 provider |
| GET | /auth/{provider}/callback | Handle OAuth2 callback |
Installation
Install RapidAuth from PyPI. Choose the extras that match your ORM.
# Base package only
pip install rapidauth
# With Tortoise ORM (recommended async ORM)
pip install "rapidauth[tortoise]"
# With SQLAlchemy async or sync
pip install "rapidauth[sqlalchemy]"
# Everything (Tortoise + SQLAlchemy + uvicorn)
pip install "rapidauth[all]"
CLI scaffold
Generate a complete working project in seconds:
rapidauth --default-setup sqlite # Tortoise ORM + SQLite
rapidauth --default-setup sqlalchemy --async # SQLAlchemy async
rapidauth --default-setup postgresql --async # SQLAlchemy + PostgreSQL
# Generates: database.py models.py main.py
rapidauth init # .env.example + auth_config.py
rapidauth version # print installed version
Requirements
- Python ≥ 3.10
- FastAPI ≥ 0.100
- Tortoise ORM ≥ 1.0 (if using Tortoise)
- SQLAlchemy ≥ 2.0 (if using SQLAlchemy)
Project Setup
Tortoise ORM 1.x users: Use RegisterTortoise inside a lifespan, not the old register_tortoise(). See the setup below.
Tortoise ORM (recommended)
from tortoise import fields
from tortoise.models import Model
class User(Model):
id = fields.IntField(pk=True)
username = fields.CharField(max_length=100, unique=True)
email = fields.CharField(max_length=254, unique=True)
password = fields.CharField(max_length=200)
is_active = fields.BooleanField(default=True)
is_verified = fields.BooleanField(default=True)
roles = fields.JSONField(default=list)
permissions = fields.JSONField(default=list)
created_at = fields.DatetimeField(auto_now_add=True)
class Meta:
table = "users"
from contextlib import asynccontextmanager
from fastapi import FastAPI, Depends
from tortoise.contrib.fastapi import RegisterTortoise
from rapidauth import RapidAuth
from models import User
@asynccontextmanager
async def lifespan(app):
async with RegisterTortoise(
app,
db_url="sqlite://./app.db",
modules={"models": ["models"]},
generate_schemas=True,
):
yield
app = FastAPI(lifespan=lifespan)
auth = RapidAuth(user_model=User, jwt_secret="change-this-in-production")
app.include_router(auth.router)
get_current_user = auth.get_current_user()
@app.get("/profile")
async def profile(user = Depends(get_current_user)):
return {"id": user.id, "username": user.username, "email": user.email}
Run with: uvicorn main:app --reload. Open http://localhost:8000/docs to see all 14 endpoints in Swagger UI.
SQLAlchemy async
from contextlib import asynccontextmanager
from fastapi import FastAPI
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, sessionmaker
from sqlalchemy import Boolean, Integer, String, JSON
from rapidauth import RapidAuth
engine = create_async_engine("sqlite+aiosqlite:///./app.db")
AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
class Base(DeclarativeBase): pass
class User(Base):
__tablename__ = "users"
id : Mapped[int] = mapped_column(Integer, primary_key=True)
username : Mapped[str] = mapped_column(String(100), unique=True)
email : Mapped[str] = mapped_column(String(254), unique=True)
password : Mapped[str] = mapped_column(String(200))
is_active : Mapped[bool] = mapped_column(Boolean, default=True)
is_verified : Mapped[bool] = mapped_column(Boolean, default=False)
roles : Mapped[list] = mapped_column(JSON, default=list)
async def get_db():
async with AsyncSessionLocal() as session:
yield session
@asynccontextmanager
async def lifespan(app):
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
yield
app = FastAPI(lifespan=lifespan)
auth = RapidAuth(user_model=User, jwt_secret="your-secret", get_db=get_db)
app.include_router(auth.router)
Authentication Flow
RapidAuth implements a standard OAuth2-style flow with JWT tokens. Here is the full lifecycle:
Registration
Client sends credentials
POST /auth/register with {"username", "email", "password"}
RapidAuth validates & creates user
Checks username/email uniqueness, hashes password with bcrypt/argon2, saves to DB.
Token pair issued
Returns access_token (15 min) and refresh_token (7 days).
Optional: verification email sent
If email_verification_required=True and email is configured, a verification link is emailed.
Login
Client sends credentials
POST /auth/login with {"username": "alice"} or {"username": "alice@example.com"}.
Rate limit check
IP checked against sliding window. 5 failed attempts triggers a 5-minute lockout.
Credentials verified
Password hashed and compared timing-safely. Checks is_active and is_verified.
Token pair returned
Fresh access_token + refresh_token. Rate limit counter reset.
Token Refresh
Client sends refresh token
POST /auth/refresh with {"refresh_token": "..."} or via cookie.
Token validated & revoked
Old refresh token removed from store immediately (rotation).
New token pair issued
Fresh access + refresh tokens returned. Any replay of the old refresh token returns 401.
JWT System
RapidAuth uses HS256 JWT tokens by default. Both access and refresh tokens carry a jti (unique ID) that enables instant revocation.
Token types
| Token | Default TTL | Stored in | Purpose |
|---|---|---|---|
| Access token | 900s (15 min) | Client memory | Authenticate API requests via Bearer header |
| Refresh token | 604800s (7 days) | Token store | Obtain new access tokens without re-login |
Configuration
auth = RapidAuth(
user_model=User,
jwt_secret="your-secret", # shorthand
# — or full dict —
jwt={
"secret": "your-secret-min-32-chars",
"algorithm": "HS256", # HS256 | HS384 | HS512
"access_expire": 900, # seconds
"refresh_expire": 604800, # seconds
},
)
Refresh token delivery
auth = RapidAuth(
...
# "body" → refresh_token in JSON response body (default, easiest for SPAs)
# "cookie" → httpOnly cookie only (more secure)
# "both" → token in both body and cookie
refresh_token_mode="cookie",
cookie={
"httponly": True,
"secure": True, # True = HTTPS only
"samesite": "lax", # "lax" | "strict" | "none"
"domain": None, # set for cross-subdomain
},
)
Session Management
Refresh tokens are persisted in a token store. The default is an in-memory dictionary — sufficient for development and single-process deployments.
The default in-memory store is lost on server restart. Use a Redis store for production multi-process deployments.
Custom store interface
Implement these four methods to plug in any backend:
from datetime import datetime, timezone
class RedisTokenStore:
def __init__(self, redis):
self.redis = redis
async def save(self, token_hash: str, user_id: str, expires_at: datetime) -> None:
"""Persist a refresh token hash with its owner and expiry."""
ttl = int((expires_at - datetime.now(timezone.utc)).total_seconds())
await self.redis.setex(f"rt:{token_hash}", ttl, user_id)
async def get(self, token_hash: str) -> dict | None:
"""Return {"user_id": "..."} if the token exists, else None."""
val = await self.redis.get(f"rt:{token_hash}")
return {"user_id": val.decode()} if val else None
async def delete(self, token_hash: str) -> None:
"""Delete a single token (called on rotation or explicit logout)."""
await self.redis.delete(f"rt:{token_hash}")
async def revoke_all_for_user(self, user_id: str) -> None:
"""Revoke all tokens for a user (POST /auth/revoke-all)."""
# Implement with a user→tokens reverse index if needed
pass
# Usage
import redis.asyncio as aioredis
redis_client = aioredis.from_url("redis://localhost")
auth = RapidAuth(
user_model=User,
jwt_secret="...",
token_store=RedisTokenStore(redis_client),
)
Register User
Creates a new user account and returns a token pair. If email is configured and email_verification_required=True, a verification email is sent. Otherwise, a welcome email is sent.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| username | string | required | 3–100 chars, letters/digits/_ and - only |
| string | required | Valid email address | |
| password | string | required | Minimum 8 characters |
Response 201
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "bearer"
}curl -X POST http://localhost:8000/auth/register \
-H "Content-Type: application/json" \
-d '{"username":"alice","email":"alice@example.com","password":"AlicePass1!"}'Login
Authenticate with username or email and password. Rate-limited to 5 failed attempts per IP per 5 minutes.
Request body
| Field | Type | Description |
|---|---|---|
| username | string | Username or email address |
| password | string | Account password |
Error responses
| Status | Detail |
|---|---|
| 401 | Invalid credentials |
| 403 | Account is inactive / Email not verified |
| 429 | Too many login attempts. Try again in 5 minutes. |
curl -X POST http://localhost:8000/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"alice","password":"AlicePass1!"}'Logout
Blacklists the current access token's jti and clears any refresh token cookies. The token is immediately invalid — no need to wait for expiry.
curl -X POST http://localhost:8000/auth/logout \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"Refresh Token
Issues a new token pair. The old refresh token is immediately revoked — any replay attempt returns 401. Accepts token from body or refresh_token cookie.
curl -X POST http://localhost:8000/auth/refresh \
-H "Content-Type: application/json" \
-d '{"refresh_token":"YOUR_REFRESH_TOKEN"}'Get Current User
Returns the authenticated user's profile fields.
{
"id": 1,
"username": "alice",
"email": "alice@example.com",
"is_active": true,
"is_verified": true,
"roles": ["user"]
}Password Reset
Request reset
Creates a 1-hour one-time reset token and sends an email. Always returns 200 regardless of whether the email is registered — this prevents user enumeration.
If reset_password_url is not configured, a dev-mode email is sent showing the raw token + curl command for testing.
Confirm reset
Validates the token, updates the password, and invalidates the token. Token expires in 1 hour and is single-use.
{"token": "ONE_TIME_TOKEN", "new_password": "NewPassword1!"}# 1. Request reset
curl -X POST http://localhost:8000/auth/reset-password \
-H "Content-Type: application/json" \
-d '{"email":"alice@example.com"}'
# 2. Confirm (token from email)
curl -X POST http://localhost:8000/auth/reset-password/confirm \
-H "Content-Type: application/json" \
-d '{"token":"TOKEN_FROM_EMAIL","new_password":"NewPass1!"}'Email Verification
Verifies the token and marks the user as verified. Returns JSON — suitable as the direct email link target in development. In production, set verify_email_url to your frontend page.
Resends verification email. Accepts a Bearer token (logged-in user) OR an email in the request body (no auth required). Useful when email_verification_required=True and the user's token expired before they verified.
Google OAuth
RapidAuth handles the full Google OAuth 2.0 flow. When a user authenticates with Google, RapidAuth creates or retrieves their account automatically and returns a token pair.
Step 1 — Create a Google Cloud project
Go to Google Cloud Console
Navigate to console.cloud.google.com and create a new project (or select an existing one).
Enable Google+ API
Go to APIs & Services → Library and enable the Google+ API (or People API).
Create OAuth credentials
Go to APIs & Services → Credentials → Create Credentials → OAuth 2.0 Client ID. Choose Web application.
Set Authorized redirect URIs
Add your callback URL: https://api.myapp.com/auth/google/callback
For local development: http://localhost:8000/auth/google/callback
Copy Client ID and Secret
Download the credentials JSON or copy the Client ID and Client Secret values.
Step 2 — Configure RapidAuth
auth = RapidAuth(
user_model=User,
jwt_secret="your-secret",
social_auth={
"google": {
"client_id": "1234567890-abc.apps.googleusercontent.com",
"client_secret": "GOCSPX-your_client_secret",
"redirect_uri": "https://api.myapp.com/auth/google/callback",
}
},
)Step 3 — Use the endpoints
RapidAuth automatically adds these two routes:
| Method | Endpoint | Description |
|---|---|---|
| GET | /auth/google/login | Redirects user to Google consent screen |
| GET | /auth/google/callback | Handles callback, creates/logs in user, returns token pair |
// Redirect user to Google login
window.location.href = "https://api.myapp.com/auth/google/login";
// After Google redirects to your callback URL, RapidAuth
// automatically handles the token exchange and returns:
// { access_token: "...", refresh_token: "...", token_type: "bearer" }Google OAuth users are created automatically on first login with is_verified=True. A random password is generated — these users log in only via Google.
GitHub OAuth
Authenticate users with their GitHub accounts. RapidAuth uses GitHub's OAuth 2.0 flow to retrieve user info and create accounts automatically.
Step 1 — Create a GitHub OAuth App
Go to GitHub Settings
Navigate to github.com → Settings → Developer settings → OAuth Apps → New OAuth App.
Fill in the application details
Application name: Your app name
Homepage URL: https://myapp.com
Authorization callback URL: https://api.myapp.com/auth/github/callback
Register the application
Click Register application. Copy the Client ID.
Generate a Client Secret
On the app settings page, click Generate a new client secret. Copy it immediately — it won't be shown again.
Step 2 — Configure RapidAuth
auth = RapidAuth(
user_model=User,
jwt_secret="your-secret",
social_auth={
"github": {
"client_id": "Iv1.abc1234567890abc",
"client_secret": "your_github_client_secret",
"redirect_uri": "https://api.myapp.com/auth/github/callback",
}
},
)Flow
| Method | Endpoint | Description |
|---|---|---|
| GET | /auth/github/login | Redirects to GitHub authorization page |
| GET | /auth/github/callback | Handles code exchange, creates/logs in user |
GitHub may not provide an email address for users with private emails. In that case, RapidAuth generates a placeholder: {username}@github.oauth.
Facebook Login
Allow users to log in with their Facebook accounts using Meta's OAuth 2.0 implementation.
Step 1 — Create a Meta App
Go to Meta for Developers
Navigate to developers.facebook.com → My Apps → Create App. Choose "Allow people to log in with their Facebook account".
Set up Facebook Login product
In your app dashboard, click Set Up under Facebook Login. Under Valid OAuth Redirect URIs, add: https://api.myapp.com/auth/facebook/callback
Get App ID and Secret
Go to Settings → Basic to copy your App ID and App Secret.
Go Live
Toggle your app to Live mode before production. In development mode, only test users can authenticate.
Configure RapidAuth
auth = RapidAuth(
user_model=User,
jwt_secret="your-secret",
social_auth={
"facebook": {
"client_id": "1234567890123456", # App ID
"client_secret": "your_facebook_app_secret",
"redirect_uri": "https://api.myapp.com/auth/facebook/callback",
}
},
)Callback Handling
RapidAuth handles the entire OAuth callback flow internally. You do not need to write any callback logic.
Complete OAuth flow
1. User initiates login
Frontend redirects user to GET /auth/{provider}/login
2. Provider redirect
RapidAuth redirects to the provider's consent screen (Google/GitHub/Facebook). User grants permission.
3. Authorization code returned
Provider redirects to GET /auth/{provider}/callback?code=…. RapidAuth receives the authorization code.
4. Token exchange
RapidAuth exchanges the code for an access token with the provider's token endpoint.
5. User info retrieved
RapidAuth calls the provider's user info endpoint to get email and username.
6. Account created or retrieved
If a user with this email exists → log them in. If not → create a new account automatically with is_verified=True.
7. Token pair returned
RapidAuth returns {"access_token":"…","refresh_token":"…"}. Frontend stores and uses these for subsequent API calls.
Handling multiple providers, same email
If a user already has a password-based account with the same email and tries to log in via OAuth, RapidAuth will find the existing account by email and log them in. No duplicate accounts are created.
Environment Variables
Never hard-code secrets in source code. Use environment variables loaded from a .env file.
# JWT
JWT_SECRET=your-very-long-random-secret-min-32-chars
JWT_ACCESS_EXPIRE=900
JWT_REFRESH_EXPIRE=604800
# Email (SMTP)
EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=587
EMAIL_USERNAME=you@gmail.com
EMAIL_PASSWORD=xxxx_xxxx_xxxx_xxxx
EMAIL_FROM=you@gmail.com
# URLs
BASE_URL=https://api.myapp.com
VERIFY_EMAIL_URL=https://myapp.com/verify-email
RESET_PASSWORD_URL=https://myapp.com/reset-password
# Google OAuth
GOOGLE_CLIENT_ID=1234567890-abc.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=GOCSPX-your_secret
GOOGLE_REDIRECT_URI=https://api.myapp.com/auth/google/callback
# GitHub OAuth
GITHUB_CLIENT_ID=Iv1.abc1234567890
GITHUB_CLIENT_SECRET=your_github_secret
GITHUB_REDIRECT_URI=https://api.myapp.com/auth/github/callback
# Facebook OAuth
FACEBOOK_CLIENT_ID=1234567890123456
FACEBOOK_CLIENT_SECRET=your_facebook_secret
FACEBOOK_REDIRECT_URI=https://api.myapp.com/auth/facebook/callback
# Database
DATABASE_URL=postgresql+asyncpg://user:pass@localhost/mydbimport os
from dotenv import load_dotenv
load_dotenv()
auth = RapidAuth(
user_model=User,
jwt_secret=os.getenv("JWT_SECRET"),
email={
"host": os.getenv("EMAIL_HOST"),
"port": int(os.getenv("EMAIL_PORT", 587)),
"username": os.getenv("EMAIL_USERNAME"),
"password": os.getenv("EMAIL_PASSWORD"),
"from_email": os.getenv("EMAIL_FROM"),
},
base_url=os.getenv("BASE_URL", "http://localhost:8000"),
verify_email_url=os.getenv("VERIFY_EMAIL_URL"),
reset_password_url=os.getenv("RESET_PASSWORD_URL"),
social_auth={
"google": {
"client_id": os.getenv("GOOGLE_CLIENT_ID"),
"client_secret": os.getenv("GOOGLE_CLIENT_SECRET"),
"redirect_uri": os.getenv("GOOGLE_REDIRECT_URI"),
},
"github": {
"client_id": os.getenv("GITHUB_CLIENT_ID"),
"client_secret": os.getenv("GITHUB_CLIENT_SECRET"),
"redirect_uri": os.getenv("GITHUB_REDIRECT_URI"),
},
},
)Password Hashing
RapidAuth supports two battle-tested hashing algorithms. Both use timing-safe comparison to prevent timing attacks.
| Algorithm | Default | Strength | Speed |
|---|---|---|---|
| bcrypt | default | Very strong (cost 12) | Moderate |
| argon2 | — | Winner of Password Hashing Competition 2015 | Slightly slower |
auth = RapidAuth(
user_model=User,
jwt_secret="...",
password_hasher="bcrypt", # "bcrypt" (default) | "argon2"
)
# Manual hashing (e.g. for seed scripts)
hashed = auth.hash_password("my-password")
valid = auth.verify_password("my-password", hashed) # TrueTiming attack prevention: When a username is not found, RapidAuth performs a dummy hash verification to ensure the response time is the same regardless of whether the user exists.
Token Expiration
| Token | Default | Config key | Recommendation |
|---|---|---|---|
| Access token | 15 min (900s) | access_expire | Keep short. 5–15 min for high security. |
| Refresh token | 7 days (604800s) | refresh_expire | 7–30 days. Longer = better UX, shorter = more secure. |
| Email verify token | 24 hours | Not configurable | Single-use, invalidated on success. |
| Password reset token | 1 hour | Not configurable | Single-use, invalidated on success. |
auth = RapidAuth(
user_model=User,
jwt={
"secret": "your-secret",
"access_expire": 300, # 5 minutes (high-security apps)
"refresh_expire": 2592000, # 30 days (mobile apps)
},
)Rate Limiting
RapidAuth includes a built-in sliding-window rate limiter on the login endpoint to prevent brute-force attacks.
How it works
- Tracks failed login attempts per client IP.
- After
max_login_attemptsfailures, the IP is locked out forlockout_seconds. - On successful login, the counter is reset immediately.
- Returns
HTTP 429during lockout.
auth = RapidAuth(
user_model=User,
jwt_secret="...",
rate_limit={
"enabled": True, # default True
"max_login_attempts": 5, # failures before lockout
"lockout_seconds": 300, # 5-minute lockout window
},
)Forwarded IPs (X-Forwarded-For header) are respected automatically. Make sure your reverse proxy (nginx, Caddy) sets this header correctly.
Middleware Usage
RapidAuth provides an AuthMiddleware that populates request.state.user on every request — useful for logging, analytics, or global access control.
auth.add_middleware(app)
@app.get("/")
async def home(request: Request):
user = request.state.user
if user.is_authenticated:
return {"hello": user.username}
return {"hello": "anonymous"}
@app.middleware("http")
async def log_requests(request: Request, call_next):
user = request.state.user
if user.is_authenticated:
print(f"Request from: {user.username}")
return await call_next(request)Unauthenticated requests get a proxy object with is_authenticated = False. Accessing any user field raises no exception — it simply returns None.
RBAC with dependency injection
from fastapi import Depends
from rapidauth.exceptions import PermissionDeniedError
def require_role(*roles: str):
"""Factory that returns a FastAPI dependency checking for roles."""
async def dep(user = Depends(auth.get_current_user())):
for role in roles:
if role not in (user.roles or []):
raise PermissionDeniedError(f"Role '{role}' required")
return user
return dep
@app.get("/admin")
async def admin(user = Depends(require_role("admin"))):
return {"ok": True}
@app.get("/staff")
async def staff(user = Depends(require_role("admin", "moderator"))):
return {"ok": True}Custom Backend Integration
You can override the token store, use custom field names, or hook into the user creation flow.
Custom field names
If your User model uses different column names than the defaults:
auth = RapidAuth(
user_model=User,
jwt_secret="...",
# Override field names to match your model
username_field="login", # default: "username"
email_field="email_address", # default: "email"
password_field="pwd_hash", # default: "password"
id_field="user_id", # default: "id"
is_active_field="active", # default: "is_active"
is_verified_field="verified", # default: "is_verified"
roles_field="user_roles", # default: "roles"
)Programmatic user management
manager = auth.manager
# Create user (e.g. in a seed script)
user = await auth.create_user(
"admin", "admin@example.com", "AdminPass1!",
extra={"roles": ["admin"], "permissions": ["users.write"]},
)
# Find users
user = await manager.get_by_email("alice@example.com")
user = await manager.get_by_username("alice")
user = await manager.get_by_id(42)
# Check credentials
user = await auth.authenticate("alice", "AlicePass1!")
# Token operations
access, refresh = await manager.create_token_pair(user)
await manager.revoke_all_tokens(user)
# Password operations
hashed = auth.hash_password("password")
ok = auth.verify_password("password", hashed)Database Adapters
RapidAuth auto-detects your ORM by inspecting the model's class hierarchy at startup. You rarely need to configure this manually.
| ORM | Detection | Session | Notes |
|---|---|---|---|
| Tortoise ORM | Auto (inspects __mro__) | Global (via RegisterTortoise) | Requires Tortoise 1.x + RegisterTortoise lifespan |
| SQLAlchemy async | Auto | Per-request via get_db | Pass get_db=get_db to RapidAuth |
| SQLAlchemy sync | Auto | Per-request via get_db | Blocks event loop — dev only |
| SQLModel | Auto | Per-request via get_db | Built on SQLAlchemy — same config |
Tortoise ORM 1.x — common error
RuntimeError: No TortoiseContext is currently active.
This happens when using Tortoise.init() or the old register_tortoise() without the global fallback. Fix: use RegisterTortoise inside a FastAPI lifespan as shown in the Project Setup section.
from tortoise.contrib.fastapi import RegisterTortoise
@asynccontextmanager
async def lifespan(app):
async with RegisterTortoise( # sets _enable_global_fallback=True
app,
db_url="sqlite://./app.db",
modules={"models": ["models"]},
generate_schemas=True,
):
yield
app = FastAPI(lifespan=lifespan)# DON'T use register_tortoise() or Tortoise.init() without the global fallback
from tortoise.contrib.fastapi import register_tortoise
register_tortoise(app, ...) # broken in Tortoise 1.x — no global context set
# Also broken:
await Tortoise.init(db_url="...", modules={"models": [...]}) # no global fallback