go-bananas/docs/development.md
Daniel Noulet 2967f1a5af
Some checks are pending
Build, test, publish, deploy / build-test (push) Waiting to run
Build, test, publish, deploy / publish (push) Blocked by required conditions
Build, test, publish, deploy / deploy-prd (push) Blocked by required conditions
refactor
2026-10-10 02:30:01 +02:00

8.9 KiB

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

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

make dev-all       # API on :8080, admin UI on :5173 with a proxy (builds the SDK first)

Open http://localhost:5173. 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:

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:

# 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 for the dependency rule and standards.md for the conventions. Read both before a substantial change.

Tests

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

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 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.