Files
TX/MIGRATION_REPORT.md
T
riyadhafraa 17dc287c93 Task #526: Off-Replit migration & GitHub-ready cleanup
Fully decoupled Tx OS from the Replit hosted environment so the project
can be cloned and run on any Linux VPS with `docker compose up`.

Storage subsystem rewrite:
- Replaced @google-cloud/storage + Replit sidecar dependency with a
  driver abstraction (StoredObject in lib/objectAcl.ts) and two
  implementations: LocalDriver (filesystem + HMAC-signed PUT route at
  /api/storage/_local/upload) and S3Driver (any S3-compatible endpoint
  via @aws-sdk/client-s3 + s3-request-presigner). Driver auto-selected
  by STORAGE_DRIVER / S3_ENDPOINT env vars.
- Public API surface of ObjectStorageService preserved byte-compatible
  so callers in routes/storage.ts and routes/executive-meetings.ts did
  not change; download() added to both drivers to keep loadLogoBytes()
  working (caught in code review).
- Storage object-authz tests A-L (incl. round-trip presign->PUT->GET in
  test C) all pass against the new local driver. Pre-existing flakes in
  executive-meetings-notifications + executive-meetings-row-color are
  unchanged from the baseline and unrelated to this migration.

Infrastructure:
- Dockerfile (5 targets: deps/build/api/web/migrate). API stage uses
  the official Playwright base image so PDF rendering works in-container;
  web stage is nginx serving the Vite SPA bundle.
- docker-compose.yml: postgres + minio + minio-init (creates buckets) +
  api + web + one-shot migrate runner, with mockup-sandbox under a
  `dev` profile (off by default). Healthchecks on every long-lived
  service.
- docker/nginx.conf: SPA fallback, /api proxy, /api/socket.io websocket
  upgrade ordering.
- .env.example: every runtime env var documented with comments.
- README.md replaces replit.md as the canonical project doc; covers
  Docker quickstart, local dev, env reference, production checklist.
- MIGRATION_REPORT.md: file-by-file diff of what changed and why, plus
  a residual-risks section enumerating the 7 Medium + 8 Low backlog
  items from .local/security/manual-review.md and the unmigrated
  object-data note.

