Customization
The most common fear with a boilerplate is that you will spend more time fighting someone else’s abstractions than you saved. The Unsexy Stack is built to be extended along the same conventions it ships with. Here is the shape of the two things you will do most. (The snippets below are illustrative excerpts — imports and surrounding wiring elided for readability — not copy-paste-complete files.)
Add an endpoint
Section titled “Add an endpoint”A new route is a thin handler that declares what it needs as FastAPI dependencies and returns a typed Pydantic response. Authentication and the database session are injected — you do not wire them by hand.
from typing import Annotatedfrom fastapi import APIRouter, Dependsfrom sqlalchemy.ext.asyncio import AsyncSessionfrom app.core.deps import get_or_create_db_user, get_sessionfrom app.models.user import User
router = APIRouter(prefix="/projects", tags=["projects"])
@router.get("/mine", response_model=list[ProjectResponse])async def list_my_projects( user: Annotated[User, Depends(get_or_create_db_user)], session: Annotated[AsyncSession, Depends(get_session)],) -> list[Project]: result = await session.execute( select(Project).where(Project.owner_id == user.id) ) return list(result.scalars())Then register it in app/api/v1/router.py. The authenticated User and the AsyncSession arrive
already resolved, so your handler stays about the business logic — nothing else.
Add a model
Section titled “Add a model”Models are SQLAlchemy 2.0 with typed Mapped[...] columns and the shared UUID + timestamp mixins,
so every table gets consistent primary keys and audit fields for free.
from sqlalchemy.orm import Mapped, mapped_columnfrom app.models.base import Base, UUIDMixin, TimestampMixin
class Project(Base, UUIDMixin, TimestampMixin): __tablename__ = "projects" name: Mapped[str] = mapped_column(String(255), nullable=False) owner_id: Mapped[UUID] = mapped_column(ForeignKey("users.id"), index=True)Generate the migration from your model change, then apply it with Alembic (async):
cd backend && alembic revision --autogenerate -m "add projects" # generatemake migrate # = alembic upgrade headAdd the matching Pydantic schema in schemas/, and FastAPI derives the live API contract from
your type hints automatically. (The static openapi.json that backs this public
API Reference is regenerated from the app when the docs are built.)
Swapping a provider
Section titled “Swapping a provider”Auth verification sits behind a FastAPI dependency, so on the backend, swapping Clerk for another
JWT provider is a change behind one boundary (core/auth.py + core/deps.py) rather than a
rewrite of every route. (You would also swap the frontend’s Clerk integration — its sign-in
routes, middleware, and keys.) The data layer is plain SQLAlchemy and the billing layer is a thin
Stripe service — each is mainstream and replaceable. You own a clean core, not a framework that
owns you.
The download includes full step-by-step guides — docs/guides/add-endpoint.md and
add-model.md — plus a customization PDF that walks a complete feature end to end.