Modern authentication for FastAPI
Project description
Belgie
Self-hosted, type-safe authentication for FastAPI that makes Google OAuth and secure session cookies work with almost zero glue code. Keep your data, skip per-user SaaS bills, and still get a polished developer experience.
Who this is for
- FastAPI teams that want Google sign-in and protected routes today, not after weeks of wiring.
- Product engineers who prefer first-class type hints and adapter-driven design over magic.
- Startups that would rather own their user data and avoid per-MAU pricing from hosted identity vendors.
What it solves
- End-to-end Google OAuth 2.0 flow with CSRF-safe state storage.
- Sliding-window, signed session cookies (no JWT juggling required).
- Drop-in FastAPI dependencies for
auth.user,auth.session, and scoped access. - A thin SQLAlchemy adapter that works with your existing models.
- Hooks so you can plug in logging, analytics, or audit trails without forking.
How it compares
- fastapi-users: feature-rich but now in maintenance mode and optimized for password-plus-OAuth flows. Belgie focuses on OAuth + session UX, keeps the surface area small, and ships type-driven adapters out of the box.
- Hosted identity (Auth0, Clerk, Supabase Auth): great UIs and more providers, but billed per Monthly Active User and hosted off your stack. Belgie is MIT-licensed, runs in your app, and never charges per user.
Features at a glance
- Google OAuth provider with ready-made router (
/auth/signin/google,/auth/callback/google,/auth/signout). - Session manager with sliding expiry and secure cookie defaults (HttpOnly, SameSite, Secure).
- Scope-aware dependency for route protection (
Security(auth.user, scopes=[...])). - Modern Python (3.12+), full typing, and protocol-based models.
- Event hooks and utility helpers for custom workflows.
Installation
pip install belgie
# or with uv
uv add belgie
For SQLAlchemy adapter support:
pip install belgie[alchemy]
# or with uv
uv add belgie[alchemy]
Optional extras: belgie[mcp], belgie[oauth], or belgie[all].
Quick start
1) Define models
from datetime import UTC, datetime
from uuid import UUID, uuid4
from sqlalchemy import ForeignKey, String
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "users"
id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid4)
email: Mapped[str] = mapped_column(String(255), unique=True, index=True)
name: Mapped[str | None] = mapped_column(String(255), nullable=True)
image: Mapped[str | None] = mapped_column(String(500), nullable=True)
email_verified: Mapped[bool] = mapped_column(default=False)
created_at: Mapped[datetime] = mapped_column(default=lambda: datetime.now(UTC))
updated_at: Mapped[datetime] = mapped_column(default=lambda: datetime.now(UTC))
class Account(Base):
__tablename__ = "accounts"
id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid4)
user_id: Mapped[UUID] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"))
provider: Mapped[str] = mapped_column(String(50))
provider_account_id: Mapped[str] = mapped_column(String(255))
access_token: Mapped[str | None] = mapped_column(String(1000), nullable=True)
refresh_token: Mapped[str | None] = mapped_column(String(1000), nullable=True)
expires_at: Mapped[datetime | None] = mapped_column(nullable=True)
scope: Mapped[str | None] = mapped_column(String(500), nullable=True)
created_at: Mapped[datetime] = mapped_column(default=lambda: datetime.now(UTC))
class Session(Base):
__tablename__ = "sessions"
id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid4)
user_id: Mapped[UUID] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"))
expires_at: Mapped[datetime] = mapped_column(index=True)
created_at: Mapped[datetime] = mapped_column(default=lambda: datetime.now(UTC))
class OAuthState(Base):
__tablename__ = "oauth_states"
id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid4)
state: Mapped[str] = mapped_column(String(255), unique=True, index=True)
expires_at: Mapped[datetime] = mapped_column(index=True)
created_at: Mapped[datetime] = mapped_column(default=lambda: datetime.now(UTC))
2) Configure Belgie
from belgie.auth import Auth, AuthSettings, GoogleProviderSettings
from belgie_alchemy import AlchemyAdapter
settings = AuthSettings(
secret="your-secret-key",
base_url="http://localhost:8000",
)
adapter = AlchemyAdapter(
user=User,
account=Account,
session=Session,
oauth_state=OAuthState,
)
auth = Auth(
settings=settings,
adapter=adapter,
providers={
"google": GoogleProviderSettings(
client_id="your-google-client-id",
client_secret="your-google-client-secret",
redirect_uri="http://localhost:8000/auth/provider/google/callback",
scopes=["openid", "email", "profile"],
),
},
)
3) Add routes to FastAPI
from fastapi import Depends, FastAPI, Security
app = FastAPI()
app.include_router(auth.router)
@app.get("/")
async def home():
return {"message": "Welcome! Visit /auth/provider/google/signin to sign in"}
@app.get("/protected")
async def protected(user: User = Depends(auth.user)):
return {"email": user.email}
@app.get("/profile")
async def profile(user: User = Security(auth.user, scopes=["profile"])):
return {"name": user.name, "email": user.email}
Run it:
uvicorn main:app --reload
Visit http://localhost:8000/auth/signin/google to sign in.
Configuration shortcuts
- Environment variables:
BELGIE_SECRET,BELGIE_BASE_URL,BELGIE_GOOGLE_CLIENT_ID,BELGIE_GOOGLE_CLIENT_SECRET,BELGIE_GOOGLE_REDIRECT_URI(loaded automatically byAuthSettings()). - Session tuning:
SessionSettings(cookie_name, max_age, update_age)controls lifetime and sliding refresh. - Cookie hardening:
CookieSettings(http_only, secure, same_site)for production-ready defaults.
Router endpoints
GET /auth/signin/google– start OAuth flowGET /auth/callback/google– handle Google callbackPOST /auth/signout– clear session cookie and invalidate server session
Limitations today
- Google is the only built-in provider; more providers and email/password are on the roadmap.
- You manage your own database migrations and deployment (by design—no third-party control plane).
Why teams pick Belgie
- Keep control of data and infra while getting a batteries-included OAuth flow.
- Minimal surface area: a single
Authinstance exposes router + dependencies. - Modern typing and clear protocols reduce integration mistakes and make refactors safer.
- MIT license, zero per-user costs.
Documentation and examples
- docs/quickstart.md for full walkthrough
- examples/auth for a runnable app
Contributing
MIT licensed. Issues and PRs welcome.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
belgie-0.1.0a2.tar.gz
(16.2 kB
view details)
File details
Details for the file belgie-0.1.0a2.tar.gz.
File metadata
- Download URL: belgie-0.1.0a2.tar.gz
- Upload date:
- Size: 16.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.9.28 {"installer":{"name":"uv","version":"0.9.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8ed1e8851bcd92e87c7ce36da7b3bfbc8beea52b8957a9164481024597a6b1fb
|
|
| MD5 |
418857ec9e14765e36de2d21aa5bdc39
|
|
| BLAKE2b-256 |
9e647840a51f3e79cdb8c64e87ba416c42e4638bedc95abf843ef58fa7144f2c
|