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

### Option A — One-shot Docker install (recommended)

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

```bash
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`):

```bash
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`](https://github.com/FiloSottile/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:

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

   ```bash
   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:

```bash
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

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

```bash
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:

```bash
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.
