Files
krow_backend/infrastructure/docker-compose.local-db.yml
Suriyakumarvijayanayagam 954ba9076f Add CORS credentials, transactional endpoints, and container deployment
CORS
  cors.go never set Access-Control-Allow-Credentials, so the
  cookie-authenticated API was unreadable from any cross-origin frontend:
  the server answered correctly and the browser blocked the page from
  reading it. Set for allowlisted origins on both the preflight and the
  actual response. Three tests added.

  HTTP_COOKIE_SAMESITE (lax|none|strict, default lax) is new. CORS is only
  half of what a cross-origin browser call needs; SameSite is judged on
  registrable domain, so a frontend on an unrelated domain gets perfect CORS
  headers and still no cookie. "none" is the only value that survives that,
  and validate() refuses it without the Secure flag.

  The "*" rejection now explains itself: browsers refuse Allow-Origin "*"
  together with credentials, so it would break every authenticated call
  rather than loosen anything.

Transactional endpoints (api-contract.md 12.1)
  POST /api/v1/job-applications/{id}/hire
  POST /api/v1/job-postings/{id}/assignments

  Replaces two client-side loops that wrote several records with no
  transaction and no rollback. Each is now one endpoint and one transaction,
  built over repo.Repo so org scoping, derived columns, type casts and error
  translation are not re-derived. Authorization reuses the existing policy
  table rather than adding a parallel one: a workflow is exactly as
  privileged as the writes it performs. 13 tests, including both rollback
  paths.

Bug fix in the repository layer
  repo.bindValue handled int64/int/float64/string but not int32, which is
  what pgx returns for a PostgreSQL `int` column. Nothing previously read a
  record and wrote one of its fields elsewhere, so it never surfaced; the
  hire flow does exactly that and failed with "ai_score must be a number".
  Both KindInt and KindFloat now accept the widths pgx actually produces.

Deployment
  infrastructure/Dockerfile.api  multi-stage, cross-compiling (BUILDPLATFORM
    + GOARCH) so linux/amd64 builds from arm64 are compiled rather than
    emulated. Alpine runtime, non-root uid 10001, 22.1 MB. Ships api, seed,
    setpassword and migrate, plus the migrations, so a Kubernetes
    initContainer can apply the schema from the same image and tag as the
    API. HEALTHCHECK keys on status code, not body, so a "degraded" instance
    is not pulled from rotation during a migration window.

  infrastructure/docker-compose.yml  migrations run to completion before the
    API starts. Assumes a managed PostgreSQL; the local-db overlay adds one
    with TLS enabled so APP_ENV=production is met rather than dodged.

  scripts/drop_public_tables.go  the one-off used to clear an unrelated
    schema from krowdb on 2026-08-24, kept for the record. Build-tagged
    ignore and gated on CONFIRM_DROP=yes.

Verified against PostgreSQL: 16/16 new tests pass, and the image was built,
run and exercised end to end (login, CORS preflight, authenticated reads,
transaction rollback).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CmQiGq73Uyfq7J4yR8Vxxw
2026-08-25 11:33:01 +05:30

102 lines
4.0 KiB
YAML

# ============================================================================
# Overlay: run PostgreSQL alongside the API.
#
# docker compose -f docker-compose.yml -f docker-compose.local-db.yml up -d
#
# FOR STAGING AND FOR TESTING THE STACK ON ONE HOST. Not for production data.
# A database in a container on the same host as its application has no backup,
# no point-in-time recovery, no failover, and dies with the host. The base file
# assumes a managed PostgreSQL for exactly that reason.
#
# ── TLS ─────────────────────────────────────────────────────────────────────
#
# PostgreSQL is started with SSL on, using a self-signed certificate generated
# on first boot into the data volume. That is enough for DATABASE_SSLMODE=
# require, which encrypts the connection but does not verify who is on the
# other end — appropriate for a private compose network, NOT a substitute for
# verify-full against a real CA.
#
# It also means APP_ENV=production works here without weakening the check in
# internal/config, which is the point: the guard stays honest and the local
# stack meets it rather than dodging it.
#
# Set these in .env to match:
# DATABASE_HOST=postgres
# DATABASE_SSLMODE=require
# DATABASE_URL=postgres://krow:<url-encoded-password>@postgres:5432/krow?sslmode=require
# ============================================================================
services:
postgres:
image: postgres:16-alpine
container_name: krow-postgres
environment:
POSTGRES_DB: ${DATABASE_NAME:-krow}
POSTGRES_USER: ${DATABASE_USER:-krow}
POSTGRES_PASSWORD: ${DATABASE_PASSWORD:?DATABASE_PASSWORD is required}
# scram-sha-256 rather than the image's md5 default. md5 has been
# deprecated upstream for years and is trivial to crack from a captured
# handshake.
POSTGRES_INITDB_ARGS: "--auth-host=scram-sha-256 --auth-local=scram-sha-256"
# Generate a self-signed certificate on first boot, then hand off to the
# image's own entrypoint with SSL enabled. The key must be 0600 and owned
# by the postgres user or the server refuses to start — that check is the
# reason this is done here rather than in a bind mount, where host
# ownership would leak in.
command:
- sh
- -c
- |
set -e
CERT=/var/lib/postgresql/data/server.crt
KEY=/var/lib/postgresql/data/server.key
if [ ! -f "$$CERT" ]; then
mkdir -p /var/lib/postgresql/data
openssl req -new -x509 -days 3650 -nodes -text \
-out "$$CERT" -keyout "$$KEY" -subj "/CN=postgres" 2>/dev/null
chmod 0600 "$$KEY"
chown postgres:postgres "$$CERT" "$$KEY"
fi
exec docker-entrypoint.sh postgres \
-c ssl=on -c ssl_cert_file="$$CERT" -c ssl_key_file="$$KEY"
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
# -U and -d so this reports on the application's database, not on
# whatever `postgres` happens to be reachable.
test: ["CMD-SHELL", "pg_isready -U ${DATABASE_USER:-krow} -d ${DATABASE_NAME:-krow}"]
interval: 10s
timeout: 5s
retries: 10
start_period: 30s
ports:
# Loopback only. Publishing 5432 to the world is how a database ends up
# in someone else's botnet.
- "${POSTGRES_BIND:-127.0.0.1}:${POSTGRES_PORT:-5432}:5432"
restart: unless-stopped
stop_grace_period: 60s
security_opt:
- no-new-privileges:true
deploy:
resources:
limits:
memory: ${POSTGRES_MEMORY_LIMIT:-1G}
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"
networks: [krow]
# The migration runner now has to wait for the database to accept
# connections, which it does not need to do when the database is managed and
# already up.
migrate:
depends_on:
postgres:
condition: service_healthy
volumes:
postgres-data:
driver: local