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. 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. 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. - Stripped Replit references from threat_model.md (now describes the self-hosted topology). - New comprehensive .gitignore: Replit configs (.replit, replit.nix, replit.md), agent state (.local/, .canvas/, .agents/, .cache/, .config/, .upm/), local storage/, .env*, build artefacts. .replit and replit.nix remain on disk (sandbox-protected) but will not ship to GitHub. - 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.md was deleted (per off-Replit scope) so its "do-not-touch files" preference list is moot. .replit/replit.nix kept on disk only because they are sandbox-protected from edit/delete in this environment, but they are git-ignored so they will not appear in a fresh clone.
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
# Tx OS
|
||||
|
||||
A bilingual (Arabic / English) internal "office OS" web platform. Single-tenant,
|
||||
self-hosted, designed to live behind a TLS-terminating reverse proxy on a
|
||||
private VPS or on-prem host.
|
||||
|
||||
The repo is a pnpm monorepo containing:
|
||||
|
||||
| Package | Purpose |
|
||||
| ------------------------------------ | -------------------------------------------------- |
|
||||
| `artifacts/api-server` | Express 5 + Socket.IO + Drizzle ORM API |
|
||||
| `artifacts/tx-os` | React 19 + Vite SPA (the user-facing app) |
|
||||
| `artifacts/mockup-sandbox` | Internal component preview server (dev-only) |
|
||||
| `lib/db` | Drizzle schema + migrations |
|
||||
| `lib/api-zod` + `lib/api-client-react` | OpenAPI-generated Zod schemas + React Query hooks |
|
||||
| `scripts` | DB seed + maintenance scripts |
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
- **Glassmorphism OS UI** with animated gradient backgrounds and an app grid / dock.
|
||||
- **Bilingual** (RTL Arabic + LTR English) with per-user persisted locale.
|
||||
- **Session auth** (`express-session` + Postgres-backed `connect-pg-simple`,
|
||||
bcrypt password hashing) with full RBAC (admin / user roles + role-permission
|
||||
matrix + group-derived permissions).
|
||||
- **Real-time** chat / notifications / executive-meeting alerts via Socket.IO.
|
||||
- **Executive Meetings module** — scheduling, change requests, approvals,
|
||||
optimistic locking on postpones, audit log, and a Playwright-rendered HTML
|
||||
PDF export.
|
||||
- **S3-compatible object storage** with signed-URL uploads (production: MinIO;
|
||||
local dev: built-in filesystem driver — no extra services required).
|
||||
|
||||
---
|
||||
|
||||
## Quick start (Docker)
|
||||
|
||||
The fastest path to a running stack on a Linux VPS with Docker installed.
|
||||
|
||||
```bash
|
||||
git clone <this-repo>
|
||||
cd tx-os
|
||||
cp .env.example .env
|
||||
# Edit .env — at minimum change SESSION_SECRET, POSTGRES_PASSWORD,
|
||||
# S3_SECRET_ACCESS_KEY, SEED_ADMIN_PASSWORD, SEED_USER_PASSWORD.
|
||||
$EDITOR .env
|
||||
|
||||
docker compose build
|
||||
docker compose up -d db minio minio-init
|
||||
docker compose run --rm migrate # one-shot: db push + seed
|
||||
docker compose up -d api web
|
||||
```
|
||||
|
||||
The SPA is now served at `http://<host>:${WEB_PORT}`. Front it with Caddy /
|
||||
Nginx / Traefik for TLS and HTTP/2.
|
||||
|
||||
### Default seeded accounts
|
||||
|
||||
| Username | Password | Role |
|
||||
| -------- | ---------------------------------- | ----- |
|
||||
| `admin` | value of `SEED_ADMIN_PASSWORD` | admin |
|
||||
| `ahmed` | value of `SEED_USER_PASSWORD` | user |
|
||||
|
||||
Both are seeded by `docker compose run --rm migrate` and **only** if they don't
|
||||
already exist (the seed is idempotent on conflict).
|
||||
|
||||
### Common compose commands
|
||||
|
||||
```bash
|
||||
docker compose logs -f api # tail API logs
|
||||
docker compose exec db psql -U tx tx_os # psql shell
|
||||
docker compose run --rm migrate # re-run migrations (safe to repeat)
|
||||
docker compose down # stop everything (data persists)
|
||||
docker compose down -v # stop AND wipe volumes (DANGER)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Local development (without Docker)
|
||||
|
||||
You can run the stack natively if you have Node 24+, pnpm 10, and Postgres 16.
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
createdb tx_os
|
||||
export DATABASE_URL=postgres://localhost/tx_os
|
||||
export PORT=8080 BASE_PATH=/ ALLOWED_ORIGINS=http://localhost:25785
|
||||
export SESSION_SECRET=$(openssl rand -hex 32)
|
||||
export PRIVATE_OBJECT_DIR=/local/private
|
||||
export PUBLIC_OBJECT_SEARCH_PATHS=/local/public
|
||||
# Note: with no S3_ENDPOINT set, the API falls back to the local-FS storage
|
||||
# driver and persists uploads under ./storage/. Safe for development only.
|
||||
|
||||
pnpm --filter db run push # apply schema
|
||||
pnpm --filter scripts run seed # seed admin/ahmed accounts
|
||||
pnpm --filter @workspace/api-server dev
|
||||
# In another terminal:
|
||||
PORT=25785 BASE_PATH=/ pnpm --filter @workspace/tx-os dev
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration reference
|
||||
|
||||
Every option is read from environment variables. See `.env.example` for the
|
||||
complete list with comments. Highlights:
|
||||
|
||||
| Variable | Purpose |
|
||||
| --------------------------------- | ---------------------------------------------------------------------- |
|
||||
| `DATABASE_URL` | Postgres connection string. **Required.** |
|
||||
| `SESSION_SECRET` | HMAC key for session cookies + local-driver upload tokens. **Required in production.** |
|
||||
| `ALLOWED_ORIGINS` | Comma-separated CORS allow-list. Defaults to `*` (dev only). |
|
||||
| `STORAGE_DRIVER` | `s3` or `local`. Defaults to `s3` if `S3_ENDPOINT` set, else `local`. |
|
||||
| `S3_ENDPOINT` etc. | MinIO / S3 connection. Required when `STORAGE_DRIVER=s3`. |
|
||||
| `PRIVATE_OBJECT_DIR` | Path inside the bucket for private uploads, e.g. `/tx-private/private`.|
|
||||
| `PUBLIC_OBJECT_SEARCH_PATHS` | Comma-separated bucket paths searched by `GET /storage/public-objects/*`. |
|
||||
| `LOCAL_STORAGE_ROOT` | Filesystem root used by the local driver. Defaults to `./storage`. |
|
||||
| `SEED_ADMIN_PASSWORD` / `SEED_USER_PASSWORD` | Required by the seed script in production. |
|
||||
| `SMTP_*` | Optional outbound mail config for Executive Meetings notifications. |
|
||||
| `LOG_LEVEL` | pino log level. Defaults to `info`. |
|
||||
|
||||
### Storage drivers
|
||||
|
||||
The API server has two object-storage backends, selected automatically:
|
||||
|
||||
- **`s3`** (production): targets any S3-compatible endpoint (MinIO, AWS S3, R2,
|
||||
Backblaze B2, ...) via `@aws-sdk/client-s3` + presigned PUT URLs.
|
||||
- **`local`** (dev fallback, default when `S3_ENDPOINT` is unset): persists
|
||||
files under `LOCAL_STORAGE_ROOT` and issues HMAC-signed upload URLs that
|
||||
the API server validates and accepts on `PUT /api/storage/_local/upload`.
|
||||
Single-host only; not suitable for production.
|
||||
|
||||
The route layer is unaware of which driver is active — both expose the same
|
||||
`StoredObject` interface in `lib/objectStorage.ts`.
|
||||
|
||||
---
|
||||
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
pnpm --filter @workspace/api-server build
|
||||
pnpm --filter @workspace/api-server start & # boot API on 8080
|
||||
pnpm --filter @workspace/api-server test # node --test suite
|
||||
pnpm --filter @workspace/tx-os test:e2e # Playwright UI tests
|
||||
```
|
||||
|
||||
The `test` workflow chains all of the above. Tests use the same database
|
||||
specified in `DATABASE_URL` and clean up after themselves with `LIKE`-prefixed
|
||||
fixture rows.
|
||||
|
||||
---
|
||||
|
||||
## Production checklist
|
||||
|
||||
Before fronting Tx OS with a public domain:
|
||||
|
||||
1. **Set every secret in `.env`.** No defaults in production.
|
||||
2. **Run behind TLS.** Caddy / Nginx / Traefik should terminate HTTPS and
|
||||
forward to `web` on `${WEB_PORT}`. The API server trusts `X-Forwarded-For`
|
||||
from the first proxy hop (`app.set("trust proxy", 1)`).
|
||||
3. **Restrict the API port.** The `api` service should never be exposed
|
||||
directly to the internet — only `web` is intended to be reachable.
|
||||
4. **Back up the volumes.** `db_data` and `minio_data` are the only stateful
|
||||
surfaces. Snapshot them on a schedule.
|
||||
5. **Rotate seeded passwords.** The first thing an admin should do is change
|
||||
the seeded `admin` and `ahmed` passwords from the user-management screen.
|
||||
6. **Review `threat_model.md`.** Document-level threat model lives in the repo
|
||||
root and lists the trust boundaries this deployment relies on.
|
||||
|
||||
---
|
||||
|
||||
## Project conventions
|
||||
|
||||
- **Package manager**: pnpm 10. The repo refuses to install with npm/yarn.
|
||||
- **Node**: 24 LTS. Older Nodes will not run the API bundle.
|
||||
- **TypeScript**: 5.9, strict mode, project-references build (`pnpm typecheck`).
|
||||
- **API contracts**: hand-written Zod schemas in `lib/api-zod` are the source
|
||||
of truth; the React-Query client in `lib/api-client-react` is generated
|
||||
from the same OpenAPI spec via Orval.
|
||||
- **Migrations**: Drizzle Kit. `pnpm --filter db run push-force` for dev,
|
||||
`pnpm --filter db run push` for production.
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
MIT.
|
||||
Reference in New Issue
Block a user