# fullstack-template backend Go 1.27 API server template: stdlib `net/http` routing (Go 1.22+ method patterns with `r.PathValue`), [Bun](https://bun.uptrace.dev) ORM (SQLite file by default, Postgres via `DATABASE_URL`), and a layered layout (handler → service → repository behind interfaces) designed to be copied per resource and iterated on with an LLM. ## Quickstart No containers, no database server — the default database is a local SQLite file (`dev.db`): ```sh cd backend make migrate-up # apply migrations (creates dev.db) make run # API on :8080 ``` Try it: ```sh curl -s localhost:8080/api/v1/items curl -s -X POST localhost:8080/api/v1/items \ -H 'Content-Type: application/json' \ -d '{"title":"First item","description":"Hello template"}' curl -s localhost:8080/api/v1/items/1 curl -s -X PATCH localhost:8080/api/v1/items/1 \ -H 'Content-Type: application/json' -d '{"status":"archived"}' curl -s -i -X DELETE localhost:8080/api/v1/items/1 ``` ## Endpoints | Method | Path | Notes | |---|---|---| | GET | `/healthz` | liveness | | GET | `/readyz` | checks the database | | GET | `/api/auth/session` | who is signed in; always 200, `{"authenticated":bool,"user":…\|null}` | | GET | `/api/auth/login` | starts the OIDC redirect; optional `?returnTo=/path` | | GET | `/api/auth/callback` | issuer lands here; do not call directly | | POST | `/api/auth/logout` | clears the session cookie; 204 | | GET | `/api/v1/items` | `?status=active|archived&limit=&offset=` — requires a session when auth is on | | POST | `/api/v1/items` | 201 + `Location`, body `{"title","description","status"}` | | GET | `/api/v1/items/{id}` | | | PATCH | `/api/v1/items/{id}` | partial update; `null`-absent fields untouched | | DELETE | `/api/v1/items/{id}` | 204 | Errors are always `{"error":{"code":"...","message":"..."}}`. ## OIDC authentication Set `OIDC_CLIENT_ID` / `OIDC_CLIENT_SECRET` (see [.env.example](.env.example)) and the API authenticates against the issuer at `OIDC_ISSUER` (default `https://users.k6n.net`) using the authorization-code + PKCE flow: - The browser is sent to the issuer from `GET /api/auth/login`; the callback path is fixed at `/api/auth/callback`. The redirect URI is derived from the incoming request (`X-Forwarded-Host`/`X-Forwarded-Proto` when behind the Vite proxy, `Host`/TLS otherwise), so the same server works on `localhost:` (loopback exception), `*.preview.k6n.net` (registered wildcard), and the API origin itself — as long as those URIs are registered for the client with the issuer. - The signed-in identity (sub, name, email, roles) is kept in an HMAC-SHA256-signed session cookie (`HttpOnly`, `SameSite=Lax`). ID and access tokens never reach the browser and are not persisted server-side; - When auth is enabled, everything under `/api/v1/` answers `401 unauthorized` without a valid session. `/healthz`, `/readyz`, and `/api/auth/*` stay public. - Without `OIDC_CLIENT_ID` the server runs standalone: no `/api/auth` routes and an open `/api/v1` (logged loudly at boot). `AUTH_SESSION_SECRET` signs the cookies — unset it only in development (a random one is generated per boot, invalidating all sessions on restart). `AUTH_SESSION_TTL` (default `168h`) bounds the session. ### Agent view (`/api/v1/browse`) `GET /api/v1/browse?url=` fetches the page server-side, renders it as markdown (the way a text-only agent sees it), gathers `/llms.txt`, `/llms-full.txt`, `/.well-known/llms.txt` and `/robots.txt` from the site root, and returns the snapshot plus agent-readiness checks. Companion files are fetched concurrently; each outbound request is bounded by `FETCH_TIMEOUT` (default `15s`) and response bodies are capped at 2 MiB. `FETCH_USER_AGENT` (default `LLM-Browser/0.1 (…)`) identifies the server to inspected sites. ## Layout ``` cmd/ api/ server entrypoint (config, wiring, graceful shutdown) migrate/ migration CLI: up | down | status | new internal/ api/ routing, middleware, JSON conventions, item + auth handlers auth/ OIDC client (code + PKCE flow) and signed session cookies config/ env parsing — the only place reading os.Getenv database/ bun connection (sqlite or postgres) + migrator migrations/ one Go file per migration: _.go items/ reference resource: model, Repository interface, Bun implementation, service (business rules) compose.yaml optional dev Postgres Makefile `make check` = fmt-check + vet + test ``` `internal/items` is the pattern to copy when adding a resource — see AGENTS.md for the step-by-step recipe. ## Configuration All settings come from the environment and a `.env` file in backend/ is autoloaded. The database defaults to the SQLite file `dev.db`; for Postgres run `make db-up` and set `DATABASE_URL=postgres://fullstack:fullstack@localhost:5432/fullstack?sslmode=disable`. See [.env.example](.env.example) for the full list. ## Tests ```sh make test # or: go test ./... ``` Service and handler tests run against in-memory fakes; the database test runs the real migrations and repository against in-memory SQLite — no database server needed for any of them.