Files
TX/README.md
T
Riyadh 3727ce8bf0 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 round 1).
- 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 (6 targets: deps/build/api/web/mockup/migrate). API stage
  uses the official Playwright base image so PDF rendering works
  in-container; web stage is nginx serving the Vite SPA bundle; mockup
  stage carries source + node_modules so the dev preview server runs
  with PORT=8081 BASE_PATH=/__mockup.
- docker-compose.yml: postgres + minio + minio-init (creates buckets) +
  api (host :8080) + web (host :3000) + one-shot migrate runner; the
  mockup-sandbox service is gated behind a `dev` profile (host :8081
  /__mockup) so a normal `docker compose up -d` does NOT start it.
  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 consumed by app/objectStorage/
  auth/seed/compose paths is documented with comments — host ports,
  PUBLIC_BASE_URL, ALLOWED_ORIGINS, SESSION_SECRET, BASE_PATH,
  DATABASE_URL, STORAGE_DRIVER, PRIVATE_OBJECT_DIR,
  PUBLIC_OBJECT_SEARCH_PATHS, S3_*, LOCAL_STORAGE_ROOT,
  LOCAL_STORAGE_SIGNING_SECRET, SEED_*, SMTP_*.
- README.md replaces replit.md as the canonical project doc; covers
  Docker quickstart with a service/port table, 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), the storage-object-authz test comment, and the
  objectStorage.ts header comment. Source/config/docs are now
- 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 MIGRATION_REPORT.md documents the one-line `git rm
--cached` an operator can run on a non-Replit clone to purge them from
upstream git history. replit.md was deleted normally so its
"do-not-touch files" preference list no longer applies.
2026-05-13 14:17:12 +00:00

8.6 KiB

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.

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

After this, the running stack exposes:

Service Default host port URL
SPA (web) 3000 http://<host>:3000/
API (api) 8080 http://<host>:8080/api/healthz
MinIO console not exposed (proxy to :9001 if you need it)
mockup-sandbox 8081 dev profile only — docker compose --profile dev up -d mockup-sandbox then http://<host>:8081/__mockup

Front the SPA (port WEB_PORT, default 3000) with Caddy / Nginx / Traefik for TLS and HTTP/2; bind the API port (API_PORT, default 8080) to 127.0.0.1 in production so the edge proxy is the sole external entry.

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

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.

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

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.