# Deploying to mcp.krowforce.com Prepared and verified. **Not executed** — this machine has no Docker daemon, no SSH access to the host and no registry credentials, and a production rollout is not something to do without the operator watching. --- ## 1. What is running now `954ba90` — or equivalently `cadea4b`, the merge commit whose tree is byte-identical. Established from the outside, without credentials: | Observation | Command | Conclusion | | --- | --- | --- | | Preflight echoes the origin and `Access-Control-Allow-Credentials` | `OPTIONS /api/v1/job-postings -H 'Origin: https://platform.krowforce.com'` → `204` | ≥ `954ba90` — that header was added there and is absent at `7d12ebe` | | Session cookie is `SameSite=Lax` while a CORS allowlist is configured | `POST /api/v1/auth/logout` → `Set-Cookie: … Secure; SameSite=Lax` | < `b6f8655` — from that commit a configured allowlist forces `SameSite=None` | | `routeOwliver` absent from `server.go` at `cadea4b` | `git show cadea4b:…/server.go \| grep s.route` | `GET /api/v1/owliver/suggestions` is not registered | **52 endpoints deployed. 53 at `HEAD`.** The missing one is the Owliver suggestions route, which is the 404 the frontend sees. `/health` returns `{"status":"ok"}` — not `degraded` — so the remote schema is present and not dirty. ## 2. What is being deployed `b6f8655`, which is `HEAD` and is already `origin/main`. Nothing needs pushing. **Plus one commit** prepared here — see §4. It is required: without it this deploy silently removes the only CSRF protection the API has. Local working-tree changes (the database-backed Owliver suggestion context) are **not** part of this deploy and are not on any branch. They do not reach the host, which builds from `origin/main`. ### Verified before shipping | Check | Result | | --- | --- | | `go build ./...`, `go vet ./...` | clean | | `GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build ./cmd/api` | 17 MB static ELF, builds clean | | `go test ./...` | 8/8 packages pass, against real migrated PostgreSQL | | Migration delta `cadea4b → HEAD` | **none** — `git diff cadea4b HEAD -- migrations/` is empty | | Config compatibility | no new validation; the new binary accepts a strict superset of what the host is configured with | **No schema step.** This is a binary-only rollout. ## 3. What changes in behaviour Beyond the new route, `b6f8655` fixes three things already broken in production: - **`POST /api/v1/ai-interviews`** becomes transactional — it sets `interview_id`, advances the application to `interview` and copies the score. The deployed version writes only the interview, and the frontend deliberately does not patch the application afterwards, so **completing an interview currently leaves the candidate un-advanced with nothing to show it.** - **`POST /api/v1/job-postings/{id}/assignments`** may now file an application for a worker placed from the talent pool who never applied. It currently cannot, so those assignments fail. - **`serverSupplies`** now checks the caller's role. Previously a talent-only derived column was treated as server-supplied for every role, so an **operator** creating a job application without an email passed validation and hit a NOT NULL violation — **a 500 where the contract promises 422.** ## 4. The required extra commit — cookie posture `b6f8655` alone derives `SameSite` from whether a CORS allowlist is configured: non-empty ⇒ `None`. On this host the allowlist *is* non-empty, so deploying it as-is flips the session cookie from `Lax` to `None`. **`SameSite` is the only CSRF protection this API has.** There is no CSRF token. The derivation is wrong for this topology, because CORS and SameSite answer different questions: - **CORS** is about **origin**. `platform.krowforce.com` → `mcp.krowforce.com` is cross-origin, so the allowlist is genuinely required. Confirmed live: that origin returns `204` with the origin echoed; an unlisted origin returns `403`. - **SameSite** is about **site**. Both share the registrable domain `krowforce.com`, so they are **same-site** and a `Lax` cookie is already sent on those requests. So the correct configuration is *CORS on, `SameSite=Lax`* — a combination `b6f8655` cannot express. The commit makes an explicit `HTTP_COOKIE_SAMESITE` authoritative and leaves the CORS-derived value as the default when it is unset. It also closes a trap: `config.Load` has always parsed and validated that variable, and nothing read it, so a deployment that set it saw it silently ignored. ``` internal/config/config.go unset is "" rather than defaulting to "lax" internal/httpserver/auth.go explicit value wins; allowlist decides the default internal/httpserver/samesite_test.go the full matrix, pinned ``` `docker-compose.yml` already passes `HTTP_COOKIE_SAMESITE: ${…:-lax}`, so a compose deploy keeps `Lax` without any `.env` change. > **Do not empty `HTTP_CORS_ORIGINS`.** It looks like a way to keep `Lax` > without a code change, and it would take `platform.krowforce.com` offline — > that frontend calls the API cross-origin from the browser. ## 5. Rollout Migrations first, then the binary — the order `docker-compose.yml` already encodes through `depends_on: service_completed_successfully`. There is nothing to migrate this time, but the step is a no-op rather than something to skip. ```sh # On the host, from the repository root git fetch origin && git checkout b6f8655 # or the extra commit from §4 cd infrastructure docker compose build api docker compose up -d --no-deps migrate # exits 0, nothing to apply docker compose up -d --no-deps api docker compose ps # api healthy docker compose logs -n 50 api # expect: "endpoints":53 ``` `"endpoints":53` in the startup log is the single fastest confirmation that the right binary is running. ## 6. Verification ```sh KROW_EMAIL=… KROW_PASSWORD=… ./scripts/verify-deployment.sh ``` Read-only by default. Add `--write` to prove the write path reaches PostgreSQL; it creates one posting with `status: draft`, which is invisible to talent — and permanent, because `JobPosting` has no `DELETE`. It checks, in order: `/health` and whether the schema reads `degraded`; login and the issued cookie; the caller's `role` (a `talent` role explains almost every 403); `GET /owliver/suggestions` as the version discriminator; that `/api/v1/positions` still `404`s; `GET /job-postings`; both suggestion modes and the 3-item cap; and the `SameSite` attribute actually being served. Exit status is the number of failures, so it can gate a rollout. ### The resource is `job-postings` `/api/v1/positions` has never existed in this API. Every `/positions` in the frontend is a React Router **UI** route. Two tests assert the phantom stays absent — `TestThereIsNoPositionsResource` and the script's own check — so nobody "fixes" a future 404 by adding a duplicate resource. ## 7. Rollback The previous image is still on the host. ```sh docker compose down api git checkout cadea4b docker compose build api && docker compose up -d --no-deps api ``` No schema change means rollback is clean: nothing to reverse, and the old binary runs against the current schema unchanged. ## 8. Separately — the deployed frontends are not reaching the API Found while verifying, out of scope for this deploy, and more severe than the 404. `platform.krowforce.com` is built with `VITE_API_BASE_URL=https://mcp.krowforce.com/api/v1` — an absolute cross-origin URL. The repository's own `.env` warns against this at length, and `httpClient.js` has a guard for it that is compiled out of production builds, so it fails silently. `app.krowforce.com` is worse off: its origin is **not** on the backend's allowlist (`403` at preflight), and its bundle carries neither `auth/login` nor any reference to the API host. Both hosts also serve their SPA for `/api/v1/*` — `GET /api/v1/me` on either returns `index.html` with status `200`. The shipped `nginx.conf` has no `/api` proxy at all, and `try_files $uri $uri/ /index.html` swallows every API path. The fix is a `/api` and `/health` `proxy_pass` in the frontend's nginx plus a rebuild with `VITE_API_BASE_URL=/api/v1`, which is what the same-origin design assumes. That is a frontend deployment change and belongs in its own rollout.