# Chicken Babies R Us — Project Instructions Small farm website for **Chicken Babies R Us** (Morrison TN — address is intentionally **not** displayed publicly). Public brochure site with a blog-style home, About, Contact, and a disabled "Shop (coming soon)" placeholder. The owner's wife, **Head Hen**, edits all content through an admin area protected by email magic-link authentication. > This file is authoritative for *this project*. Where it conflicts with `docs/code_guidelines.md`, this file wins. --- ## Stack (authoritative for this project) | Layer | Choice | |---|---| | Language | Python 3.12 | | Web framework | **FastAPI** (overrides the Flask default in `docs/code_guidelines.md`) | | Templates | Jinja2 | | ASGI server | Uvicorn, behind Caddy reverse proxy | | Database | SQLite (WAL mode) — single-writer is fine at this scale | | Cache | In-process TTL cache + row-level rendered-HTML cache (no Redis) | | Email | Resend (contact form + magic-link auth) | | Anti-spam | hCaptcha + honeypot + SlowAPI rate limits | | Logging | `structlog` | | Container | Docker (multi-stage), target = Debian 12 VM on home server | | Repo host | Gitea (Actions for image build/publish wired later) | ## Deployment Topology ``` Internet → Cloudflare (proxied DNS) → OPNsense firewall (inbound 443 allowed only from Cloudflare IP ranges) → Virtual IP → Debian 12 VM → Caddy (TLS termination) → Uvicorn + FastAPI container ``` The app MUST trust `X-Forwarded-For` / `X-Forwarded-Proto` **only from Caddy's IP**. Run Uvicorn with `--proxy-headers --forwarded-allow-ips=`. Never read `X-Forwarded-*` in application code directly — let Starlette's `ProxyHeadersMiddleware` do it. ## Target Repository Layout (after Phase 0) ``` / repo root ├── app/ FastAPI application package │ ├── main.py app factory + startup │ ├── config.py typed config loader (pydantic-settings) │ ├── models/ dataclasses + SQL schema + migrations │ ├── routes/ public_router.py, admin_router.py, auth_router.py │ ├── services/ auth, email, cache, markdown, media, hcaptcha │ ├── templates/ Jinja2 (public/ + admin/ + emails/) │ └── static/ CSS, JS, site images, logo ├── data/ runtime (SQLite DB + uploads) — mounted volume in prod ├── docs/ business + architecture docs (NO application code) │ ├── README.md │ ├── ROADMAP.md │ ├── code_guidelines.md │ ├── security.md │ └── MANUAL_TESTING.md (added in Phase 1) ├── tests/ pytest ├── Logo/ brand assets (source) ├── Dockerfile ├── docker-compose.yml ├── requirements.txt ├── .env.example ├── .gitignore └── CLAUDE.md ``` ## Security Must-Haves (in addition to `docs/security.md`) - **SQL**: parameterized statements only (sqlite3 `?` placeholders or SQLAlchemy Core bind params). Never f-string a query. - **Markdown**: hardened pipeline `markdown-it-py` → `bleach` allowlist. No raw HTML pass-through. - **Image uploads**: validate magic bytes with `python-magic`, cap at 8 MB, re-encode through Pillow, store under a random filename, discard the client-supplied extension. - **CSRF**: double-submit cookie on every admin `POST` / `PUT` / `DELETE`. - **Cookies**: `Secure`, `HttpOnly`, `SameSite=Lax`; session IDs signed with `itsdangerous`. - **Security headers** (middleware): strict nonce-based CSP, HSTS, `X-Content-Type-Options: nosniff`, `Referrer-Policy: strict-origin-when-cross-origin`, `Permissions-Policy`. - **Magic-link tokens**: 256-bit random (`secrets.token_urlsafe(32)`), stored hashed (SHA-256), single-use, 15-minute expiry. IP is logged but not enforced (mobile roaming). - **Rate limits** on auth endpoints: 5 requests / 15 min / IP *and* / email. - **Admin allowlist**: only addresses in `ADMIN_EMAILS` env var may request a magic link. Any other address silently succeeds (no user enumeration) but sends no email. - **Secrets**: env / `.env` only, never committed. `.env.example` is the public contract. - **Audit logging**: every auth event (link requested, link consumed, session created/revoked, rate-limit hit) at INFO. Never log raw tokens or email bodies. ## How to Run (dev) ```bash python3.12 -m venv venv source venv/bin/activate pip install -r requirements.txt cp .env.example .env # fill RESEND_API_KEY, HCAPTCHA_*, ADMIN_EMAILS, SECRET_KEY uvicorn app.main:app --reload ``` Docker (parity with prod): ```bash docker compose up --build ``` ## Testing - `pytest` for auth, magic-link lifecycle, markdown sanitization, rate limits, contact form. - **Never mock the DB** in auth/magic-link tests — use a temp SQLite file so behavior matches prod. - Manual test checklist lives in `docs/MANUAL_TESTING.md` (added in roadmap Phase 1). ## Git Strategy (this project) - Branches: `master` (prod) · `dev` (integration) · `feat/*`, `chore/*`, `docs/*`, `fix/*` (work branches). - **Work branches off `dev`.** Local `--no-ff` merge into `dev`, then push `dev`. Promote `dev` → `master` with `--no-ff` for releases. Tag `master` with `vX.Y.Z`. - Conventional commits: `feat:`, `fix:`, `chore:`, `docs:`, `refactor:`, `test:`. - Do **not** push directly to `master`. Do **not** force-push shared branches. ## Pointers - Generic Python standards: `docs/code_guidelines.md` (FastAPI overrides its Flask default) - Security baseline: `docs/security.md` - Phased roadmap, dataclasses, SQL schema, visual design: `docs/ROADMAP.md` - Docs folder guide: `docs/README.md`