PRD-driven backend scaffold. Reads AGENTS.md and PRD.md from the repo root and creates a FastAPI app inside server/ with optional Supabase integration and one stub route per entry listed under PRD.md > Backend Routes. Refuses to run if AGENTS.md and PRD.md do not exist, or if PRD.md says Backend Needed? = No. Run AFTER scaffold-frontend.
This skill creates the backend for the user's project inside a server/ directory at the repo root. Like scaffold-frontend, it is driven entirely by PRD.md and AGENTS.md. Every route, every Pydantic model, and the decision to include Supabase are all derived from the PRD.
Before doing anything else, check three conditions:
AGENTS.md exists at the repo root.PRD.md exists at the repo root.PRD.md > Backend Needed? starts with Yes.test -f AGENTS.md && test -f PRD.md || { echo "MISSING_DOCS"; exit 1; }
grep -A1 "^## Backend Needed?" PRD.md | tail -n1
If AGENTS.md or PRD.md is missing, STOP and respond exactly:
This skill cannot run yet.
AGENTS.mdandPRD.mdmust exist at the repo root. Run thedomain-to-specskill first, or run thequickstartskill to chain everything automatically.
If Backend Needed? is No (or empty), STOP and respond exactly:
This project does not need a backend according to
PRD.md > Backend Needed?. If that is wrong, rerun thedomain-to-specskill to update the PRD. Otherwise, run thefeature-builderskill to build out frontend features.
Do not create any files in either fail case.
Extract from PRD.md:
PRD.md > Backend Routes). Each bullet becomes a FastAPI route.PRD.md > Data Model). Each entity becomes a Pydantic model.PRD.md > Domain Constraints). Used to decide what validation rules to add.PRD.md > Core Features (MVP) or PRD.md > User Flow mention login, accounts, or user-specific data. If yes, Supabase Auth is needed.Extract from AGENTS.md:
## Tech Stack > Database.Prompt the user:
"Your PRD implies this app needs a database. I recommend Supabase because it is free, hosted, and integrates auth, Postgres, and storage in one service. Should I wire it up now? (yes / no / later)"
server/Create the following structure at the repo root:
server/
app/
__init__.py
main.py # FastAPI app entry point
config.py # Environment variable loader
db.py # Supabase client or in-memory store
models.py # Pydantic models (from PRD Data Model)
routes/
__init__.py
health.py # GET /health
<entity>.py # One file per entity in the PRD (e.g. submissions.py)
tests/
__init__.py
test_health.py
.env.example
.gitignore
requirements.txt
README.md
requirements.txtAlways include:
fastapi>=0.115
uvicorn[standard]>=0.32
pydantic>=2.9
pydantic-settings>=2.6
python-dotenv>=1.0
pytest>=8.3
httpx>=0.27
If Supabase was chosen, also include:
supabase>=2.9
main.pyfrom fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.routes import health
# Import one route module per entity in PRD > Backend Routes
# Example: from app.routes import submissions
app = FastAPI(title="{project name from PRD}")
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(health.router)
# app.include_router(submissions.router, prefix="/submissions", tags=["submissions"])
Replace {project name from PRD} with the one-sentence summary's subject. Uncomment and add one include_router call per entity found in PRD.md > Backend Routes.
For each entity in PRD.md > Data Model, create a class in app/models.py:
from datetime import datetime
from typing import Literal
from pydantic import BaseModel, Field
class SubmissionBase(BaseModel):
patient_name: str = Field(..., min_length=1)
date: datetime
medications: list[str]
class SubmissionCreate(SubmissionBase):
pass
class Submission(SubmissionBase):
id: str
status: Literal["pending", "approved", "rejected"] = "pending"
Always generate a Base, a Create, and a full model per entity. This pattern gives you clean request/response separation.
PRD.md > Backend RoutesFor each bullet under PRD.md > Backend Routes, generate a FastAPI route. Example input:
- POST /submissions — create a new submission from the form
- GET /submissions — list the current user's submissions
- GET /submissions/{id} — fetch one submission
- POST /submissions/{id}/approve — mark a submission as approved
Generated app/routes/submissions.py:
from fastapi import APIRouter, HTTPException
from uuid import uuid4
from app.models import Submission, SubmissionCreate
from app.db import store
router = APIRouter()
@router.post("", response_model=Submission)
def create_submission(payload: SubmissionCreate) -> Submission:
submission = Submission(id=str(uuid4()), **payload.model_dump())
store.setdefault("submissions", {})[submission.id] = submission
return submission
@router.get("", response_model=list[Submission])
def list_submissions() -> list[Submission]:
return list(store.get("submissions", {}).values())
@router.get("/{submission_id}", response_model=Submission)
def get_submission(submission_id: str) -> Submission:
submission = store.get("submissions", {}).get(submission_id)
if submission is None:
raise HTTPException(status_code=404, detail="Submission not found")
return submission
@router.post("/{submission_id}/approve", response_model=Submission)
def approve_submission(submission_id: str) -> Submission:
submission = store.get("submissions", {}).get(submission_id)
if submission is None:
raise HTTPException(status_code=404, detail="Submission not found")
submission = submission.model_copy(update={"status": "approved"})
store["submissions"][submission_id] = submission
return submission
Every route should return dummy but type-safe data so the frontend has something to consume immediately.
db.pyfrom supabase import create_client, Client
from app.config import settings