Files
TX/README.md
T

289 lines
12 KiB
Markdown
Raw Normal View History

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