7396c4c1af
Refactor test execution to use a new script that manages the API server lifecycle, including building, starting, waiting for health, running tests, and shutting down, with support for using an existing server.
303 lines
13 KiB
Markdown
303 lines
13 KiB
Markdown
# 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.
|