riyadhafraa f6ff70cd2d feat(setup): Stage 1 first-time setup wizard backend (no UI)
Task #534 — backend, infra, tooling. UI ships in Stage 2 (#535).

Backend
- New system_settings table (id=1 singleton): installed flag, base_url,
  local_domain, local_ip, https_mode, app_version. Pushed to dev DB.
- New /api/setup/status (open) and /api/setup/{validate,complete}
  (gated by requireSetupOpen — 409 once installed).
- completeInstall is fully transactional: pg_advisory_xact_lock
  serializes concurrent callers, double-gates on installed flag and
  admin existence, then atomically creates the admin user, assigns
  admin role + Admins/Everyone groups, and flips system_settings to
  installed=true. Rolls back on any failure.
- Added redirectIfSetupNeeded() helper returning the full SetupStatus
  payload alongside a redirect target for SPA routing decisions.
- Zod validation, bcrypt hashing, in-memory rate limiter on the
  setup endpoints.

Backward compat
- scripts/src/seed.ts now branches on installed flag + admin
  existence + SEED_*_PASSWORD env vars. Legacy installs (admin
  exists, system_settings empty) get backfilled to installed=true
  via ON CONFLICT DO UPDATE so they are never forced through the
  wizard. When env passwords are unset and no admin exists, the
  seed prints a wizard hint instead of seeding.

Infra
- docker-compose.yml: replaced nginx edge with a Caddy service that
  mounts ./certs and ./docker/Caddyfile{,.skip}. The web service no
  longer publishes a port directly — Caddy is the only public ingress.
- Caddy entrypoint picks Caddyfile.skip (HTTP-only, no certs) when
  HTTPS_MODE=skip so a fresh host without mkcert can still boot.
- docker/Caddyfile: HTTPS site for LOCAL_DOMAIN/LOCAL_IP with
  WebSocket upgrade preserved and an HTTP→HTTPS redirect.
- start.sh: preserved. Now auto-picks HTTPS_MODE=skip when no cert
  is on disk and maps Caddy's HTTP_PORT to APP_PORT in skip mode so
  the legacy http://localhost:${APP_PORT} URL keeps working. In
  local/byo mode it prints the https://${LOCAL_DOMAIN} URL.
- .env.example: added LOCAL_DOMAIN, LOCAL_IP, BASE_URL, HTTP_PORT,
  HTTPS_PORT, HTTPS_MODE; SEED_*_PASSWORD now optional.

Tooling
- scripts/local-setup.sh: idempotent OS-aware bootstrap (.env upsert,
  mkcert hint, cert SAN check, dry-run via LOCAL_SETUP_DRY_RUN).

Tests
- artifacts/api-server/tests/setup-wizard.test.mjs: 7/7 pass.
- scripts/tests/local-setup.test.mjs: 2/2 pass.

Constraints honored: no force-push, no destructive ops, start.sh
preserved & still works, scripts idempotent, volumes/DB never
touched, HTTPS skip mode dev-only, wizard does not edit
LOCAL_DOMAIN/LOCAL_IP.

Out of scope / not addressed: pre-existing TS errors in
routes/users.ts and pre-existing failure in
executive-meetings-postpone-race.test.mjs.
2026-05-14 08:08:02 +00:00

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: 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.

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.

S
Description
No description provided
Readme 114 MiB
Languages
TypeScript 68.9%
JavaScript 29.4%
CSS 0.6%
Shell 0.6%
Dockerfile 0.2%
Other 0.2%