# Development Working on go-bananas. ## Requirements | Tool | Version | Why | | --- | --- | --- | | Go | 1.27 or later | Static binaries, `net/http` routing primitives | | Node.js | 22 or later | Vite 8 requires it | | Docker | 24 or later | BuildKit for the multi-stage image | | Helm | 3.14 or later | Chart validation and `helm lint --strict` | Optional, installed by `make tools`: `air` for hot reload, `golangci-lint` for linting, `sqlc` if you add a query. ## Setup ```bash git clone https://github.com/urpi/go-bananas cd go-bananas make install # Go modules, admin UI dependencies, SDK dependencies make tools # air, golangci-lint, sqlc ``` ## Running ```bash make dev-all # API on :8080, admin UI on :5173 with a proxy (builds the SDK first) ``` Open . The Vite dev server proxies `/api`, `/rt` and `/auth` to the Go process, so the session cookie works exactly as it will in production. Same-origin matters: a cross-origin setup would not authenticate at all, and the problem would look like a bug rather than a topology difference. The admin UI is itself a consumer of the runtime: the header theme selector and the light/dark toggle are the shipped controls, loaded from `/rt/v1/components.js`, which the **API** serves from `sdk/dist` (Vite proxies `/rt` to it). `make dev-all` builds the SDK up front for exactly this reason — an unbuilt `sdk/dist` means those controls never define and the switcher silently falls back. Other entry points: ```bash make dev # the API with hot reload (air); serves web/dist and sdk/dist, so `make build` first make dev-api # the API alone, no hot reload; serves the runtime SDK, Vite serves the UI make dev-web # the admin UI alone (its /api and /rt proxy to :8080) make run # build everything, run the real binary ``` ### Trying the theme preference locally The theme switcher in the header writes to the account, not just to the browser, so the selection follows you to another browser and survives a re-login. To see that end to end without a second browser: ```bash # 1. Start the stack and sign in. `make dev-all` defaults to AUTH_MODE=dev, so the # dev-user query parameter is enough to be somebody: # http://localhost:5173/?dev-user=designer@example.test # 2. Pick a published theme and a scheme in the header switcher. That is a PUT to # /api/v1/me/preferences. # 3. Read back what the server decided, which is the authoritative value: curl -s localhost:8080/api/v1/me/preferences \ -H 'Cookie: go-bananas-session=...' | jq # 4. Confirm it arrives with the session, which is how the UI gets it on first paint: curl -s localhost:8080/api/v1/auth/me \ -H 'Cookie: go-bananas-session=...' | jq .preferences # 5. Prove it is account state rather than cookie state by clearing local storage, # or by asking in a second browser profile: the theme you chose is still selected. ``` Two things are deliberately not local state: - **The theme is a preference, not a session claim.** The token comes from the identity provider, so this service cannot put anything in it; `/api/v1/me/preferences` is the contract instead. - **`localStorage` is still read on the first render.** A server round trip cannot happen before paint, and waiting for one flashes the wrong theme on every load. The stored preference is adopted as soon as the session arrives, and a change writes both places, so the next paint is correct even offline. Pointing it at a draft theme returns 400, and pointing it at a theme that has since been archived reads back as no choice. Both are covered in `docs/api.md`. ## Layout ``` cmd/go-bananas/ main(): signals, exit codes, nothing else internal/ app/ dependency injection and the process lifecycle auth/ OIDC, sessions, roles config/ environment parsing and validation domain/ entities, invariants, sentinel errors httpapi/ routing, middleware, handlers logging/ slog configuration seed/ the built-in themes store/ SQLite: migrations and repositories tokenengine/ pure logic: colour, validation, rendering store/migrations/ embedded SQL, applied at start-up web/ the administration UI (React, Vite, Tailwind) sdk/ the runtime SDKs, served from /rt/v1 deploy/helm/go-bananas/ the chart docs/ this, plus the ADRs ``` See [architecture.md](architecture.md) for the dependency rule and [standards.md](standards.md) for the conventions. Read both before a substantial change. ## Tests ```bash make test # everything make test-go # Go, with the race detector make test-web # the admin UI make test-sdk # the runtime SDK make test-cover # Go, with a coverage profile make bench # the token engine benchmarks ``` Every test run in CI is `go test -race`. A data race in a cache or a store is a production incident waiting for load. ## The pre-merge gate ```bash make verify # check + helm lint ``` This is exactly what CI runs. A pull request that does not pass it will not merge. ## Things that will surprise you **The admin UI consumes the runtime.** It loads themes from `/rt/v1` and maps the variables onto Tailwind's namespaces. Switching the UI to the "Light" or "Dark" mode in the top bar loads a published theme's real stylesheet. A change to the token engine that breaks delivery breaks the editor, immediately. **Publishing bumps the version, and the version is the cache key.** Every mutation does, not only publication. If you change `store.Update`, check whether the version needs to move. **History is append-only.** A rollback restores a snapshot as a *new* version. There is no code path that removes a version, by design: an application deployed against version 7 must never have version 7 taken away. **The built-in themes are reconciled, not overwritten.** `internal/seed` compares the stored version with the seed version. An edited built-in theme is recognised by its higher version and left alone. **Validation gates publication, not editing.** A draft can be incomplete. That is what makes building a theme up incrementally practical. ## Common changes ### Adding a token group 1. Add a `GroupSpec` to `tokenengine.Groups`. 2. Add the tokens to the built-in themes in `internal/seed`. 3. Run `make test-go`. The seed tests validate every built-in theme and check WCAG contrast for body text on every surface, so an inaccessible colour fails the build. ### Adding an API endpoint 1. The handler in `internal/httpapi`. No business logic; call the store, encode the result. 2. The route in `internal/httpapi/router.go`, with the role it requires. 3. The type in `web/src/lib/types.ts` and the client function in `web/src/lib/api.ts`. 4. The documentation in `docs/api.md`. 5. Tests for each branch and each authorisation decision. ### Adding a configuration variable 1. The field and default in `internal/config`. 2. The parsing in `loadFrom`. 3. A validation rule in `Validate` if it can be wrong. 4. The environment variable in the chart's `configmap.yaml` and `values.yaml`, and in `values.schema.json`. 5. The table in `docs/deployment.md`. A variable with no validation is a variable that will be set to something wrong. ### Changing the runtime contract Add, do not change. `/rt/v1` is a contract with applications that have no way to upgrade in step with you. Adding an optional query parameter or a response field is safe. Changing the meaning of an existing one is not. ## Conventions Enforced, not suggested. `make check` is the same command CI runs. | | | | --- | --- | | Go formatting | `gofmt`, `make fmt-check` | | Go lint | `golangci-lint`, `make lint-go` | | Go imports | `gci`, configured in `.golangci.yml` | | TypeScript | `tsc --build`, strict | | Frontend lint | `eslint --max-warnings 0` | | Frontend format | `prettier`, including the Tailwind plugin | | Chart | `helm lint --strict` plus `values.schema.json` | Read [standards.md](standards.md) before contributing. It is the difference between a change that fits and one that has to be rewritten. ## Troubleshooting **Port 8080 in use.** `make dev-api` with `BANANAS_HTTP_ADDR=:8081`, or stop whatever holds the port. **The admin UI cannot reach the API.** The Vite proxy targets `http://localhost:8080`. If the API is elsewhere, edit `server.proxy` in `vite.config.ts`. **`npm ci` fails.** A stale `node_modules`. `make clean-cache`. **`go build` fails after a dependency change.** `make tidy`, then `make install`. **A test is flaky.** Do not re-run it and hope. Find the ordering dependency. The most common cause here is module-level state in a frontend test: use `vi.resetModules()` and a fresh module instance. **The chart will not render.** Read the error. Every chart failure names the value to change — that is deliberate, and the alternative is a failure at run time instead.