Template
A published database port puts the data on the public internet, and Docker's own iptables rules bypass ufw — the server looks firewalled while the port is open. Previously this was only a comment in compose.yaml, which an agent adding a db service later is free to ignore. Anchored at every point where a service could be added or shipped: - rules/security.md: the rule, the reasoning, and the audit commands - rules/backend.md + architecture SKILL: constraint when picking the data layer - agents/backend-dev.md: non-negotiable for any service it adds - agents/qa-engineer.md: a published port is a Critical finding - skills/deploy: blocking pre-flight audit - compose.yaml: db example now uses expose:, with depends_on healthcheck - docs/DEPLOYMENT.md + README: SSH tunnel instead of publishing for debugging
2.3 KiB
2.3 KiB
paths
| paths | |||||
|---|---|---|---|---|---|
|
Backend Rules
No preselected data layer
This template ships no database, ORM or BaaS. /architecture picks one per
project and records the choice in the spec's Technical Decisions table. Until that
decision exists, do not install an ORM or write schema code.
Whatever is chosen must be self-hostable inside the Docker Compose stack — the deployment target is a plain server, not a managed platform.
When you add it to compose.yaml, it gets no published port. The app reaches it
by service name over the compose network. See the port-exposure rule in
.claude/rules/security.md.
Route Handlers
- Validate every request body with a Zod schema before touching it. Reuse the schema the form already uses; never trust client-side validation alone.
- Check authentication before doing any work — see
.claude/rules/security.md - Return correct status codes: 400 validation, 401 unauthenticated, 403 unauthorized, 404 missing, 409 conflict, 500 unexpected
- Error responses carry a message safe to show a user. Log the detail server-side; never leak stack traces, SQL or internal IDs to the client.
Data Access
- Scope every query by the current user. Ownership is checked in the query
itself (
where userId = session.user.id), not after fetching. A filter applied in application code after a broad read is a data leak waiting for a bug. - Index columns used in WHERE, ORDER BY and JOIN
- Foreign keys with an explicit ON DELETE behaviour
- Always paginate or
.limit()list queries — no unbounded reads - Use joins instead of looping queries (N+1)
- Only parameterized queries. Never build SQL through string concatenation.
- Handle the error branch of every database call
Migrations
- Schema changes go through checked-in migration files, never manual edits on a running database
- Migrations must be reversible or explicitly documented as one-way
- The deployment step runs pending migrations before the new container serves
traffic — see
docs/DEPLOYMENT.md
Secrets
- Read configuration from
process.env, never hardcode it - Every new variable goes into
.env.examplewith a dummy value in the same commit NEXT_PUBLIC_only for values that are safe in a browser bundle