# Decisions Spec-silent choices, recorded as required by the build instructions. ## Phase 1 - **Go toolchain 1.26.4** (latest stable; spec requires ≥ 1.22). Installed at `/usr/local/go`. - **Module name `bountyboard`** — no remote repository exists; a single-word module keeps import paths short (`bountyboard/internal/...`). - **Package in `internal/http` is named `httpx`** to avoid clashing with stdlib `net/http` inside its own files. The directory stays `internal/http` per spec §14; importers alias it (`httpx "bountyboard/internal/http"`). - **`/readyz` uses a pluggable checker registry** (`AddReadinessCheck`): required checks (Mongo, Phase 2) gate readiness with 503; optional checks (atomizer / work performer, Phase 11) are reported as `degraded` but stay 200, matching §6's "non-fatal, reported". - **Runtime image is `alpine:3.21`, not distroless** — busybox `wget` enables the compose healthcheck without adding tooling, and `ca-certificates` is needed for outbound HTTPS to Jira/ADO/YouTrack. Non-root user. - **Config validation is strict and fail-fast**: `CREDENTIALS_ENC_KEY` is mandatory at startup (base64, exactly 32 bytes) and `SESSION_SECRET` must be ≥ 16 chars, even before the features using them land — a misconfigured deployment should die at boot, not at first use. All validation errors are reported together via `errors.Join`. - **Trusted-proxy resolution walks X-Forwarded-For right-to-left**, skipping addresses inside `TRUSTED_PROXY_CIDRS`; the first untrusted address is the client. A malformed entry stops the walk (everything left of it is attacker-controllable). If the whole chain is trusted, the leftmost entry is used. Headers are ignored entirely when the direct peer is untrusted. - **ULID**: canonical 48-bit ms timestamp + 80-bit crypto randomness, no intra-millisecond monotonicity (spec only needs sortability at ms resolution; randomness makes collisions negligible). - **`.env` loader semantics**: real environment variables win over `.env` values; unquoted values support trailing ` # comment`; missing `.env` file is not an error (containers receive env via compose `env_file`). - **Makefile `seed`/`backup`/`restore` are failing stubs until Phase 12** so the targets exist but cannot be mistaken for working. ## Phase 2 - **Integration tests reach Mongo via `docker-compose.test.yml`**, an explicit test-only overlay publishing Mongo on `127.0.0.1:27017`. The normal compose file still never publishes Mongo (§12); the spec's integration-test requirement (§2.1) needs host access, and a loopback-only opt-in overlay is the smallest hole. Each run uses a `bountyboard_test_` database and drops it on cleanup. - **`UpdateVersioned` rejects updates that touch `version`** and merges `updatedAt`/`$inc version` into the caller's operators; it distinguishes `ErrNotFound` from `ErrVersionConflict` with a follow-up existence check. - **File MIME type = sniffed (`http.DetectContentType`) unless inconclusive** (`application/octet-stream`), in which case the client-declared type is kept. A confident sniff overrides a lying declaration. - **sha256 of uploads is recorded post-upload** via an update on `fs.files` metadata (GridFS metadata must be supplied before streaming; the hash is only known after). - **Signed file tokens** are `base64url(exp || hmac-sha256(fileID|exp))` with a key derived as `sha256("bountyboard/file-url/v1" + SESSION_SECRET)` so file tokens can never collide with other uses of the session secret. ## Phase 3 - **OIDC linking requires `email_verified=true`.** An unverified IdP email matching an existing local account is rejected (`oidc_email_unverified`) instead of linking or creating a duplicate — linking on unverified email would allow account takeover via a rogue IdP account. - **OIDC provider discovery is lazy** (first login attempt, retried on failure) so an unreachable IdP cannot block app startup. - **CSRF protection applies to authenticated mutations only**; login and register are pre-session and protected by rate limiting + SameSite=Lax. The CSRF cookie is intentionally JS-readable (double-submit pattern). - **Password change revokes all other sessions** of the user (recovery semantics) but keeps the current one. `SetPassword` is deliberately not version-checked: a password change must never lose an optimistic-locking race against a profile edit. - **Forced password change (`mustChange`)** is enforced in `requireAuth` via a path allowlist (`me`, `logout`, `logout-all`, `change-password`). - **Sessions slide at most once per hour** (refreshedAt watermark) instead of updating expiry on every request. - **Login rate-limit key is `clientIP|email`** (and `register|clientIP` for registration), using the trusted-proxy-resolved IP. - **Mongo runs with `--wiredTigerCacheSizeGB 0.5`** in compose — the target host has 4 GB RAM; default cache (≈50% of RAM) caused a restart under the integration-test load. ## Phase 4 - **CSP has no `unsafe-inline` for scripts from day one** (§12), so all JS is external ES modules (`theme.js` loads synchronously in `` to avoid a theme flash; everything else is `type=module`). Inline event handlers are never used. - **`PATCH /api/v1/profile` is a partial update**: only fields present in the body change. `version` is optional — when present the update is version-checked (409 + reload toast on conflict); the nav theme toggle omits it so flipping themes can't conflict with a profile edit. - **Avatar uploads must sniff as `image/*`**; the rejected/replaced GridFS files are deleted eagerly. Replacing an avatar removes the previous file. - **File serving (`GET /files/{id}`)**: valid signed token (?st=) OR session. With a session: avatars are visible to any logged-in user; other scopes (chat/task) currently allow owner/admin/consultant and will be tightened as those features land. - **Pages that need login redirect to `/login?next=…`** (HTML UX), while API routes return 401 JSON. Forced password change redirects all pages to /change-password. - **Pages for later phases render an "under construction" placeholder** so role-based navigation is complete and clickable now. ## Phases 5–12 (selected) - **`internal/extsvc`** (not in the §14 list) holds the circuit breaker and retrying JSON client shared by the two service clients — sharing plumbing, not state; each service keeps its own breaker, URL, and token. - **Mock services are separate codebases**: `services/atomizer` has its own `go.mod` (stdlib only); `services/work-performer` is plain Node with zero npm dependencies (plus the globally installed claude CLI in its image). - **AI-approved tasks earn no `bountyAwards` row** — §4.5 awards are a developer performance ledger; the AI submitter has no developer identity. - **Bulk archive/publish are partial-success APIs** returning `{done[], failed{}}` instead of failing the whole batch. - **Extension siblings record `parentId = source task`** (spec literal), so the UI tree shows extensions beneath their source. ## Phase 13 (deployment) - **`APP_INTERNAL_URL` (compose: `http://app:8787`)** is handed to the external services for callback URLs and signed attachment URLs — the §5.2 example uses the in-network hostname; `APP_BASE_URL` stays browser-facing. - **The work-performer container runs as the `node` user** (uid 1000) with `${HOME}/.claude` mounted into `/home/node/` instead of `/root/` (§9.2 shows /root): the claude CLI refuses `--dangerously-skip-permissions` as root, so the literal spec mount can never execute jobs. - **Run compose with the real user's HOME** — `sudo docker compose` resolves `${HOME}` to `/root` and silently mounts the wrong Claude credentials. Use `sudo --preserve-env=HOME docker compose …` (or run docker unprivileged). - **UFW**: a rule allowing `172.16.0.0/12` (docker networks) to reach the host was added so container→host callbacks work in contract tests. - **`scripts/acceptance.sh`** automates the §13 checklist live (import → subdivide → extend → publish → claim/decline/approve → review → award → AI job via real Claude Code → breaker independence) and is re-run-safe.