Too important for spreadsheets. Too specific for SaaS.
A small, production-shaped full-stack foundation designed to be safely extended by AI coding agents.
Most starter templates are designed for humans to read once and forget. This one is designed to be re-read by an LLM every session. The codebase is deliberately small enough to fit in a coding agent's context window, with conventions, gotchas, and extension recipes documented in CLAUDE.md so the next change lands correctly the first time.
Live demo: https://frontend-production-7642.up.railway.app — public homepage + items list. Admin login behind a credential gate. See DEPLOYMENT.md for what it took to deploy it.
Coding agents are very good at extending clear patterns and very bad at inventing safe foundations. Baseplate gives the agent the boring decisions already made — cookie auth + CSRF, service-layer data access, migrations, typed API boundaries, tests, Docker, documented extension recipes — so the next change is "extend this," not "invent this."
The goal isn't to include every feature. It's to keep the base small enough that both humans and agents can hold it in their heads.
AI coding agents have made a category of software economically viable that wasn't before: small, specific apps that solve one workflow for one team, client, or community. Not SaaS. Not enterprise platforms. Just situated software — useful for a particular context, not a generic market. The line at the top of this README is the lane: things too important to leave in a spreadsheet, too specific to justify a SaaS subscription, too narrow for off-the-shelf tools.
Vibe-coded apps work brilliantly until the foundation matters: when something has to handle a password, persist a session, run a scheduled job, or survive a redeploy without losing data. That's where agents typically improvise badly. Baseplate is the foundation underneath, so the agent's free-form work happens on top of safe rails instead of on a blank canvas.
The recipes are where the growth happens — each one a documented transformation an agent can apply cleanly to extend the base for a specific use case.
Baseplate is shaped for single-tenant, admin-driven apps: a logged-in admin manages data; the public reads pages backed by that data; scheduled jobs do work in the background. Concrete app shapes that map cleanly onto what Baseplate ships:
- Directory or data product — public listings (venues, grants, tools, companies, charities, datasets), admin CRUD, scheduled freshness checks. The canonical use case.
- Internal operations dashboard — lightweight CRM, supplier tracker, lead review board, compliance task tracker, contractor pipeline. Single organisation, several internal users.
- Intake + review queue — public submits a form (case, application, complaint, candidate), admin reviews internally, status changes through a workflow.
- AI workflow with human-in-the-loop — upload documents, LLM extracts/summarises, admin reviews and approves. Scheduler triggers extraction batches; the review queue is the admin-side product.
- Niche structured CMS — content database with admin screens and a custom public frontend. Use when WordPress or Sanity is too generic and you want code ownership.
- Scheduled monitor — scrape sources daily, store results, surface a digest for admin review. APScheduler handles the cron side; the admin UI is the review layer.
- Internal tool with company data — admin app sitting near where work already happens: SSO login via Google Workspace or Microsoft Entra, inbox-as-queue via IMAP, optional read-only links to existing operational data. The base ships with simple local auth; the SSO, user-management, and email-intake recipes layer on when you're ready.
If your app looks like one of these shapes, Baseplate gets you to "production-shaped foundation" in under an hour.
Baseplate is the production-shaped foundation that a Flatpack graduates into when "my tool" becomes "our tool" — when a personal single-file utility crosses a real promotion trigger (a second user, shared state, audit history, server-side secrets).
You don't convert a Flatpack into a Baseplate project — a Flatpack is one HTML file, this is a full stack. What promotes is the understanding embedded in the Flatpack's inline manifest: entities, validations, exports, sample data, edge cases, test cases. The Flatpack stays alongside the Baseplate version as a reference artifact for parity checks.
If you have a promotion plan in hand:
- Read
docs/promoting-a-flatpack.md— the receiving flow. - Map the archetype to a recipe set via
docs/flatpack-archetype-to-recipe-map.md. - Drop the Flatpack artifacts into
reference/. - Apply the recipes, walk the confidence tiers in the plan.
- Verify with
make verify-promotion.
If you don't have a Flatpack yet but your idea is small enough that you're not sure Baseplate is the right starting point, try Flatpack first. That repo's prompts/generate-flatpack.md is ~100 lines, fits in any agent's context, and produces a working single-file tool you can use locally. Promote later if and when it stops being personal.
- Solo founders prototyping a real product with LLM assistance — who don't want to trust the agent to invent auth, CSRF, and deployment from scratch.
- Consultants building bespoke internal tools — start every client engagement from the same production-shaped foundation. Faster delivery, fewer auth/deploy mistakes, easier handover, and the client owns the code outright.
- Domain experts with technical help — a lawyer, researcher, or operator working with a technical collaborator (human or LLM) on a custom workflow tool.
- Internal tools engineers at small companies — enough structure to be maintainable without becoming enterprise architecture.
- Founders validating non-SaaS products — data products, directories, review workflows, AI-assisted services that aren't billable SaaS yet.
- People building consumer social apps or anything needing public user accounts on day one.
- Multi-tenant B2B SaaS with organisations, billing, plan limits, invitations (see What if my app grows into a SaaS? below — it's possible, just not the default).
- People who want Stripe + RBAC + team onboarding on day one — that's a SaaS boilerplate, this is its smaller cousin.
- Enterprise teams needing Terraform, IAM, K8S manifests, observability stacks, policy-as-code.
- Beginners who don't know what an HTTP cookie is — Baseplate assumes some web-app fluency.
- Clone the repo. The example
Itemmodel is a full vertical slice (model → migration → service → routes → frontend page). - Point your coding agent at
CLAUDE.md. It reads conventions, dev commands, gotchas, and anti-patterns up front. - Follow the 10-step recipe for adding your own domain models. The agent has every pattern it needs without inventing one from scratch.
- Tests + lint + CI catch the mistakes coding agents typically make.
| Layer | Technology |
|---|---|
| Backend | FastAPI, SQLAlchemy 2 (async), Pydantic v2, Alembic, APScheduler |
| Frontend | Next.js 16, React 19, Tailwind CSS 4, TypeScript |
| Database | PostgreSQL 16 (asyncpg driver) |
| Auth | JWT in httpOnly cookies, bcrypt password hashing |
| Testing | pytest + pytest-asyncio (backend), Vitest + Testing Library (frontend) |
| Deploy | Docker multi-stage builds, Railway via GitHub Actions |
| Logging | structlog (structured JSON in prod, colored console in dev) |
This is a starter, not a finished product. Be aware of these intentional limits before building on top:
- No user-management UI yet — the
userstable exists and supports multiple admins, but there's no admin page to create/list/delete them. The bootstrap admin is created fromADMIN_EMAIL+ADMIN_PASSWORD_HASHon first startup. Add a/admin/userspage when you need more. - No password reset / email verification — no email infra is wired up. Passwords are managed by re-running
make hash-passwordand updating the DB by hand, or via a future user-management UI. - No background queue —
APSchedulerruns in-process for periodic jobs. Fine for cron-style work; not a substitute for Celery/Redis if you need durable retries or a separate worker pool.
Baseplate starts single-tenant on purpose. Most small apps don't need organisations, billing, invitations, or tenant-scoped roles on day one — and adding those abstractions too early makes the code harder for humans and coding agents to understand.
But Baseplate isn't a dead end. The architectural seams that make multi-tenancy feasible later are already in place:
- All data access goes through a service layer — adding
organization_idlater means changing service method signatures, not spraying queries across routes. - Auth context is centralised —
deps.get_current_adminreturns aUser; threadingcurrent_orgthrough it is mechanical. - The users table already supports multiple admins — adding a
/admin/userspage is the next obvious step before introducing tenancy at all. - Alembic-managed migrations — adding tenancy columns and backfilling existing rows is a normal alembic flow.
When (if) you need it, see docs/growth-paths/multi-tenant.md for the step-by-step migration guide. Baseplate deliberately does not ship unused organization_id columns or tenancy machinery you're not using yet — unsafe or unused multi-tenancy is worse than no multi-tenancy.
cp .env.example backend/.env
cp .env.example frontend/.env.local # only the API_URL line
make install # pip install backend deps + npm install frontend deps
make db # start Postgres 16 via Docker on port 5433
make migrate # run Alembic migrations
make hash-password # generate a bcrypt hash, paste into backend/.env as ADMIN_PASSWORD_HASH
make dev # backend on :8001, frontend on :3001Open http://localhost:3001/admin/login and log in with the username/password you configured.
├── backend/
│ ├── app/
│ │ ├── config.py # Pydantic Settings (env vars)
│ │ ├── database.py # async engine + session factory
│ │ ├── deps.py # FastAPI dependencies (auth)
│ │ ├── bootstrap.py # Idempotent admin-user seed on startup
│ │ ├── rate_limit.py # SlowAPI Limiter instance
│ │ ├── main.py # App factory, middleware, routers
│ │ ├── models/
│ │ │ ├── base.py # DeclarativeBase, TimestampMixin, uuid_pk()
│ │ │ ├── item.py # Example model
│ │ │ └── user.py # Users (email, password_hash, is_admin)
│ │ ├── schemas/
│ │ │ ├── auth.py # LoginRequest / LoginResponse
│ │ │ ├── item.py # ItemCreate / ItemUpdate / ItemResponse
│ │ │ └── user.py # UserResponse (excludes password_hash)
│ │ ├── api/
│ │ │ ├── auth.py # POST /login, /logout, GET /me
│ │ │ ├── items.py # Admin CRUD (GET/POST/PATCH/DELETE)
│ │ │ └── public.py # Public read endpoints
│ │ ├── services/
│ │ │ ├── items.py # DB query logic, separate from routes
│ │ │ └── users.py # get_by_email, create, authenticate
│ │ └── tasks/
│ │ └── scheduler.py # APScheduler with placeholder job
│ ├── alembic/ # Migration config + versions
│ ├── tests/
│ └── pyproject.toml
├── frontend/
│ ├── src/
│ │ ├── app/ # Next.js App Router pages
│ │ │ ├── (public)/ # Public route group (Header + Footer)
│ │ │ └── admin/ # Admin route group (Sidebar)
│ │ ├── components/
│ │ │ ├── ui/ # Button, Card, Input, Modal, StatusPill
│ │ │ └── layout/ # Header, Footer
│ │ ├── lib/
│ │ │ ├── api.ts # fetchAPI wrapper + typed endpoint functions
│ │ │ ├── auth.ts # useRequireAuth() hook
│ │ │ ├── types.ts # TypeScript interfaces
│ │ │ ├── constants.ts # Site name, description
│ │ │ └── server-config.ts # Server-side API_BASE from env
│ │ └── middleware.ts # Redirects unauthenticated /admin/* to /login
│ └── package.json
├── docker-compose.yml # Local Postgres
├── Makefile # All dev/test/deploy commands
└── .github/workflows/
├── ci.yml # Tests + lint (always runs)
└── deploy-railway.yml # Railway deploy (opt-in, see Deployment)
| Variable | Required | Default | Description |
|---|---|---|---|
DATABASE_URL |
Yes | postgresql+asyncpg://myapp:myapp@localhost:5433/myapp |
Async PostgreSQL connection string |
ADMIN_EMAIL |
Yes | — | Bootstrap admin's email (creates the first user on startup if none exists) |
ADMIN_PASSWORD_HASH |
Yes | — | bcrypt hash (generate with make hash-password) |
JWT_SECRET |
Yes | — | Random string for signing tokens |
JWT_ALGORITHM |
No | HS256 |
JWT signing algorithm |
JWT_EXPIRE_MINUTES |
No | 1440 |
Token lifetime (default 24h) |
COOKIE_SECURE |
No | true |
Set false for local HTTP dev |
CORS_ORIGINS |
No | ["http://localhost:3001"] |
Allowed CORS origins (JSON list) |
DEBUG |
No | false |
Enables /docs and /redoc, disables startup validation |
| Variable | Required | Default | Description |
|---|---|---|---|
API_URL |
Yes | http://localhost:8001 |
Backend URL for API proxying |
When DEBUG=false (the default), the backend will refuse to start if:
JWT_SECRETis the default placeholder or emptyADMIN_PASSWORD_HASHis emptyDATABASE_URLuses default local credentials
This prevents deploying with insecure defaults. Set DEBUG=true locally to skip these checks, or set real values.
Browser → Next.js (:3001) → rewrites /api/* → FastAPI (:8001) → PostgreSQL
The Next.js next.config.ts proxies all /api/* requests to the backend. The browser only talks to the frontend server. In production on Railway, each service gets its own URL and the same rewrite proxy applies — the frontend's API_URL env var points to the backend's internal Railway URL.
- Bootstrap: on first startup with no users in the DB,
app/bootstrap.py:ensure_admin_usercreates an admin fromADMIN_EMAIL+ADMIN_PASSWORD_HASH. Idempotent — subsequent starts skip if any admin exists. - Login: user submits
{ email, password }toPOST /api/auth/login. Backend looks up the user viaUserService.authenticate, bcrypt-verifies the password, and issues a JWT withsub = str(user.id)set as an httpOnly cookie. - Subsequent requests include the cookie automatically.
middleware.tson the frontend checks for the cookie and redirects to/admin/loginif missing.useRequireAuth()hook validates the token server-side viaGET /api/auth/me.- Admin API routes either gate via
dependencies=[Depends(get_current_admin)]on the router (cleanest when the route doesn't need the User) or acceptuser: User = Depends(get_current_admin)in the signature (when the route does).
Cookie auth with SameSite=lax blocks cross-origin fetch() calls but not top-level form-POST navigation. To close that gap, every write (POST/PUT/PATCH/DELETE) requires a CSRF token:
- Backend (
app/middleware/csrf.py): a global middleware checks every non-safe, non-exempt request. TheX-CSRF-Tokenheader must equal thecsrf_tokencookie (constant-time comparison viasecrets.compare_digest). - Token issuance: login sets the
csrf_tokencookie alongsideaccess_token.GET /api/auth/csrfrefreshes it (sets the cookie and returns the token in the body for non-cookie consumers). - Cookie attributes:
Secure=COOKIE_SECURE,SameSite=lax,HttpOnly=false— JS must read it. - Frontend (
lib/api.ts):fetchAPIauto-attachesX-CSRF-Tokenon writes by reading the cookie vialib/csrf.ts:getCSRFToken(). Don't callfetch()directly for writes — you'll get 403'd. - Exempt paths:
/api/auth/login(no prior token possible) and/api/auth/csrf(issues the token). Safe methods (GET/HEAD/OPTIONS) bypass the check entirely.
Models inherit from Base and TimestampMixin. Use uuid_pk() for UUID primary keys with auto-generation:
class Item(Base, TimestampMixin):
__tablename__ = "items"
id = uuid_pk()
name: Mapped[str] = mapped_column(String(255))Services contain all database query logic. Routes stay thin — they validate input, call a service method, and return the result:
@router.post("", response_model=ItemResponse, status_code=201)
async def create_item(data: ItemCreate, admin=Depends(get_current_admin), db=Depends(get_db)):
return await ItemService.create(db, data)Schemas use Pydantic v2 with model_config = {"from_attributes": True} so SQLAlchemy models serialize directly.
API client (lib/api.ts): A single fetchAPI<T>() wrapper handles JSON headers, error extraction, and 204 responses. All endpoint functions are typed and use credentials: "include" for cookie auth.
Route groups: (public)/ has the Header + Footer layout. admin/ has the sidebar layout. This separation means public pages and admin pages can have completely different chrome.
Server components fetch data at the edge. Client components ("use client") handle interactivity. The admin pages are client components because they need auth state and user interaction.
| Command | What it does |
|---|---|
make dev |
Starts Postgres + backend + frontend in parallel |
make dev-backend |
Backend only on :8001 |
make dev-frontend |
Frontend only on :3001 |
make db |
Start Postgres container on port 5433 |
make install |
pip install -e ".[dev]" + npm install |
make install-hooks |
Register pre-commit hooks (ruff, tsc, eslint) |
make generate-client |
Regenerate frontend/src/lib/api-types.ts from backend OpenAPI |
make migrate |
alembic upgrade head |
make migrate-new msg="..." |
Generate a new auto-detected migration |
make test-backend |
pytest -v |
make test-frontend |
vitest run |
make lint |
ruff (backend) + tsc + ESLint (frontend) |
make hash-password |
Interactive bcrypt hash generator |
make stop |
Kill dev servers + stop Docker |
make restart |
Stop then start everything |
The basic transformation is "add a new domain model" — covered below.
For larger guided transformations, see docs/recipes/:
- Audit log — record who did what when. For compliance, case management, internal review queues.
- Public submission + admin queue — the intake-and-review pattern: unauthenticated public form → admin review with status workflow.
- SSO via OpenID Connect — log in via Google Workspace (canonical) or Microsoft Entra / generic OIDC. Domain-allowlisted, additive to local password auth. The first Internal Tools recipe.
- User-management admin page — admin UI for inviting + deactivating other admins. Refuses to demote or deactivate the last active admin. Pairs with the SSO recipe.
- Email intake → admin review queue — scheduled IMAP poll turns an inbox into a submission queue. Composes with the public-submission recipe.
More recipes welcome — see docs/recipes/README.md for the format and what's planned.
See a recipe applied: baseplate-example-feedback is a working app — public feedback form + admin review queue — built by taking Baseplate v0.1.0, removing the Item example, and applying the public-submission recipe. Roughly 12 file changes, the same kind of work an LLM following the recipe would produce.
This is the most common operation. Replace "Item" or add alongside it.
-
Create the model in
backend/app/models/:# backend/app/models/widget.py from sqlalchemy import String, Integer from sqlalchemy.orm import Mapped, mapped_column from app.models.base import Base, TimestampMixin, uuid_pk class Widget(Base, TimestampMixin): __tablename__ = "widgets" id = uuid_pk() title: Mapped[str] = mapped_column(String(255)) count: Mapped[int] = mapped_column(Integer, default=0)
-
Register the model in
backend/app/models/__init__.py:from app.models.widget import Widget
Alembic's
env.pyimportsBasefrom here, so any model that inheritsBaseis auto-detected. -
Generate the migration:
make migrate-new msg="add widgets table" make migrate -
Add schemas in
backend/app/schemas/widget.py:from pydantic import BaseModel from uuid import UUID from datetime import datetime class WidgetCreate(BaseModel): title: str count: int = 0 class WidgetResponse(BaseModel): id: UUID title: str count: int created_at: datetime updated_at: datetime model_config = {"from_attributes": True}
-
Add a service in
backend/app/services/widget.py— follow theItemServicepattern. -
Add routes in
backend/app/api/widget.py— follow theitems.pypattern for admin routes,public.pyfor public routes. -
Register the router in
backend/app/main.py:from app.api import widget app.include_router(widget.router)
-
Add the TypeScript type in
frontend/src/lib/types.ts:export interface Widget { id: string; title: string; count: number; created_at: string; updated_at: string; }
-
Add API functions in
frontend/src/lib/api.ts— follow the Item functions pattern. -
Add pages — copy
admin/items/page.tsxas a starting point for the admin UI, and(public)/items/page.tsxfor the public view.
Edit backend/app/tasks/scheduler.py:
async def my_job():
async with async_session() as db:
# your logic here
pass
def start_scheduler():
scheduler.add_job(my_job, IntervalTrigger(minutes=15), id="my_job", replace_existing=True)
scheduler.start()Import async_session from app.database to get a database session inside jobs — don't use FastAPI's Depends outside of request handlers.
Components live in frontend/src/components/ui/. The design system uses CSS custom properties defined in globals.css:
bg-background/text-foreground— page-level colorsbg-surface/bg-surface-elevated— card and panel backgroundstext-muted— secondary textborder-border— bordersbg-accent/bg-accent-bright— primary action color (indigo by default)
Dark mode is available by adding the dark class to <html>. The CSS variables swap automatically.
- Create
frontend/src/app/(public)/your-page/page.tsx - It automatically inherits the Header + Footer layout from
(public)/layout.tsx - For server-rendered data, fetch from the backend using
API_BASEfromlib/server-config.ts:const res = await fetch(`${API_BASE}/api/public/your-endpoint`, { next: { revalidate: 60 } });
- Create
frontend/src/app/admin/your-page/page.tsx - It automatically gets the sidebar layout and is protected by
middleware.ts - Add the nav link in
frontend/src/app/admin/layout.tsx
CI (.github/workflows/ci.yml) runs tests + lint on every push and PR — no
platform secrets required. Deploy is a separate, opt-in workflow.
See DEPLOYMENT.md for end-to-end notes from the actual first deployment, including the three non-obvious issues that came up (Railway's
$PORTinjection, missing--environmentin CI,railway uppicking the wrong Dockerfile when run from the wrong directory). The quick path below assumes you've read those caveats.
Baseplate ships with Railway as the default deployment path because it's the fastest way to get a small full-stack app online. It is not Railway-coupled. The portability contract — what every supported platform must provide — is:
| Requirement | How Baseplate uses it |
|---|---|
| Standard Docker containers | Both services build as multi-stage images |
$PORT env var at runtime |
Both services bind to $PORT (or their dev default) |
| Non-root container runtime | Both run as uid 1000 by default |
| HTTP healthchecks | /api/health (backend), /healthz (frontend) |
| Environment-variable config | All secrets and tunables are env-driven |
| Managed Postgres | The only required external service |
| One-off command on deploy | Backend's CMD runs alembic upgrade head before uvicorn |
That means Baseplate runs unchanged on Render, Fly.io, Google Cloud Run, AWS App Runner / ECS Fargate, and Kubernetes — though only Railway is first-class-verified today. Treat other platforms as reachable but unverified until smoke-tested examples land.
To swap platforms: delete .github/workflows/deploy-railway.yml, add a
deploy-<platform>.yml alongside it using the same workflow_run trigger
pattern. Both Dockerfiles are already platform-agnostic; no app-layer change
needed.
The repo ships with .github/workflows/deploy-railway.yml. It's dormant until
you flip a switch, so a freshly cloned starter doesn't fail CI before a
project exists.
First-time setup:
- Create a Railway project with three services:
backend,frontend, and a PostgreSQL plugin. - Set environment variables on each Railway service:
- backend:
DATABASE_URL(from Postgres plugin),JWT_SECRET,ADMIN_EMAIL,ADMIN_PASSWORD_HASH,COOKIE_SECURE=true,CORS_ORIGINS=["https://your-frontend.up.railway.app"] - frontend:
API_URL(internal Railway URL of the backend, e.g.http://backend.railway.internal:8001)
- backend:
- Add two GitHub secrets to your repo:
RAILWAY_TOKEN— a workspace/account token (Railway → Account Settings → Tokens). Not a per-project token. The workflow feeds it to the CLI asRAILWAY_API_TOKEN; a project token in this slot fails withInvalid RAILWAY_TOKEN.RAILWAY_PROJECT_ID— the project's ID (Railway project → Settings).
- Add a GitHub variable (not secret):
RAILWAY_DEPLOY_ENABLED=trueunder Settings → Secrets and variables → Actions → Variables. This is the gate that turns deploys on. - Push to
main. CI runs first; once it succeeds, the deploy workflow fires.
ci.ymlruns on every push and PR — backend ruff + pytest, frontend typecheck + ESLint + vitest + build.deploy-railway.ymltriggers onworkflow_run: CI completed, only when the CI run was successful, only onmain, and only whenvars.RAILWAY_DEPLOY_ENABLED == 'true'. Without the variable, the deploy workflow's jobs are skipped (no failure).- Deploy uses
railway up --service <name>, which builds each service's Dockerfile on Railway's infrastructure.
Delete deploy-railway.yml and add a deploy-<platform>.yml alongside it. Use the same workflow_run trigger pattern so deploys still gate on CI success. Both Dockerfiles are already platform-agnostic (read $PORT at runtime).
- Railway provides
DATABASE_URLin the standardpostgresql://format. The backend config expectspostgresql+asyncpg://— you may need to adjust the variable or add a prefix in Railway's variable references. - Both Dockerfiles expose their dev ports (backend
8001, frontend3001) and read$PORTat runtime. Railway injects$PORTautomatically. - Both images run as non-root users (uid 1000 in each —
appfor backend,nodefor frontend) and ship a DockerHEALTHCHECKdirective. The backend image is multi-stage sobuild-essentialdoesn't ship to production. - The backend Dockerfile's CMD runs
alembic upgrade headbefore launching uvicorn, so migrations apply on every deploy. No manualmake migrateneeded in production.
Tests use SQLite (via aiosqlite) instead of PostgreSQL so they run without Docker. The conftest.py sets up an in-memory test database, overrides FastAPI's get_db dependency, and generates a real bcrypt hash for auth tests.
make test-backend # runs pytest -vTo add tests, create files in backend/tests/test_*.py. The client fixture gives you an authenticated-capable httpx.AsyncClient:
@pytest.mark.asyncio
async def test_my_endpoint(client):
await _login(client) # sets auth cookie
response = await client.get("/api/admin/widgets")
assert response.status_code == 200Tests use Vitest with jsdom and Testing Library. The setup file is at src/__tests__/setup.ts.
make test-frontend # runs vitest run/docsis disabled in production. SetDEBUG=trueto enable the Swagger UI at/docsand ReDoc at/redoc. This is controlled inmain.py.- Rate limiting is set to 60 requests/minute globally via SlowAPI. Adjust in
main.py. Add per-endpoint limits with@limiter.limit("10/minute")on individual route handlers. - The middleware.ts deprecation warning — Next.js 16 is renaming the
middleware.tsconvention toproxy.ts. The current file still works but you'll see a build warning. Rename when ready. - UUID primary keys — all models use UUID v4 via PostgreSQL's native UUID type. The
uuid_pk()helper inmodels/base.pyhandles this. from_attributes = Trueon response schemas means you return SQLAlchemy model instances directly from routes — Pydantic serializes them automatically.- The frontend proxies
/api/*to the backend via Next.js rewrites innext.config.ts. The browser never talks to the backend directly. This avoids CORS issues and keeps the backend URL private. - Cookies require HTTPS in production.
COOKIE_SECURE=true(the default) means auth cookies won't be sent over plain HTTP. This is correct for production. Setfalsefor local dev without HTTPS.