Update notification permission logic to differentiate between denied and dismissed states, and adjust locale messages accordingly. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 02bc349c-c9f1-47f4-967c-cfefeabde8bb Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/1WKAcHk Replit-Helium-Checkpoint-Created: true
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-backedconnect-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:
git clone <this-repo>
cd tx-os
./start.sh
start.sh will:
- Copy
.env.docker.example→.envon first run (and pause so you can edit secrets — at minimumSESSION_SECRET,POSTGRES_PASSWORD). - Build the images, bring up the database + object storage, run migrations, then start the API and web containers behind Caddy.
- 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:
- Detect your OS / package manager and tell you how to install
mkcertif it's missing. - 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). - Generate a trusted local TLS certificate covering
localhost, the chosen domain, and the LAN IP. - Bootstrap
.envfrom.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:
-
Add the public URL to
ALLOWED_ORIGINSin.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 -
Set
TRUST_PROXY_HTTPS=truein.env. Tunnels like Tailscale serve forward HTTPS traffic to the upstream as plain HTTP without sending anX-Forwarded-Proto: httpsheader. Without this flag,req.secureis 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=truewhen 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 internaltx-netnetwork, 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 whenS3_ENDPOINTis unset): persists files underLOCAL_STORAGE_ROOTand issues HMAC-signed upload URLs that the API server validates and accepts onPUT /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:
- Set every secret in
.env. No defaults in production. - Run behind TLS. Caddy / Nginx / Traefik should terminate HTTPS and
forward to
webon${WEB_PORT}. The API server trustsX-Forwarded-Forfrom the first proxy hop (app.set("trust proxy", 1)). - Restrict the API port. The
apiservice should never be exposed directly to the internet — onlywebis intended to be reachable. - Back up the volumes.
db_dataandminio_dataare the only stateful surfaces. Snapshot them on a schedule. - Rotate seeded passwords. The first thing an admin should do is change
the seeded
adminandahmedpasswords from the user-management screen. - 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-zodare the source of truth; the React-Query client inlib/api-client-reactis generated from the same OpenAPI spec via Orval. - Migrations: Drizzle Kit.
pnpm --filter db run push-forcefor dev,pnpm --filter db run pushfor production.
License
MIT.