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
41 lines
2.4 KiB
Markdown
41 lines
2.4 KiB
Markdown
---
|
|
paths:
|
|
- "backend/src/routes/**"
|
|
- "backend/src/db/**"
|
|
---
|
|
|
|
# Backend Development Rules
|
|
|
|
## Database (PostgreSQL + Drizzle)
|
|
- Every table gets explicit foreign keys with `onDelete` behavior 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 via `npm 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 with `safeParse`, 400 on failure with `error.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 in `server.ts`, verifies the JWT via `request.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 in `backend/src/types.d.ts` (`FastifyJWT` module augmentation), so `request.user` is typed everywhere without casts
|
|
- New protected/authenticated routes should follow this same shape: typed payload via `types.d.ts`, `app.authenticate` as `preHandler`, never `request.user as any`
|