Files
TX/DEPLOYMENT_MIGRATION.md
T
Riyadh 19e7f49520 Document Docker rebuild after git pull (Task #549)
Root cause of "service images don't show on the Mac after pulling latest":
the SPA is baked into the nginx (`web`) image and the API binary is baked
into the `api` image at `docker compose build` time. Running just
`git pull && docker compose up -d` reuses the existing images, so new
static assets in `artifacts/tx-os/public/` (and any front-end / API code
changes) never reach the browser.

Changes:
- New `scripts/redeploy.sh` — single-command redeploy. Steps:
  1. git pull --ff-only (skip with --no-pull)
  2. docker compose build api web
  3. docker compose run --rm migrate  (drizzle push + idempotent seed)
  4. docker compose up -d
  Detects `docker compose` v2 vs legacy `docker-compose`, runs from any
  cwd via BASH_SOURCE, set -euo pipefail, --help prints the header.
  chmod +x. Verified `bash -n` and `--help`.
- `replit.md` — new "Redeploying after `git pull` (Docker hosts)"
  section that calls out the gotcha and points at the script.
- `DEPLOYMENT_MIGRATION.md` — one-line cross-reference at the top of
  the deploy section so anyone reading deploy docs finds the script.

No code or runtime behavior changed. No follow-ups proposed: the only
natural next step (PWA full-screen) is already tracked as Task #550.
2026-05-16 08:49:12 +00:00

9.8 KiB

Deployment Migration Report

Migration of Tx OS from a hosted single-tenant runtime to a self-contained Docker Compose stack runnable on any Linux host (or macOS workstation with Docker Desktop).

Summary

Tx OS previously depended on three runtime surfaces tied to its original hosting environment:

  1. A managed object-storage sidecar (http://127.0.0.1:1106) used by the API server to mint signed upload/download URLs against a managed GCS bucket via @google-cloud/storage + google-auth-library.
  2. Three Vite plugins (@*/vite-plugin-cartographer, @*/vite-plugin-dev-banner, @*/vite-plugin-runtime-error-modal) loaded conditionally in the SPA and the component sandbox.
  3. Hosted-platform configuration files consumed by the previous runtime.

After this migration:

  • Object storage is backed by a self-hosted MinIO container in production and a built-in local-filesystem driver in development. Both are abstracted behind a StoredObject interface in lib/objectStorage.ts so the route layer is driver-agnostic.
  • Hosted-platform Vite plugins are removed from both vite.config.ts files and from pnpm-workspace.yaml's catalog.
  • A Dockerfile (5 build targets — deps / build / api / web / migrate) and a docker-compose.yml (db + minio + minio-init + api + web + one-shot migrate) bring the entire stack up with docker compose up -d. A new .env.example template documents every env var the runtime reads.

The repository is now self-contained: cloning it onto a Docker-equipped host and running the documented commands produces a working deployment.

Files changed

Added

  • Dockerfile — multi-stage build (Node 24 deps → esbuild API + Vite SPA builds → Playwright runtime for API → nginx runtime for SPA → migrate runner).
  • docker-compose.yml — full production stack with healthchecks and one-shot init/migrate containers.
  • docker/nginx.conf — SPA static serve + /api and /api/socket.io reverse proxy to the api container.
  • .env.example — exhaustive, commented configuration template.
  • README.md — canonical project doc; covers Docker quickstart, local dev, env reference, and a production checklist.
  • DEPLOYMENT_MIGRATION.md — this file.
  • artifacts/api-server/src/routes/storage-local-upload.ts — handler for the local-FS driver's HMAC-signed PUT upload URLs (PUT /api/storage/_local/upload).

Replaced (full rewrite)

  • artifacts/api-server/src/lib/objectStorage.ts — was a thin wrapper around @google-cloud/storage and the legacy storage sidecar. Now ships a driver interface with two implementations (LocalDriver, S3Driver) selected by the STORAGE_DRIVER env var (defaulting to s3 if S3_ENDPOINT is set, else local). Public API surface is preserved byte-compatible so callers in routes/storage.ts and routes/executive-meetings.ts did not need to change.
  • artifacts/api-server/src/lib/objectAcl.ts — dropped the @google-cloud/storage File import in favour of a local StoredObject interface that both drivers implement. The deprecated canAccessObject shim is retained (still always denies, per the original safe default).
  • artifacts/tx-os/vite.config.ts and artifacts/mockup-sandbox/vite.config.ts — stripped all hosted-platform plugin imports and gated dynamic imports. The tx-os config now reads its API proxy target from VITE_API_PROXY_TARGET for arbitrary dev setups.
  • .gitignore — restructured so that hosted-platform configs, agent state, local storage, test artifacts, and any .env* (except .env.example) are excluded from git.

Edited

  • pnpm-workspace.yaml — removed the three hosted-platform catalog entries and emptied minimumReleaseAgeExclude.
  • artifacts/api-server/package.json — dropped @google-cloud/storage and google-auth-library; added @aws-sdk/client-s3 and @aws-sdk/s3-request-presigner.
  • artifacts/tx-os/package.json and artifacts/mockup-sandbox/package.json — removed hosted-platform plugin dependencies.
  • scripts/src/seed.ts — admin/user passwords now read from SEED_ADMIN_PASSWORD / SEED_USER_PASSWORD; the script throws in NODE_ENV=production if either is unset.
  • threat_model.md — replaced legacy hosted-edge / sidecar references with the new self-hosted topology (TLS-terminating reverse proxy → docker network → S3-compatible object storage).

Deleted

  • attached_assets/ — 23 MB of legacy uploaded assets unused by the codebase.
  • artifacts/api-server/dist/, lib/*/dist/, all *.tsbuildinfo — build artefacts that should not be tracked.
  • Obsolete post-merge hook script consumed by the previous runtime.

Verification

  • pnpm install --frozen-lockfile — clean install with the new dependency set; pnpm reports +87 -69 packages (S3 SDK in, GCS plugins out).
  • pnpm --filter @workspace/api-server build — esbuild bundle succeeds.
  • API-server test suite — all 12 storage / object-authz tests (storage-object-authz.test.mjs cases A through L) pass against the new local-FS driver, including the round-trip presigned PUT → DB-backed authz check → streamed GET.

Operational notes for first-time deploy

Updating an existing deploy after git pull? Use ./scripts/redeploy.sh — it rebuilds the api and web images, runs migrations + seed, and restarts the stack in the right order. See replit.md → "Redeploying after git pull".

  1. cp .env.example .env and fill in every value.
  2. docker compose build
  3. docker compose up -d db minio minio-init — Postgres + MinIO + create buckets.
  4. docker compose run --rm migrate — apply Drizzle schema + seed admin / user accounts (one-shot).
  5. docker compose up -d api web — boot the API and the SPA.
  6. Front web (port ${WEB_PORT}) with a TLS-terminating reverse proxy.

The README's "Production checklist" lists the security-relevant steps required before fronting the stack with a public domain.

Residual security risks (carried over)

The following findings predate this migration and were not addressed as part of this scope (which is environment portability only). They remain on the security backlog.

Medium (7)

ID Summary
MR-M1 No CSRF protection; session cookie is sameSite: "lax".
MR-M2 No session regeneration on login (session-fixation window).
MR-M3 Helmet / security headers are not set on API responses.
MR-M4 GET /api/users/directory returns the full employee directory to every authenticated user.
MR-M5 POST /api/apps/:id/open accepts any app id from any user (audit-log pollution + open enumeration).
MR-M6 Errors caught in executive-meetings routes echo raw Error.message to the client.
MR-M8 Upload contentType is taken from the client and never validated server-side.

Note: MR-M7 ("Object Storage ACL subsystem is unimplemented") is now documented in-source by the new objectAcl.ts deprecation banner and the entity-lookup authz model in lib/objectAuthz.ts; no behaviour change is required for the migration to ship.

Low (8)

ID Summary
MR-L1 executive_meetings_changed and executive_meeting_notifications_changed events are broadcast globally.
MR-L2 Bcrypt cost factor is 10 (industry guidance is 12).
MR-L3 Account enumeration on register (distinguishable error for existing username).
MR-L4 No socket connection cap or handshake rate limit.
MR-L5 GET /api/settings is unauthenticated.
MR-L6 Several notes.ts mutation routes use ad-hoc body parsing instead of Zod.
MR-L7 express.json() uses the default 100 KB body limit.
MR-L8 GET /executive-meetings/me returns the caller's full role list.

Unmigrated object-data risk

Object data stored in the legacy managed GCS bucket prior to this migration is not automatically copied into MinIO. Operators standing up a fresh MinIO instance start with empty buckets. If historical uploads must be preserved, the operator is responsible for a one-time mc mirror (or equivalent) from the legacy bucket into the new MinIO bucket; the API server's signed-URL contract and DB-side attached_object_path columns are already compatible because both backends key by <bucket>/<objectName>.

What did NOT change

Per the migration scope, the following surfaces were intentionally left untouched:

  • All API and SPA test files (artifacts/api-server/tests/**, artifacts/tx-os/tests/**).
  • lib/db/scripts/** (DB push/pull scripts).
  • The PDF renderers (pdf-renderer.ts, pdf-html-renderer.ts).
  • The SPA shell (App.tsx, executive-meetings.tsx, upcoming-meeting-alert.tsx).
  • Locale catalogues (ar.json, en.json).
  • The auto-generated React-Query client (custom-fetch.ts).
  • The Executive Meetings DB schema (schema/executive-meetings.ts).
  • Authentication, sessions, rate limiting, and CSRF/CORS middleware — only the storage subsystem was rewritten.