riyadhafraa dd68923ba3 Allow relative URLs for upload destinations in API responses
Update OpenAPI spec and generated schemas to permit relative paths for upload URLs, resolving validation errors with the local storage driver.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 563d7ca2-281f-4d1e-bada-a5bbc9d9f061
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/IO8TMSC
Replit-Helium-Checkpoint-Created: true
2026-05-20 09:30:21 +00:00
2026-04-18 02:00:09 +00:00
2026-04-18 02:00:09 +00:00
2026-04-18 02:00:09 +00:00

Tx OS

A bilingual (Arabic / English) internal "office OS" web platform. Single-tenant, self-hosted, designed to run on any Linux server (cloud or on-prem) behind a TLS-terminating reverse proxy.

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

Two ready-made install scripts cover the common cases — pick the one that matches your machine. Both are idempotent (safe to re-run) and never delete existing data.

For any Linux server with Docker + Docker Compose. Boots the full stack (Postgres, MinIO, API, SPA, Caddy edge) with a single command:

git clone <this-repo>
cd tx-os
./start.sh

start.sh will:

  1. Copy .env.docker.example.env on first run (and pause so you can edit secrets — at minimum SESSION_SECRET, POSTGRES_PASSWORD).
  2. Build the images, bring up the database + object storage, run migrations, then start the API and web containers behind Caddy.
  3. Print the URLs you can reach the system on.

After install, open the printed URL and the first-time setup wizard will let you create the admin account in the browser. (You can also pre-seed an admin by setting SEED_ADMIN_PASSWORD in .env before running.)

Option B — Local development with HTTPS (mkcert)

For developers running the stack on their own laptop with trusted local certificates (so the SPA can talk to a real https://tx.local):

git clone <this-repo>
cd tx-os
./scripts/local-setup.sh

local-setup.sh will:

  1. Detect your OS / package manager and tell you how to install mkcert if it's missing.
  2. Prompt for a local domain (default tx.local) and detect your LAN IP (so phones / tablets on the same Wi-Fi can reach the system too).
  3. Generate a trusted local TLS certificate covering localhost, the chosen domain, and the LAN IP.
  4. Bootstrap .env from .env.example (preserving any keys you've already set), then bring the Docker stack up.

Manual Docker install (advanced)

If you'd rather drive docker compose yourself:

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 are
# OPTIONAL — leave them blank to use the in-browser setup wizard.
$EDITOR .env

docker compose build
docker compose up -d db minio minio-init
docker compose run --rm migrate          # one-shot: pnpm run migrate (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.

Reverse proxy / Tailscale / external URL

If you front Tx OS with a TLS-terminating proxy that you don't control (Tailscale serve, ngrok, Cloudflare Tunnel, …), two things matter:

  1. Add the public URL to ALLOWED_ORIGINS in .env. If you skip this, the browser blocks every API call from the SPA with a CORS error and the app appears frozen on the login screen.

    ALLOWED_ORIGINS=http://localhost:3000,https://it-demo.tail70b2bc.ts.net
    PUBLIC_BASE_URL=https://it-demo.tail70b2bc.ts.net
    
  2. Set TRUST_PROXY_HTTPS=true in .env. Tunnels like Tailscale serve forward HTTPS traffic to the upstream as plain HTTP without sending an X-Forwarded-Proto: https header. Without this flag, req.secure is false, the session cookie is silently dropped, and the user is bounced back to the login screen on every refresh.

    Security note: only enable TRUST_PROXY_HTTPS=true when the API container/port is not reachable directly over plain HTTP from outside (i.e. only the TLS edge proxy can hit it). The default Docker compose only exposes the SPA port (3000) to the host and keeps the API on the internal tx-net network, which satisfies this requirement. If you change the compose file to expose the API port publicly, do not enable this flag.

After editing .env, restart the stack:

docker compose up -d

You should be able to log in via the public HTTPS URL with no other changes.

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. MUST list every URL the SPA is reached at (LAN IP, Tailscale name, custom domain, ...).
TRUST_PROXY_HTTPS true when fronted by a TLS-terminating proxy that doesn't forward X-Forwarded-Proto (Tailscale serve, ngrok free, some Cloudflare Tunnels). Required for the session cookie to persist on those setups.
SEED_DEMO_MEETINGS true to populate Executive Meetings with a day of demo data on first boot. Default off so real installs start empty.
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 test                                          # full suite, auto-boots API
pnpm --filter @workspace/api-server test           # api-server tests only
pnpm --filter @workspace/tx-os test:e2e            # Playwright UI tests

Both pnpm test and pnpm --filter @workspace/api-server test go through artifacts/api-server/scripts/with-server.mjs, which builds the API server, spawns it on PORT (default 8080), waits for /api/healthz, runs the wrapped command with TEST_API_BASE exported, and tears the server down on exit (including Ctrl+C and failures). If startup fails the helper prints the tail of the server's stderr/stdout so the root cause is visible.

To run the tests against an already-running API server (e.g. the dev workflow), set TEST_API_BASE before invoking the script — the helper will skip spawning and use that URL instead:

TEST_API_BASE=http://localhost:8080 pnpm --filter @workspace/api-server test

Tests use the same database specified in DATABASE_URL and clean up after themselves with LIKE-prefixed fixture rows. The test:wait script remains available for anyone who only wants the bare health-check wait behaviour.


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.

S
Description
No description provided
Readme 138 MiB
Languages
TypeScript 69.2%
JavaScript 29.2%
CSS 0.6%
Shell 0.6%
Dockerfile 0.2%
Other 0.2%