Keeps repo root lean: CLAUDE.md is the only doc at root. All reference/architecture material lives under docs/. Also updates all cross-references in CLAUDE.md, docs/README.md, and the FastAPI override note in code_guidelines.md so links stay valid after the move. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5.6 KiB
5.6 KiB
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) |
| 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=<caddy-ip>. 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→bleachallowlist. 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 withitsdangerous. - 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_EMAILSenv var may request a magic link. Any other address silently succeeds (no user enumeration) but sends no email. - Secrets: env /
.envonly, never committed..env.exampleis 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)
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):
docker compose up --build
Testing
pytestfor 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-ffmerge intodev, then pushdev. Promotedev→masterwith--no-fffor releases. TagmasterwithvX.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