diff --git a/.env.example b/.env.example index 77e698a9..dba5c774 100644 --- a/.env.example +++ b/.env.example @@ -1,34 +1,65 @@ # ============================================================================ # Tx OS — environment configuration. Copy to `.env` and edit before deploying. -# Everything in this file is read by docker-compose.yml at `up` time. +# Everything in this file is read either by docker-compose.yml at `up` time +# or directly by the application processes (`api-server`, `tx-os`, scripts). # NEVER commit `.env` — it is gitignored. `.env.example` is the public template. # ============================================================================ -# --- Web (nginx → API reverse proxy) ---------------------------------------- -# Host port that exposes the SPA. Front this with Caddy/Nginx/Traefik for TLS. -WEB_PORT=8080 - -# Public base URL the SPA + API are reachable at (used to build absolute -# upload URLs for the local-FS storage driver and CORS allow-list checks). -PUBLIC_BASE_URL=http://localhost:8080 +# ----------------------------------------------------------------------------- +# Host ports (compose only) +# ----------------------------------------------------------------------------- +# Port the SPA (nginx) is reachable on. Front with TLS in production. +WEB_PORT=3000 +# Port the API container is reachable on. In public deployments, bind this +# to 127.0.0.1 only so the edge proxy is the sole external entry point. +API_PORT=8080 +# Component preview server port (dev profile only). +MOCKUP_PORT=8081 +# ----------------------------------------------------------------------------- +# API server runtime +# ----------------------------------------------------------------------------- +# In compose this is set automatically; required when running natively. +PORT=8080 +# Public base URL the SPA + API are reachable at. Used to build absolute +# upload URLs for the local-FS storage driver and to validate CORS origins. +PUBLIC_BASE_URL=http://localhost:3000 # Comma-separated list of origins allowed by the API server's CORS layer. # Must include PUBLIC_BASE_URL and any additional reverse-proxy domains. -ALLOWED_ORIGINS=http://localhost:8080 +ALLOWED_ORIGINS=http://localhost:3000 +# pino log level: trace | debug | info | warn | error | fatal +LOG_LEVEL=info +# 32+ random bytes. Generate with `openssl rand -hex 32`. REQUIRED. +SESSION_SECRET=change-me-session-secret-min-32-chars-please -# --- Database --------------------------------------------------------------- +# ----------------------------------------------------------------------------- +# SPA build (tx-os) — read by Vite at build time AND `pnpm --filter tx-os dev` +# ----------------------------------------------------------------------------- +# Path the SPA is mounted at. Use "/" when serving from the domain root. +BASE_PATH=/ + +# ----------------------------------------------------------------------------- +# Database +# ----------------------------------------------------------------------------- POSTGRES_USER=tx POSTGRES_PASSWORD=change-me-postgres POSTGRES_DB=tx_os +# Composed automatically by docker-compose; set explicitly when running natively. +# DATABASE_URL=postgres://tx:change-me-postgres@localhost:5432/tx_os +DATABASE_URL=postgres://tx:change-me-postgres@db:5432/tx_os -# --- API server ------------------------------------------------------------- -# 32+ random bytes. Generate with `openssl rand -hex 32`. Required. -SESSION_SECRET=change-me-session-secret-min-32-chars-please +# ----------------------------------------------------------------------------- +# Object storage +# ----------------------------------------------------------------------------- +# Driver selection. Set to "s3" for production (MinIO/S3); "local" persists +# uploads on the API container's filesystem (single-host, dev-only). +# If unset, the driver auto-detects: "s3" when S3_ENDPOINT is set, else "local". +STORAGE_DRIVER=s3 -# pino log level: trace | debug | info | warn | error | fatal -LOG_LEVEL=info +# Bucket-level paths used by the API. Format: // +PRIVATE_OBJECT_DIR=/tx-private/private +PUBLIC_OBJECT_SEARCH_PATHS=/tx-public/public -# --- Object storage (S3 / MinIO) ------------------------------------------- # Bucket names — created on first boot by the minio-init container. S3_BUCKET_PRIVATE=tx-private S3_BUCKET_PUBLIC=tx-public @@ -41,13 +72,26 @@ S3_SECRET_ACCESS_KEY=change-me-minio-secret-min-20-chars # Region label is cosmetic for MinIO but @aws-sdk requires one. S3_REGION=us-east-1 -# --- Seeded demo accounts --------------------------------------------------- -# Used only by `docker compose run --rm migrate` on first boot. Required in +# Local-FS driver settings (only consulted when STORAGE_DRIVER=local). +# Filesystem root for uploaded objects. Defaults to ./storage relative to +# the API server's working directory. +LOCAL_STORAGE_ROOT=./storage +# HMAC key used to sign + verify upload URLs for the local driver. If +# unset, the driver falls back to SESSION_SECRET. Set explicitly to rotate +# upload-URL trust independently of session trust. +LOCAL_STORAGE_SIGNING_SECRET= + +# ----------------------------------------------------------------------------- +# Seeded demo accounts +# ----------------------------------------------------------------------------- +# Used only by `docker compose run --rm migrate` on first boot. REQUIRED in # production — the seed script throws if these are unset and NODE_ENV=production. SEED_ADMIN_PASSWORD=change-me-admin-password SEED_USER_PASSWORD=change-me-user-password -# --- Email (optional) ------------------------------------------------------- +# ----------------------------------------------------------------------------- +# Email (optional) +# ----------------------------------------------------------------------------- # Leave SMTP_HOST blank to disable outbound mail. The Executive Meetings # notifier no-ops when no SMTP config is present. SMTP_HOST= diff --git a/Dockerfile b/Dockerfile index b910bdd4..986362d7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -78,7 +78,22 @@ HEALTHCHECK --interval=30s --timeout=5s --retries=3 \ CMD wget -qO- http://localhost/healthz >/dev/null || exit 1 # ----------------------------------------------------------------------------- -# Stage 5: one-shot DB migration + seed runner (used by `docker compose run migrate`). +# Stage 5a: mockup-sandbox runtime (dev-only). Carries source + node_modules +# so `pnpm --filter @workspace/mockup-sandbox dev` can run the Vite preview +# server. Not part of the default compose stack — gated behind the `dev` profile. +# ----------------------------------------------------------------------------- +FROM node:${NODE_VERSION}-bookworm-slim AS mockup +WORKDIR /app +RUN corepack enable && corepack prepare pnpm@10.26.1 --activate +COPY --from=build /app /app +ENV NODE_ENV=development \ + PORT=8081 \ + BASE_PATH=/__mockup +EXPOSE 8081 +CMD ["pnpm", "--filter", "@workspace/mockup-sandbox", "run", "dev"] + +# ----------------------------------------------------------------------------- +# Stage 5b: one-shot DB migration + seed runner (used by `docker compose run migrate`). # ----------------------------------------------------------------------------- FROM node:${NODE_VERSION}-bookworm-slim AS migrate WORKDIR /app diff --git a/README.md b/README.md index 20fb175a..673ba208 100644 --- a/README.md +++ b/README.md @@ -51,8 +51,18 @@ docker compose run --rm migrate # one-shot: db push + seed docker compose up -d api web ``` -The SPA is now served at `http://:${WEB_PORT}`. Front it with Caddy / -Nginx / Traefik for TLS and HTTP/2. +After this, the running stack exposes: + +| Service | Default host port | URL | +| ---------------- | ----------------- | ------------------------------ | +| SPA (`web`) | `3000` | `http://:3000/` | +| API (`api`) | `8080` | `http://:8080/api/healthz` | +| MinIO console | not exposed | (proxy to `:9001` if you need it) | +| `mockup-sandbox` | `8081` | dev profile only — `docker compose --profile dev up -d mockup-sandbox` then `http://:8081/__mockup` | + +Front the SPA (port `WEB_PORT`, default `3000`) with Caddy / Nginx / Traefik +for TLS and HTTP/2; bind the API port (`API_PORT`, default `8080`) to +`127.0.0.1` in production so the edge proxy is the sole external entry. ### Default seeded accounts diff --git a/artifacts/api-server/src/lib/objectStorage.ts b/artifacts/api-server/src/lib/objectStorage.ts index eae6f24e..fb627ba9 100644 --- a/artifacts/api-server/src/lib/objectStorage.ts +++ b/artifacts/api-server/src/lib/objectStorage.ts @@ -406,8 +406,8 @@ export const ACTIVE_STORAGE_DRIVER = (): "s3" | "local" => // --------------------------------------------------------------------------- // ObjectStorageService — public API consumed by routes/storage.ts and -// routes/executive-meetings.ts. Surface preserved byte-compatible with the -// previous Replit-sidecar implementation. +// routes/executive-meetings.ts. Surface is driver-agnostic and works +// identically against either the local-FS or S3-compatible backend. // --------------------------------------------------------------------------- export class ObjectStorageService { diff --git a/docker-compose.yml b/docker-compose.yml index 07eca0e7..ba5efc74 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -100,6 +100,11 @@ services: SMTP_PASS: ${SMTP_PASS:-} SMTP_SECURE: ${SMTP_SECURE:-} SMTP_FROM: ${SMTP_FROM:-} + ports: + # API — exposed on host :8080 for direct curl/integration access. In + # a public-facing deployment this should be bound to 127.0.0.1 only + # (e.g. "127.0.0.1:8080:8080") so only the edge proxy reaches it. + - "${API_PORT:-8080}:8080" networks: [internal] healthcheck: test: ["CMD-SHELL", "node -e \"require('http').get('http://localhost:8080/api/healthz', r => process.exit(r.statusCode===200?0:1)).on('error', () => process.exit(1))\""] @@ -117,28 +122,28 @@ services: api: condition: service_healthy ports: - # Production endpoint — the SPA + reverse-proxied /api ride one port, - # which an operator-managed TLS terminator (Caddy / Nginx / Traefik) - # fronts on the public interface. Default 8080 keeps WEB_PORT-driven - # configs stable; override via .env to bind 80/443 directly. - - "${WEB_PORT:-8080}:80" + # SPA — exposed on host :3000. Front this with an operator-managed + # TLS terminator (Caddy / Nginx / Traefik) for production HTTPS. + - "${WEB_PORT:-3000}:80" networks: [internal] # Component preview server. Dev-only — used to iterate on isolated - # React components in an iframe-friendly preview. Not part of the - # production user surface; kept under the `dev` profile so a normal - # `docker compose up -d` does NOT start it. Run with: + # React components in an iframe-friendly preview at /__mockup. Not part + # of the production user surface; kept under the `dev` profile so a + # normal `docker compose up -d` does NOT start it. Run with: # docker compose --profile dev up -d mockup-sandbox mockup-sandbox: build: context: . - target: api - image: tx-os/api:latest + target: mockup + image: tx-os/mockup:latest profiles: ["dev"] - command: ["pnpm", "--filter", "@workspace/mockup-sandbox", "run", "dev"] - working_dir: /app + environment: + NODE_ENV: development + PORT: "8081" + BASE_PATH: /__mockup ports: - - "8081:8081" + - "${MOCKUP_PORT:-8081}:8081" networks: [internal] # One-shot DB migrate + seed. Run on first boot: