Replace Next.js/Supabase/Vercel stack with a self-hostable setup: Vite+React frontend, Fastify+Drizzle+PostgreSQL backend with own JWT auth, deploy via git pull + rsync + systemd (checkout/service separated, updates pulled from Gitea). Adds a reference auth route (register/login/me) as the canonical pattern for new routes. Updates all skills/rules/agents/docs to match. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BMdBJfD9TT6XUU2NqGG1PN
2.4 KiB
2.4 KiB
paths
| paths | ||
|---|---|---|
|
Backend Development Rules
Database (PostgreSQL + Drizzle)
- Every table gets explicit foreign keys with
onDeletebehavior defined - Add indexes on columns used in WHERE, ORDER BY, and JOIN clauses
- Auth/ownership checks happen in route handlers, not in the database — there is no RLS equivalent
- Migrations generated via
npm run db:generate, applied vianpm run db:migrate— never hand-edit generated SQL after it has been applied
API Routes (Fastify)
- Validate all inputs using Zod schemas before processing
- Always check authentication: verify JWT via
request.jwtVerify()before touching protected data - Return meaningful error messages with appropriate HTTP status codes
- Paginate all list queries (
limit/offset, sane default limit)
Query Patterns
- Use Drizzle relational queries / joins instead of N+1 query loops
- Wrap multi-statement writes in a transaction (
db.transaction(...)) - Always handle and log errors from
pg/ Drizzle calls
Security
- Never hardcode secrets in source code
- Use environment variables for all credentials (see
.env.example) - Validate and sanitize all user input
- Drizzle uses parameterized queries by default — never build SQL via string concatenation
Reference Pattern: Auth Routes
backend/src/routes/auth.ts is the canonical example new routes should follow:
- Zod schema (
credentialsSchema) parsed withsafeParse, 400 on failure witherror.issues[0].message - Passwords hashed with
bcryptjs(bcrypt.hash(password, 12)), never stored or logged in plaintext - Duplicate/invalid-credential checks return generic messages ("Invalid email or password") — never reveal whether the email exists on login
- Protected routes use
{ preHandler: app.authenticate }(decorator defined inserver.ts, verifies the JWT viarequest.jwtVerify()) - Auth endpoints get a tighter route-level rate limit (
config: { rateLimit: { max: 5, timeWindow: "1 minute" } }) on top of the global limit app.jwt.sign({ sub: user.id, email: user.email })— the JWT payload shape is declared once inbackend/src/types.d.ts(FastifyJWTmodule augmentation), sorequest.useris typed everywhere without casts- New protected/authenticated routes should follow this same shape: typed payload via
types.d.ts,app.authenticateaspreHandler, neverrequest.user as any