Docs

Standalone Node

Build

git clone <your fork> 3x && cd 3x
npm ci
npm run pack:staged -w @3x-engine/sdk   # builds the SDK + the npm tarball served at /v1/sdk/
npm run build -w @3x/api          # → apps/api/dist/server.mjs (+ dist/migrations, dist/sdk)
npm run build -w @3x/dashboard    # → apps/dashboard/.next

Run the API

export X3_ENV=production
export X3_DATABASE_URL=postgres://x3:…@db.internal:5432/x3
export X3_PUBLIC_API_URL=https://api.3x.example.com
export X3_DASHBOARD_URL=https://app.3x.example.com
export X3_SECRET=… X3_ENCRYPTION_KEY=… CRON_SECRET=… X3_DASHBOARD_KEY=…
export X3_SCHEDULER=1            # in-process scheduler, one tick per minute
export X3_TRUSTED_IP_HEADERS=x-real-ip   # the header YOUR proxy sets – see below
export PORT=4310

npm run migrate -w @3x/api       # or X3_AUTO_MIGRATE=1
node apps/api/dist/server.mjs

With X3_ENV=production the server refuses to start while required settings are missing or malformed and names them (never their values). Any Postgres ≥ 15 works. Run exactly one process with X3_SCHEDULER=1 per deployment if you want predictable load – several are safe (jobs are claimed with leases), they just compete.

A systemd unit:

[Service]
EnvironmentFile=/etc/3x/api.env
WorkingDirectory=/opt/3x
ExecStart=/usr/bin/node apps/api/dist/server.mjs
Restart=always
User=x3

Run the dashboard

X3_API_URL=http://127.0.0.1:4310 X3_DASHBOARD_KEY=… npm run start -w @3x/dashboard   # port 4311

Client IPs behind a reverse proxy

3x needs the visitor's IP for bot budgets and the daily session hash (it never stores it). Without X3_TRUSTED_IP_HEADERS the socket address is used – correct when Node faces the internet directly, wrong behind a proxy (every visitor would share the proxy's IP).

SetupX3_TRUSTED_IP_HEADERS
Node directly on the internetunset
nginx with proxy_set_header X-Real-IP $remote_addr;x-real-ip
Caddy (header_up X-Real-IP {remote_host})x-real-ip
Cloudflare in front (proxied DNS)cf-connecting-ip
Vercelunset (auto x-real-ip,x-forwarded-for)

Never use x-forwarded-for outside Vercel unless your proxy overwrites it: clients can send their own first entry. Country comes from X3_COUNTRY_HEADERS (default x-vercel-ip-country,cf-ipcountry); behind nginx with GeoIP set a header such as x-country and list it there.

nginx example:

server {
  server_name api.3x.example.com;
  location / {
    proxy_pass http://127.0.0.1:4310;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_http_version 1.1;
    proxy_buffering off;          # MCP streams responses
  }
}

Health

GET /v1/health → {ok, db, version, time}; use it for load-balancer checks. The dashboard reaches the API over the internal URL in X3_API_URL; it forwards the visitor's IP only together with X3_DASHBOARD_KEY.