Files

7.6 KiB
Raw Permalink Blame History

❯ Please create a skeleton project for a typical web application that serves HTML and JSON data. The skeleton should build out a basic scaffold using python FastAPI, HTMX, and an ORM that can use sqlite or postgres. The skeleton should also support an admin dashboard, RBAC, and JWT token management interfaces for users. Since this is a skeleton configuration should be accepted either by file or by environment variables. There should be a Docker file as well. Please ask any clarifying questions and propose a plan for this project.

● I'll ask a few key questions to shape the skeleton, since several decisions here meaningfully change the structure.

● User answered Claude's questions: ⎿  · Which ORM / data layer approach do you want? → SQLAlchemy 2.0 async (Recommended) · How should the admin dashboard be built? → Custom HTMX dashboard (Recommended) · What auth/JWT depth do you want in the skeleton? → Access + refresh + revocation (Recommended) · Python tooling / dependency manager? → uv (Recommended)

● Good — all defaults confirmed. Let me present a concrete plan for approval before building.

● Here's my proposed plan for the FastAPI skeleton.

Stack (confirmed)

  • FastAPI + Jinja2 + HTMX (server-rendered HTML, JSON API alongside)
  • SQLAlchemy 2.0 async + Alembic migrations; SQLite (aiosqlite) or Postgres (asyncpg) via one DATABASE_URL
  • JWT: access + refresh tokens with rotation and DB-backed revocation (logout / blocklist)
  • RBAC: users ↔ roles ↔ permissions, enforced via FastAPI dependencies
  • Custom HTMX admin dashboard (user/role CRUD)
  • uv for deps; Docker + docker-compose (app + Postgres)
  • Config via pydantic-settings — reads env vars and .env file

Project layout

fastapi_skeleton/ ├── app/ │ ├── main.py # app factory, router + middleware wiring, lifespan │ ├── core/ │ │ ├── config.py # pydantic-settings (env + .env) │ │ ├── security.py # password hashing, JWT encode/decode │ │ └── database.py # async engine, session, Base │ ├── models/ # user, role, permission, refresh_token (revocation) │ ├── schemas/ # pydantic request/response models │ ├── crud/ # data-access helpers │ ├── deps.py # get_db, get_current_user, require_permission(...) │ ├── api/ # JSON API │ │ ├── auth.py # register, login, refresh, logout │ │ ├── users.py # me, user management │ │ └── router.py │ ├── web/ # HTML/HTMX routes │ │ ├── pages.py # login page, dashboard home │ │ └── admin.py # admin: users, roles, permissions CRUD (HTMX) │ ├── templates/ # Jinja2: base, partials, admin/* │ └── static/ # htmx.min.js, minimal css ├── alembic/ + alembic.ini # migrations ├── scripts/seed.py # bootstrap admin user + default roles/perms ├── tests/ # pytest: auth, rbac, smoke ├── pyproject.toml + uv.lock ├── Dockerfile # multi-stage, uv-based ├── docker-compose.yml # app + postgres ├── .env.example ├── .dockerignore / .gitignore └── README.md

Key design points

  • RBAC model: require_permission("users:write") dependency factory; roles aggregate permissions; a superuser flag bypasses checks. Seed script creates admin/user roles + an initial admin.
  • Auth flow: /api/auth/login → access (short) + refresh (long) tokens; /api/auth/refresh rotates and revokes the old refresh token; /api/auth/logout revokes. Refresh tokens stored hashed in DB with a revoked flag.
  • Web vs API auth: API uses Authorization: Bearer; the HTMX UI uses an httpOnly cookie holding the access token so the dashboard works without JS token handling.
  • DB portability: single DATABASE_URL; defaults to SQLite file so it runs with zero setup, Postgres via compose.
  • Config: all settings (secret key, token TTLs, DB URL, CORS, admin bootstrap creds) overridable by env var or .env.
  • Tests: pytest + httpx async client against an in-memory SQLite, covering register/login/refresh/logout and an RBAC permission check.

Build order

  1. Tooling + config + Docker scaffolding
  2. Database, models, Alembic
  3. Security + auth API + revocation
  4. RBAC deps + user/role API
  5. HTMX templates + admin dashboard
  6. Seed script, tests, README

This is a sizable scaffold (~35–40 files). Want me to build it all now, or would you prefer I build it phase by phase so you can review as I go? I'll start with phase 1 either way unless you'd like to adjust anything above.

● Everything works. Let me verify the hand-written Alembic migration applies cleanly against a fresh database:

● Bash(cd /home/iroddis/dev/fastapi_skeleton export DATABASE_URL="sqlite+aiosqlite:///./alembic_test.db" SECRET_KEY=test…) ⎿  == upgrade head == INFO [alembic.runtime.migration] Context impl SQLiteImpl. INFO [alembic.runtime.migration] Will assume non-transactional DDL. … +7 lines (ctrl+o to expand)

● Migrations work both directions. Let me stop the server and clean up:

● Bash(rm -f app.db alembic_test.db /tmp/skel_cookies.txt /tmp/skeleton_server.log; echo "cleaned") ⎿  cleaned

● Stop Task ⎿  SECRET_KEY=test-secret AUTO_CREATE_TABLES=true uv run uvicorn app.main:app --port 8099 · stopped

● Verified and working. Here's what I confirmed:

Results

Static checks

  • ruff check . → All checks passed (B008 ignored — that rule fires on FastAPI's Depends()-in-defaults idiom, which is intentional)
  • uv sync --extra dev resolved and installed cleanly on Python 3.14

Test suite — uv run pytest → 7 passed

  • Auth lifecycle: register → login → /me → refresh (with rotation) → confirms old refresh token is revoked → logout invalidates the rotated token
  • Wrong password rejected; unauthenticated /me returns 401
  • RBAC: reader role can GET but not POST users (403); superuser bypasses checks; user with no roles is forbidden; registration grants the default user role

Live smoke test (seeded SQLite, real uvicorn server)

  • Login page renders; unauthenticated / → 302 redirect to /login
  • API bearer-token login works; /api/users/me returns the admin with all permissions; /api/roles → 200
  • Web cookie flow: form login → 303 + httpOnly access_token cookie → dashboard and /admin/users render → HTMX create-user returns the table partial with the new row
  • RBAC denial confirmed live: a normal registered user gets 403 on POST /api/users but 200 on GET /api/users

Alembic — the hand-written 0001_initial migration applies (upgrade head creates all 7 tables) and reverses cleanly (downgrade base).

One fix made during verification

app/services/auth.py was passing datetime.fromtimestamp(payload["exp"]), but PyJWT leaves exp as a timezone-aware datetime in the payload dict (it only converts to epoch inside the encoded token). Changed it to use payload["exp"] directly — this was the cause of the initial 5 failures, now all green.

The skeleton is complete and functional. To run it yourself:

uv sync --extra dev uv run python -m scripts.seed # admin@example.com / changethis uv run uvicorn app.main:app --reload # http://localhost:8000

Test artifacts (app.db, cookie jar, logs) were cleaned up, and the background server was stopped.