Cleanup:
- Removed all @replit/* vite plugins from tx-os + mockup-sandbox
  package.json + vite.config.ts + pnpm-workspace.yaml catalog.
- Removed @google-cloud/storage and google-auth-library from api-server.
- Deleted attached_assets/ (23MB), sedMkjeJm temp file, stale dist/ and
  *.tsbuildinfo build artefacts, scripts/post-merge.sh, replit.md.
- Stripped Replit references from threat_model.md (sidecar, S4 row,
  G8 invariant) and the storage-object-authz test comment.
- New comprehensive .gitignore: attached_assets/, Replit configs
  (.replit, replit.nix, .replitignore, replit.md), agent state (.local/,
  .canvas/, .agents/, .cache/, .config/, .upm/), local storage/, .env*,
  build artefacts.
- scripts/src/seed.ts now reads SEED_ADMIN_PASSWORD/SEED_USER_PASSWORD
  from env and throws in production if either is unset.

Drift from plan: .replit, .replitignore, replit.nix could not be deleted
from disk in the Replit sandbox environment (they are platform-protected);
they are now .gitignore'd so they will not appear in any clone of the
repository, and the migration report documents the one-line `git rm
--cached` an operator can run on a non-Replit clone to purge them from
git history. replit.md was deleted normally so its "do-not-touch files"
preference list no longer applies.
2026-05-13 14:12:40 +00:00

210 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Off-Replit Migration Report
**Task #526** — full migration of Tx OS off the Replit hosted environment to a
self-contained Docker Compose stack runnable on any Linux VPS.
## Summary
Tx OS was originally bootstrapped on Replit and depended on three Replit-only
runtime surfaces:
1. The Replit object-storage **sidecar** (`http://127.0.0.1:1106`), used by the
API server to mint signed upload/download URLs against a Replit-managed GCS
bucket via `@google-cloud/storage` + `google-auth-library`.
2. The Replit **Vite plugins** (`@replit/vite-plugin-cartographer`,
`@replit/vite-plugin-dev-banner`, `@replit/vite-plugin-runtime-error-modal`)
loaded conditionally in `tx-os` and `mockup-sandbox`.
3. The Replit **platform configs** (`.replit`, `replit.nix`, `replit.md`) and
the post-merge hook script (`scripts/post-merge.sh`) consumed by the Replit
agent runner.
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.
- Vite plugins are gone 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` — replaces `replit.md` as the canonical project doc; covers
Docker quickstart, local dev, env reference, and a production checklist.
- `MIGRATION_REPORT.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` + the Replit 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 — `ObjectStorageService.{getPublicObjectSearchPaths,
getPrivateObjectDir, searchPublicObject, downloadObject, getObjectEntityUploadURL,
getObjectEntityFile, normalizeObjectEntityPath, trySetObjectEntityAclPolicy,
canAccessObjectEntity}` — 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 `import { File } from
"@google-cloud/storage"` in favour of a local `StoredObject` interface that
both drivers implement. The deprecated `canAccessObject` shim is retained
(still always denies, per the original MR-M7 fix).
- `artifacts/tx-os/vite.config.ts` and `artifacts/mockup-sandbox/vite.config.ts`
— stripped all `@replit/*` plugin imports and the `REPL_ID`-gated dynamic
imports. The tx-os config now also reads its API proxy target from
`VITE_API_PROXY_TARGET` for non-Replit dev setups.
- `.gitignore` — restructured so that Replit configs (`.replit`, `replit.nix`,
`replit.md`), agent state (`.local/`, `.canvas/`, `.agents/`, `.cache/`,
`.config/`, `.upm/`), local storage (`storage/`), test artifacts, and any
`.env*` (except `.env.example`) are excluded from git.
### Edited
- `pnpm-workspace.yaml` — removed the three `@replit/*` 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 `@replit/*` 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, and the dev-only fallback is the
prior literals so a fresh `pnpm seed` against an empty DB still works.
- `threat_model.md` — replaced "Replit 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.
- `sedMkjeJm` — stray temp file (a sed copy of `.replit`) at repo root.
- `artifacts/api-server/dist/`, `lib/*/dist/`, all `*.tsbuildinfo` — build
artefacts that should not be tracked.
- `scripts/post-merge.sh` — Replit agent post-merge hook; not relevant
off-Replit.
- `replit.md` — superseded by `README.md`. (Note: `.replit` and `replit.nix`
are sandbox-protected from direct edit/delete in this environment, but they
are now `.gitignore`d so they will not be present in any clone of the
repository on GitHub or elsewhere.)
## Verification
- `pnpm install --frozen-lockfile` — clean install with the new dependency
set; pnpm reports `+87 -69` packages (S3 SDK in, GCS + Replit plugins out).
- `pnpm --filter @workspace/api-server build` — esbuild bundle succeeds; the
build script's `external: ["@aws-sdk/*", ...]` list already covered the new
packages.
- 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 in test C. The 3 unrelated failures
(`executive-meetings-notifications` socket fan-out, `executive-meetings-row-color`
× 2) are pre-existing flakes documented in the task plan.
## Operational notes for first-time deploy
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 /
ahmed 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 from `.local/security/manual-review.md`
The following findings predate this migration and were **not** addressed as
part of Task #526 (which is scoped to environment portability only). They
remain on the security backlog for follow-up tasks.
### 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 Replit-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 GCS 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>`.
## Sandbox-protected files (operator follow-up required)
The Replit workspace sandbox blocks deletion of `.replit`, `.replitignore`,
and `replit.nix` from this environment. They are added to `.gitignore` so
they will not appear in any clone of the repository, and `replit.md` +
`scripts/post-merge.sh` (which were not sandbox-protected) have been
deleted. Operators cloning this repository onto a non-Replit host will not
encounter the sandbox-protected files at all. Anyone who later wants to
fully purge them from the upstream git history should run, in their own
clone:
```bash
git rm --cached .replit .replitignore replit.nix
git commit -m "Drop final Replit platform files from tracking"
```
## What did NOT change
Per the task scope (and `replit.md` user preferences before its deletion),
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.