17dc287c93
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.
210 lines
12 KiB
Markdown
210 lines
12 KiB
Markdown
# 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.
|