FastFort
Batteries-included authentication and admin framework for FastAPI.
Django-style model registration · Professional admin UI · JWT + session auth · Roles & permissions · Audit logging · SQLAlchemy & Tortoise · No Node.js required
[!WARNING] Early stage.
0.1.0is the first release and the public API is not stable yet — before 1.0, a minor release may contain breaking changes, each one listed under a Breaking heading in the changelog. Pin a version.
Why FastFort?
Every FastAPI project rebuilds the same things from scratch: an admin panel, login, refresh tokens, roles, an audit trail. Django ships all of that out of the box. FastAPI does not.
FastFort fills that gap.
uv add "fastfort[sqlalchemy]"
# main.py
from fastapi import FastAPI
from fastfort import FastFort, FastFortSettings
from fastfort.orm.sqlalchemy import SQLAlchemyBackend
from app.db import Base, session_factory
from app.models import User
app = FastAPI()
fort = FastFort(
settings=FastFortSettings(project_name="Shop"),
backend=SQLAlchemyBackend(session_factory=session_factory, base=Base),
)
fort.set_user_model(User, identity_field="email")
fort.autodiscover("app")
fort.mount(app)
# app/products/admin.py
from fastfort import admin
from app.models import Product
@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
list_display = ("id", "name", "price", "is_active", "created_at")
list_filter = ("is_active", "category")
search_fields = ("name", "description")
ordering = ("-created_at",)
select_related = ("category",)
icon = "box" # drawn beside the sidebar entry
# Offered once rows are selected. "delete" is built in; this adds another.
actions = ("delete", "archive")
@admin.action("Archive", icon="box")
async def archive(self, adapter, objects):
for product in objects:
await adapter.update(product, {"is_active": False})
return f"{len(objects)} products archived."
Create the first account and start the server:
uv run fastfort generate-secret --export # a signing key
FF_PASSWORD=... uv run fastfort createsuperuser \
--identity you@example.com --password-env FF_PASSWORD --no-input
uv run uvicorn main:app
Open http://127.0.0.1:8000/admin, sign in, and you have a list, a search box,
filters, sortable columns, numbered pagination, row selection with bulk actions,
and working create, edit and delete pages. Foreign keys become searchable
pickers -- backed by an autocomplete endpoint once the target table outgrows a
dropdown -- and many-to-many fields become removable chips.
None of it needs JavaScript to work. Every control is a real form input and every sort header a real link; the browser-side code upgrades them in place and gets out of the way when it is not there.
Field names are checked against the model when the admin is built, so a typo in
list_display is a start-up error naming every problem at once -- not a 500 the
first time someone opens that page.
Features
| 🎛 Django-style admin | @admin.register, list_display, list_filter, search_fields, fieldsets, actions |
| 🗑 Deletes you can trust | The confirmation counts what actually goes: rows that cascade, rows kept with the link cleared, and rows that block the delete outright — refused with a sentence instead of a constraint violation |
| 🎨 A UI you will not want to replace | Light and dark themes, brand colour from a single setting, ⌘K command palette, full keyboard navigation, real mobile layout |
| 🔐 Production-grade auth | Argon2id hashing, JWT access/refresh, token rotation with reuse detection, login lockout, CSRF protection |
| 👥 Roles and permissions | Object-level, row-level and field-level access control |
| 📝 Audit log | Who changed what and when, with an old → new diff |
| 🗄 Three databases | SQLite · PostgreSQL · MySQL, with identical behaviour |
| 🔌 Two ORMs, one admin | SQLAlchemy 2.0 and Tortoise ORM behind one adapter contract — and a conformance suite that asks both the same questions, so "the second one behaves the same" is a test rather than a claim |
| 🧩 Every column type | Text, numbers, money, dates, durations, UUIDs, JSON, hstore, arrays, enums, ranges and multiranges, inet/cidr/macaddr, bit strings — each with a real control, real validation and a filter where one makes sense. register_type adds your own |
| 🗺 PostGIS | All seven geometry kinds, drawn and edited on a hand-rolled slippy map — point, line, polygon with holes, and the multi-shapes. Spatial filters: within, intersects, and "5 km from here" |
| 🧠 Vector search | pgvector columns ranked by similarity — ?embedding__near=[…] with cosine, L2, L1 or inner product, a neighbour count and a distance bound |
| 🌍 Eleven languages | English, Uzbek, Russian, Turkish, German, French, Spanish, Chinese, Japanese, Korean and Arabic — the last of them right-to-left, which turns the whole layout around from one attribute. The catalogues ship in the package, so the admin is already translated the moment you install it — there is nothing to configure |
| ⌨️ A CLI that matters | createsuperuser so a fresh install has a way in, and check --deploy that exits non-zero |
| 📤 Export and import | The current view out as CSV, Excel or JSON — filters, search and ordering included. And back in again: the same parsers the form uses, foreign keys resolved by name or id, every bad cell reported at once with its line number, and nothing written unless the whole file parses. No openpyxl, no pandas |
| 📦 No Node.js | CSS and JavaScript ship pre-built inside the package, served Brotli-compressed with a gzip fallback — 24 KB of CSS and 29 KB of JavaScript on the wire |
How it is put together
UI (Jinja2 · CSS · HTMX) ─┐
Admin (ModelAdmin, forms) ─┤
Auth (tokens, permissions) ─┼──► Spec layer (immutable, JSON-serialisable) ◄── ORM adapters
Core (settings, registry) ─┘
Everything above the spec layer is ORM-agnostic, and that boundary is enforced by tests rather than by convention. Adding a new ORM therefore never touches the admin or the UI, and a JSON API for a future SPA front end comes for free from the same specs the templates render.
Installation
uv add "fastfort[sqlalchemy,postgres]" # SQLAlchemy on PostgreSQL
uv add "fastfort[sqlalchemy,mysql]" # SQLAlchemy on MySQL
uv add "fastfort[sqlalchemy,sqlite]" # SQLAlchemy on SQLite
uv add "fastfort[tortoise,postgres]" # Tortoise on PostgreSQL
uv add "fastfort[all]" # everything
Requires Python 3.11 or newer. The ORMs are separate extras and neither is imported at package level, so installing one never pulls in the other.
Tortoise instead of SQLAlchemy
Four lines differ. Everything else — the settings, @admin.register,
list_display, actions, export, import — is identical, because everything else
is above the ORM layer and never sees a model:
from tortoise import Tortoise
from fastfort.orm.tortoise import TortoiseBackend
await Tortoise.init(
db_url="postgres://…",
modules={"models": ["app.models"]},
# Tortoise 1.1 keeps its connections in a contextvar, and an ASGI server runs
# the lifespan in a different task from the requests. Without this flag the
# init above is invisible to every view: start-up looks healthy and the first
# page that touches the database is a 500. `RegisterTortoise` from
# `tortoise.contrib.fastapi` passes it for you.
_enable_global_fallback=True,
)
fort = FastFort(settings=FastFortSettings(...), backend=TortoiseBackend())
fort.set_user_model(User)
fort.mount(app)
Development
The only prerequisite is uv. No Node.js, ever.
uv sync --all-extras # set up the environment
uv run pytest # tests (SQLite)
uv run pytest --db=all # tests against all three databases (needs Docker)
uv run ruff check . # linting
uv run mypy fastfort # type checking
make check # every gate at once
Two scratch applications come with the repository, and they are the fastest way to see any of this working:
make sandbox # test_api/ on PostgreSQL — every column type,
# PostGIS geometry, pgvector, on :8000
make sandbox-tortoise # test_api_tortoise/ on SQLite — a different schema,
# the same admin, on :8001
Open them side by side. The models, the ORM and the database all differ; the admin does not, which is the whole of what the layering buys.
See CONTRIBUTING.md before opening a pull request, and SECURITY.md to report a vulnerability.
License
MIT © Matnazar
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file fastfort-0.3.1.tar.gz.
File metadata
- Download URL: fastfort-0.3.1.tar.gz
- Upload date:
- Size: 488.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
af745da86b9d59465da07d803a1210bd84cef51e01e30bfc54ab8d4e8aeef880
|
|
| MD5 |
f6a57ddd0dd5abb77bbf4122a9b1b069
|
|
| BLAKE2b-256 |
ec89d7805e27e5495955b05ca0194c700af0078e3e3946eefa1f355e0eb19e5d
|
Provenance
The following attestation bundles were made for fastfort-0.3.1.tar.gz:
Publisher:
publish.yml on Matnazar-Matnazarov/fastfort
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
fastfort-0.3.1.tar.gz -
Subject digest:
af745da86b9d59465da07d803a1210bd84cef51e01e30bfc54ab8d4e8aeef880 - Sigstore transparency entry: 2429016278
- Sigstore integration time:
-
Permalink:
Matnazar-Matnazarov/fastfort@9a3ce3b3e29cd3eb3c13e65d4e9f30dfe954b732 -
Branch / Tag:
refs/tags/v0.3.1 - Owner: https://github.com/Matnazar-Matnazarov
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@9a3ce3b3e29cd3eb3c13e65d4e9f30dfe954b732 -
Trigger Event:
push
-
Statement type:
File details
Details for the file fastfort-0.3.1-py3-none-any.whl.
File metadata
- Download URL: fastfort-0.3.1-py3-none-any.whl
- Upload date:
- Size: 398.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
222d1c60f96813ea79bf39a6564067318b522aa9df69c00b430f506e74829595
|
|
| MD5 |
f20d427f85e8d8e70d5ea574c2649917
|
|
| BLAKE2b-256 |
8995429d7dc18994a10da57321216e8b478e74d484bfaaa5e2659a6b7a1786b9
|
Provenance
The following attestation bundles were made for fastfort-0.3.1-py3-none-any.whl:
Publisher:
publish.yml on Matnazar-Matnazarov/fastfort
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
fastfort-0.3.1-py3-none-any.whl -
Subject digest:
222d1c60f96813ea79bf39a6564067318b522aa9df69c00b430f506e74829595 - Sigstore transparency entry: 2429016358
- Sigstore integration time:
-
Permalink:
Matnazar-Matnazarov/fastfort@9a3ce3b3e29cd3eb3c13e65d4e9f30dfe954b732 -
Branch / Tag:
refs/tags/v0.3.1 - Owner: https://github.com/Matnazar-Matnazarov
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@9a3ce3b3e29cd3eb3c13e65d4e9f30dfe954b732 -
Trigger Event:
push
-
Statement type: