Skip to content

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.)

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.

backend/app/api/v1/projects.py
from typing import Annotated
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.deps import get_or_create_db_user, get_session
from 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.

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.

backend/app/models/project.py
from sqlalchemy.orm import Mapped, mapped_column
from 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):

Terminal window
cd backend && alembic revision --autogenerate -m "add projects" # generate
make migrate # = alembic upgrade head

Add 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.)

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.