Files
llm-browser/backend
2026-10-19 16:04:17 +02:00
..
2026-10-19 16:04:17 +02:00
2026-10-19 16:04:17 +02:00
2026-10-19 16:04:17 +02:00
2026-10-19 16:04:17 +02:00
2026-10-19 16:04:17 +02:00
2026-10-19 16:04:17 +02:00
2026-10-19 16:04:17 +02:00
2026-10-19 16:04:17 +02:00
2026-10-19 16:04:17 +02:00
2026-10-19 16:04:17 +02:00

fullstack-template backend

Go 1.27 API server template: stdlib net/http routing (Go 1.22+ method patterns with r.PathValue), Bun 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):

cd backend
make migrate-up     # apply migrations (creates dev.db)
make run            # API on :8080

Try it:

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
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) 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:<any port> (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.

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: <timestamp>_<name>.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 for the full list.

Tests

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.