Skip to main content

FastFort

Batteries-included authentication and admin framework for FastAPI.

Django-style model registration · Professional admin UI · Session + JWT auth · PostGIS & pgvector · SQLAlchemy & Tortoise · No Node.js required

CI PyPI License: MIT Python

Documentation · Live demo · Changelog

[!WARNING] Pre-1.0. 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.

The badge above is the released one. This README describes main, which may be ahead of it.


Why FastFort?

Every FastAPI project rebuilds the same things from scratch: an admin panel, a sign-in form, password hashing, a token endpoint, refresh rotation, CSRF, an upload field that cannot be tricked. Django ships that. FastAPI does not.

FastFort does — for an async stack, with no build step and no Node.js. What it does not ship yet is listed below, plainly, rather than left for you to find out after installing.

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.

And a token API for the rest of your application

One setting, and the same user model and password hashes the admin uses are behind four routes in OAuth 2's shapes:

FastFortSettings(project_name="Shop", auth={"api_enabled": True})
POST /auth/token     identity + password  ->  access + refresh
POST /auth/refresh   refresh              ->  a new pair, retiring the old
POST /auth/logout    refresh              ->  204
GET  /auth/me        Bearer access        ->  who that is

The dependency is worth having even without the routes:

from fastfort.auth import bearer_user

current_user = bearer_user(fort)


@app.get("/orders")
async def orders(user: Annotated[User, Depends(current_user)]) -> list[Order]:
    return await orders_for(user)

It hands your route the user row, and rejects an account deactivated since the token was issued -- which a signature check alone would keep admitting until the token expired.


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, session cookies invalidated by a password change, login lockout with a growing delay, CSRF on every form, and a CSP that starts at default-src 'none'
🎫 A token API in one setting auth={"api_enabled": True} mounts /auth/token, /refresh, /logout and /me in OAuth 2's shapes. Refresh rotation with reuse detection: a token presented twice revokes the whole family. bearer_user(fort) is a Depends for your own routes, and it hands them the user row
🚦 Rate limiting, on by default Three budgets — reads, writes, and sign-in. Argon2 is slow by design, which makes the sign-in form the cheapest thing on any site to attack, so it is charged in middleware before the handler and a refused request never reaches the hash
📎 Uploads that are what they say Extension allow-list per field kind, a deny-list checked alongside it, magic numbers checked against the name, and a Content-Type at serve time decided from the bytes rather than the filename. hack.exe.png is stored as hack_exe.png
🗄 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 are hand-written files inside the package, served Brotli-compressed with a gzip fallback. An ordinary page costs about 32 KB of CSS and 33 KB of script on the wire; the map and the data editors are separate files, fetched only by a page that needs them. A test weighs the lot and fails when it grows

How it is put together

UI (Jinja2 · CSS · vanilla JS)  ─┐
Admin (ModelAdmin, forms)       ─┤
Auth (sessions, tokens, CSRF)   ─┼──►  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 second ORM never touched the admin or the UI — fastfort/orm/tortoise/ was written without a change anywhere above fastfort/orm/, and a conformance suite asks both backends the same questions so that "the second one behaves the same" is a test rather than a claim.

There is no front-end framework. ui/static/js/ is one IIFE, no modules, no build step, and every control works with JavaScript switched off — sorting and filtering are links, everything else is a form. The script upgrades them in place; it never owns them.

Not in the box yet

fastfort/contrib/ is a placeholder. Roles and permissions beyond is_staff/is_superuser, an audit log, and soft delete are not implemented — the spec layer carries the ChangeSet an audit log would store and the field-masking it would need, but nothing writes one. Earlier versions of this README listed them as features. They were a plan.

Inline editing of related rows is the other gap worth naming — Django's InlineModelAdmin, editing an invoice's lines on the invoice's own page. A related model is reachable, and editable, on its own page.


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

Metadata

Release files for fastfort 0.3.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for fastfort 0.3.2
File Size Uploaded
fastfort-0.3.2.tar.gz 535.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for fastfort 0.3.2
File Interpreter ABI Platform
fastfort-0.3.2-py3-none-any.whl Python 3 none any Details

Total release size: 965.9 kB

Release files / fastfort-0.3.2.tar.gz

Download URL fastfort-0.3.2.tar.gz
Size 535.5 kB
Tags Source
SHA-256 checksum
How to use checksums
1492bebd59bc41b1bed60428097a3191ea92cc5c63130ae50a7ecdb3e320dd9c
BLAKE2b-256 checksum
How to use checksums
910fef0cfbd3ba96278a1eeee7f718c2eced910d9ccc84f0ca1d224368fd44ae
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 13, 2026.

Transparency log

Release files / fastfort-0.3.2-py3-none-any.whl

Download URL fastfort-0.3.2-py3-none-any.whl
Size 430.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e6f18fbae1fc484eed2c263d65f7431f0b477b57d8a71d383cada74c4d64ee5c
BLAKE2b-256 checksum
How to use checksums
fef6e9306f2c774b9ec98ea7b9d8cf51d76e0f2a0d8c67e0c8d7565b067981c7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 13, 2026.

Transparency log

Release history Release notifications | RSS feed

0.7.0

2 release files

0.6.0

2 release files

0.5.0

2 release files

0.4.4

2 release files

0.4.3

2 release files

0.4.2

2 release files

0.4.1

2 release files

0.4.0

2 release files

This release

0.3.2 This release

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page