# Backend — agent handbook Go 1.27 API server: stdlib `net/http` (Go 1.22+ method patterns + `r.PathValue`), Bun ORM (SQLite file by default, Postgres via DSN), layered so each change touches a predictable set of files. Follow this handbook unless the user says otherwise. ## Commands | Task | Command | |---|---| | Full gate: fmt-check + vet + tests | `make check` — run before declaring any change done | | Dev server | `make run` from `backend/` (SQLite: no setup, `make migrate-up` once) | | Tests | `make test` (`go test ./...`; no database server required) | | Optional Postgres up / down | `make db-up` / `make db-down` (set `DATABASE_URL=postgres://…`) | | Migrations | `make migrate-up` / `make migrate-down` / `make migrate-status` | | New migration | `make migrate-new name=create_projects` | | Binaries | `make build` (writes `bin/`) | No linters beyond `go vet` and `gofmt` are configured — don't add config files for other tools unless asked. ## Architecture — dependencies flow one way ``` cmd/api (wiring only) │ ▼ internal/api ──HTTP──▶ internal/items.Service ──▶ items.Repository (interface) (transport) (business rules) ▲ BunRepository (SQL) ``` - `internal/api` — transport only: route patterns, parse input, call the service, map errors to status codes. No SQL, no business rules. - `internal/items/service.go` — validation and business rules. Depends on the `Repository` interface, never on `*bun.DB`; that is what makes the tests fake-backed. - `internal/items/repository.go` — the only file speaking Bun/SQL. - `internal/config` — the only file reading `os.Getenv`. - `cmd/*` — wiring and process concerns (signals, exit codes). No logic. Imports may only point rightward in the diagram. `internal/api` must never import `internal/database`, and nothing imports `internal/api` except `cmd`. ## Adding a resource (the recipe) `items` is the reference. For a new resource `projects`: 1. `internal/projects/model.go` — struct with `bun:"table:…,alias:…"` tags, json tags, `Repository` interface, `ErrNotFound`, `ValidationError`. 2. `internal/projects/repository.go` — `BunRepository` implementing it. 3. `internal/projects/service.go` — `Service`, `CreateInput`, pointer-field `UpdateInput`, validation. 4. `make migrate-new name=create_projects`, then fill in the generated Go migration in `internal/database/migrations/` (model-driven DDL, plus a down migration). 5. `internal/api/projects.go` — handler struct + `fail` error mapping. 6. `internal/api/server.go` — register routes on the mux. 7. Tests: `projects/service_test.go` (fake repo) + extend `api/server_test.go`. 8. `make check`. ## HTTP conventions - Routes are method-prefixed ServeMux patterns (`"PATCH /api/v1/projects/{id}"`); read segments with `r.PathValue("id")`, parse via the `pathID` helper. Wrong methods get 405 + `Allow` — the `jsonNotFounds` wrapper in server.go keeps that mux behavior, so don't register catch-all routes. - Success bodies are the resource JSON directly; lists are bare arrays. Create answers 201 + `Location`; delete answers 204. - Errors are always `{"error":{"code","message"}}`. Codes live in `internal/api/respond.go`: `invalid_json`, `validation_failed`, `not_found`, `invalid_id`, `method_not_allowed`, `internal_error`, `timeout`, `unavailable`. Map domain errors only in the handler's `fail` — sentinel `ErrNotFound` → 404, `*ValidationError` → 400, everything else → 500 (logged, never leaked to the client). - Decode request bodies with `decodeJSON` (strict: unknown fields and >1 MiB bodies are 400s). List endpoints take `?limit=&offset=` and resource-specific filters; defaults and caps live in the service. - Middleware chain (outermost first): logging → recover → cors → timeout → requireAuth (only when OIDC is configured). Add transport-level concerns there, not in handlers. ## Auth OIDC authorization-code + PKCE against the issuer in `OIDC_ISSUER` (default `https://users.k6n.net`). Enabled iff `OIDC_CLIENT_ID` is set; otherwise the server runs standalone (no `/api/auth` routes, `/api/v1` open) and logs that loudly. Pieces: - `internal/auth` — the OIDC client (discovery-driven endpoints, never hardcoded paths) and the cookie layer: session + login-transaction values are `.` sealed by `Signer`. Tokens stay server-side; the browser only ever holds the session cookie. - `internal/api/auth.go` — the four routes: `GET /api/auth/session` (always 200), `GET /api/auth/login?returnTo=`, `GET /api/auth/callback`, `POST /api/auth/logout`. The callback path `/api/auth/callback` is fixed; the redirect URI is derived per request from `X-Forwarded-Host`/`Proto` (set by the Vite proxy) so localhost, `*.preview.k6n.net`, and the API origin all work with one client registration. - `requireAuth` guards `/api/v1/*` with the session cookie (unknown sub-routes answer 401, not 404). It is skipped in standalone mode and in `api.New` tests that omit `Deps.Auth`. - Login failures redirect to `/?auth_error=` — the SPA reads that; details go to server logs only. `returnTo` is sanitized to a relative path (no `//`, no control chars) to stay an internal redirect. ## Migrations - One Go file per migration in `internal/database/migrations/`, named `_.go`, registering itself via `Migrations.MustRegister(up, down)` in `init()`. `make migrate-new name=` scaffolds it with a fresh timestamp — never invent timestamps by hand. Apply with `go run ./cmd/migrate up` (also `down`, `status`). - Prefer model-driven DDL (`db.NewCreateTable().Model((*pkg.Thing)(nil))`): bun emits dialect-correct SQL for SQLite and Postgres, so migrations stay portable across the default file database and a real Postgres. Use `db.Exec` with raw SQL only when a model can't express it. - Never edit or reorder an applied migration — add a new one. Every migration gets a down migration. - `internal/database/database_test.go` runs all migrations plus the items repository against in-memory SQLite — extend it when adding resources so dialect drift fails `make check`, not production. ## Databases - Default: `DATABASE_URL=sqlite:dev.db` — a local file (modernc pure-Go driver, no CGO, WAL + busy_timeout applied automatically unless the DSN carries its own `_pragma` params). Zero setup for iteration. - Postgres: `make db-up` then `DATABASE_URL=postgres://fullstack:fullstack@localhost:5432/fullstack?sslmode=disable`. The repository layer uses portable queries (`?` placeholders, no dialect-specific SQL) — if you must, branch on `db.Dialect().Name()`. ## Testing - Three layers: `internal//service_test.go` drives business rules through an in-memory fake of the Repository interface; `internal/api/server_test.go` drives the real mux + middleware with the same fake behind the real service; `internal/database/database_test.go` runs migrations + the real BunRepository against in-memory SQLite. - Fakes are package-local and duplicated on purpose — don't build a shared testutil package until there are enough resources to justify it. - Table-driven tests for error cases; one flow test per handler group. ## Config - Env-only; `.env` in backend/ is autoloaded by `cmd/api`. Add a field in `internal/config/config.go`, a line in `.env.example`, and a row in the README table. - CORS allows the Vite dev server (`http://localhost:5173`) by default so the frontend can call this API during development.