3c8bfbf84d
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.
185 lines
9.8 KiB
Markdown
185 lines
9.8 KiB
Markdown
# 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.
|