A batteries-included FastAPI starter so you never rebuild the boring 70% again.
Distilled from many production apps — the best pattern from each.
Every request flows through the same clean spine: security middleware → router → auth & RBAC →
service layer → data, with Stripe, object storage, email, Redis and a background scheduler wired in
around it. The pieces above are real modules in app/, already connected and tested.
- App factory (
create_app()) + lifespan, cleanAPIRoutersplit (no monolith) - Config — typed
pydantic-settings, fully.env-driven - Async DB (SQLAlchemy 2.0) — SQLite by default, Postgres by setting
DATABASE_URL; optional read replica - Auth — JWT (in an httpOnly cookie and
Authorization: Bearer), revocable server-side sessions, refresh-token rotation,X-API-Key, TOTP 2FA, email verification + password reset - Real RBAC —
Role+Permissiontables,require_role(...)/require_permission(...)dependencies, seededadmin/user/supportroles - Admin panel — server-rendered: dashboard, users (+credit adjust, role assign), roles/permissions editor, tickets queue, error log, plans/subscriptions
- Stripe billing — credits (one-time top-ups → ledger) and subscriptions (Checkout + Customer Portal), idempotent webhook (dedup table) handling checkout, subscription sync, refunds & disputes
- Support tickets — user ↔ staff threads
- Ops — rotating-file logging, DB-persisted error log viewer, rate limiting (proxy-aware, in-memory → Redis auto), CSP + security headers, custom 404/422/500 pages, health + readiness endpoints, optional Sentry & Telegram alerts
- Static versioning —
?v={{ deploy_version }}+ immutable cache headers - Legal pages — Terms / Privacy / Refunds / Contact stubs
- Templates —
base.htmlwithtitle/head/content/scriptsblocks, working forms, zero CSS framework (style it yourself) - Fernet crypto helper for storing third-party API keys at rest (BYOK)
- Object storage — one API over local disk or any S3-compatible backend (AWS S3, Cloudflare R2, MinIO, Backblaze). Public files can be served through your own CDN domain (
S3_PUBLIC_CDN_URL, e.g.cdn.mydomain.com); private files use short-lived presigned URLs (locally: signed expiring links). Wired to avatars (public) and ticket attachments (private). - CSRF protection — double-submit token on every form; header-auth (Bearer / API key) requests are exempt
- 2FA QR codes — inline SVG (no external service) + one-time recovery codes
- Background jobs — APScheduler: session cleanup, expired-token purge, subscription dunning
- Audit log — logins, password/role/credit changes, impersonation; filterable at
/admin/audit - Social login — Google + GitHub OAuth (buttons appear when configured)
- Bot protection — Cloudflare Turnstile on register/login/forgot, auto-added to the CSP
- Session management —
/account/sessionslists devices; revoke one or sign out everywhere - Admin impersonation — "view as user" with a persistent banner and audit trail
- Settings / feature flags — DB-backed, editable at
/admin/settings, no redeploy - Quality harness — pytest suite, ruff, pre-commit, GitHub Actions CI, templated emails
- Account lifecycle — self-service deactivate (reversible by signing in again), delete with a 30-day grace period + scheduled purge, and a one-click JSON data export (GDPR)
- Profile & email change — edit display name/locale; changing email confirms the new address and notifies the old one, so a hijacked session can't silently move it
- Notifications — in-app centre + unread badge, per-kind email preferences, and one-click
/unsubscribe(no login) for non-transactional kinds - Admin lists that scale — pagination, sorting, filtering and CSV export on users / audit / errors / tickets / subscriptions, plus bulk actions
- Receipts & tax — customer billing profile (company, VAT ID, address), printable receipts, optional Stripe Tax (
automatic_tax+tax_id_collection) - Security pack — new-device login alerts, HIBP breached-password rejection (k-anonymity), per-user rate limits (not just per-IP), and terms versioning that forces re-acceptance
- Ops — admin-toggled maintenance mode (admins still get through), public
/statuspage, Prometheus/metrics, andscripts/backup_db.pyfor cron - Cookie consent — banner + analytics that only load after consent
- i18n — JSON catalogues in
app/locales/,{{ t("key") }}in templates,/locale/{code}switcher - SEO — OpenGraph/Twitter cards, canonical URLs,
robots.txt,sitemap.xml - Soft delete — tickets go to
/admin/tickets/trashand can be restored - Referrals — every user gets a share link (
/?ref=CODE), attributed at signup with an optional credit bonus
Every page is server-rendered and working — the screenshot above is the real /login. The template
ships with a deliberately tiny 20-line stylesheet so nothing fights your design: the markup, forms,
CSRF, flash messages and flows are done, and the look is entirely yours.
cp .env.example .env # then set SECRET_KEY at minimum
python -m venv .venv && source .venv/bin/activate # (Windows: .\.venv\Scripts\Activate.ps1)
pip install -r requirements.txt
uvicorn app.main:app --reloadOpen http://localhost:8000. The first account you register becomes admin
(FIRST_USER_IS_ADMIN=1), so register, then visit /admin.
Helper launchers: ./run.sh (Linux/macOS) or ./run.ps1 (Windows).
Set in .env (no code changes):
DATABASE_URL=postgresql+asyncpg://user:pass@localhost:5432/mydb
Then alembic upgrade head. The asyncpg (app) and psycopg (Alembic) drivers
are already in requirements.txt.
REDIS_URL=redis://localhost:6379/0
Left blank, everything runs in-memory (fine for a single worker).
Set STRIPE_SECRET_KEY, STRIPE_PUBLISHABLE_KEY, STRIPE_WEBHOOK_SECRET, then
create plans in /admin/billing (give each a Stripe price id). Point a
Stripe webhook at POST /billing/webhook. Credit packs use kind=credit with a
credits_granted amount; subscriptions use kind=subscription with an interval.
Local webhook testing:
stripe listen --forward-to localhost:8000/billing/webhookLocal disk is the default and needs no setup. To use a bucket:
STORAGE_BACKEND=s3
S3_ENDPOINT_URL=https://<account>.r2.cloudflarestorage.com # omit for AWS S3
S3_ACCESS_KEY_ID=...
S3_SECRET_ACCESS_KEY=...
S3_PUBLIC_BUCKET=my-public
S3_PRIVATE_BUCKET=my-private
S3_PUBLIC_CDN_URL=https://cdn.mydomain.com # public files served from your domain
S3_FORCE_PATH_STYLE=1 # MinIO / some R2 setups
Use it anywhere:
from app.services.storage import get_storage
s = get_storage()
url = await s.save_public("avatars/x.png", data, "image/png") # CDN URL if configured
await s.save_private("invoices/2026.pdf", pdf, "application/pdf")
link = await s.presigned_get("invoices/2026.pdf", expires=600) # expiring link
upload_url = await s.presigned_put("uploads/big.zip", "application/zip") # browser-direct (s3 only)pip install -r requirements-dev.txt
pytest # 22 tests covering auth, CSRF, 2FA, storage, tickets, billing, admin
ruff check .
pre-commit installgunicorn -c gunicorn.conf.py app.main:appSet ENV=prod (enables Secure cookies, disables /docs), HSTS_ENABLED=1
behind TLS, a strong SECRET_KEY, and run behind nginx/Cloudflare. If you run
more than one worker, set REDIS_URL so rate limits are shared and enable the
scheduler in one process only (SCHEDULER_ENABLED=1 there, 0 elsewhere).
Backups — add to cron on the VPS:
0 2 * * * cd /srv/app && .venv/bin/python scripts/backup_db.py >> logs/backup.log 2>&1
It uses SQLite's online-backup API or pg_dump -Fc, then prunes anything older
than BACKUP_KEEP_DAYS.
Maintenance mode is a DB flag — toggle it at /admin/settings (admins keep full
access, everyone else gets a 503 page). /metrics is Prometheus text format;
protect it with METRICS_TOKEN or block the path at your proxy.
app/
main.py factory + lifespan config.py typed settings
database.py async engine + sessions crypto.py Fernet (BYOK)
logging_config.py rotating logs redis_client optional redis
errors.py exception handlers seed.py RBAC seed
scheduler.py background jobs templating.py Jinja env + render()
ops.py maintenance + metrics MW pagination.py paging/sort/CSV
i18n.py JSON catalogues
models/ base, rbac, user, billing, billing_profile, ticket,
errorlog, recovery, audit, oauth, setting, notification
security/ passwords, tokens, deps, headers, ratelimit, csrf,
turnstile, breach_check
services/ credit_service, stripe_service, email, storage, twofactor,
audit, settings_service, account_service, notifications,
referrals
routers/ health, ops, auth, oauth, account, notifications, tickets,
pages, billing, files, admin/*
templates/ base + auth/ account/ admin/ errors/ legal/ emails/ partials/
locales/ en.json, es.json
static/ css/app.css, js/app.js
migrations/ Alembic
scripts/ backup_db.py
tests/ pytest suite
- Change
SECRET_KEY(and setENCRYPTION_KEY) before production. - The template ships without a hardcoded admin — the first registrant becomes
admin. Lock this down (
FIRST_USER_IS_ADMIN=0+ADMIN_EMAILS=...) once set up. - Rate limits, CSP, and Secure cookies are on by default; tune them in
.env.
