Stable
Sourced from: docs/08-PLATFORM-SPEC.md §7, docs/02-NODE-ARCHITECTURE.md (boot sequence, §6 config), docs/10-SECURITY-SPEC.md §3.1, docs/DECISIONS.md D0.2 D5.1 D5.8 D6.1

Deploying your treasury node

Your treasury node is the piece of ZuPayments you run. It holds every private key your business ever uses, serves checkout to your customers, and talks to the SplitChain gateway directly. ZuPayments (the hosted platform) never sees a key — not at deploy time, not ever (D0.2). This page walks through getting a node live and completing the mandatory first-login security setup.

1. Launch from the Railway template

From the ZuPayments dashboard, click Create Treasury Node. This deep-links to Railway's template-deploy flow for the published apps/node package (app + Postgres):

https://railway.com/deploy/<template-code>?referralCode=zupayments

You'll be asked to sign in to (or create) your own Railway account — the node runs on your infrastructure, under your custody root of trust. ZuPayments never has console access to your Railway project.

2. What gets provisioned

The template declares two services — the node app and a Postgres database — and a set of environment variables. The security-critical ones are generated by Railway itself at deploy time, so ZuPayments never sees them:

Env var Source Purpose
VAULT_MASTER_KEY Railway generator, 64 random bytes Decrypts your private-key vault at boot
SESSION_SECRET Railway generator, 64 random bytes Signs admin session cookies
DATABASE_URL Railway Postgres plugin, auto-wired Node's database connection
NODE_PUBLIC_URL Railway-provided service domain Base URL for checkout, push receiver, discovery
PRICE_API_URL Pre-filled default Zucoin fiat price feed

A handful of other variables control pool sizing, treasury wallet count, session TTL, and optional features (reporting sync, origin relay) — all have sane defaults and can be tuned later from Railway's variable editor without a redeploy in most cases. See the full table in the reference appendix at the bottom of this page.

INITIAL_ADMIN_USERNAME and INITIAL_ADMIN_PASSWORD seed your first admin account on first boot only — set these in the Railway variable editor before the first deploy if you want a specific username, otherwise use whatever the template pre-fills and change the password after first login.

3. First boot

On first boot the node, in strict fail-hard order:

  1. Validates every environment variable (rejects placeholder/example values in production — the node will refuse to boot rather than run insecurely).
  2. Runs database migrations.
  3. Provisions the vault — generates one treasury wallet keypair and your pool-floor of sub-wallet keypairs (5 by default), encrypts them (AES-256-GCM, PBKDF2-SHA256 600k iterations) under VAULT_MASTER_KEY, and writes the single encrypted vault row. This is the only point at which fresh keys are minted without your explicit action.
  4. Loads and decrypts the vault into memory.
  5. Seeds your initial admin account from INITIAL_ADMIN_USERNAME / INITIAL_ADMIN_PASSWORD, if no admin exists yet.
  6. Runs crash recovery (a no-op on a genuinely fresh node — see incidents.md for what this does after a restart).
  7. Starts the background engine (sweep, autoscale, webhook delivery crons).
  8. Binds the HTTP server and flips /health/ready to 200.

Railway's healthcheck polls /health/ready; until boot finishes you'll see the deploy as "starting" — this is expected and normally takes well under a minute.

4. First admin login and mandatory TOTP enrollment

Once the node is live, open https://<your-node-domain>/admin and log in with the seeded admin username/password.

First login is password-only. The response tells the admin UI mustEnrolTotp: true — you are not allowed to reach any other admin functionality (including money-moving actions) until you enroll a TOTP authenticator. There is no way to skip this step and no email-based recovery if you lose the factor later, so:

  1. Log in with your username and password.
  2. The UI walks you into TOTP enrollment: the node mints a secret and shows it once as a QR code (otpauth:// URI) — scan it into an authenticator app (1Password, Authy, Google Authenticator, etc.) immediately. This is the only time the secret is ever shown or leaves the node.
  3. Enter a real code from your authenticator app to confirm enrollment. Until this confirmation succeeds, your account stays in the mustEnrolTotp state.
  4. Once confirmed, every subsequent login requires username + password + a fresh TOTP code, and every money-moving action (sweep retry, backup export/import, key rotation, settings changes) additionally requires a fresh, single-use TOTP code at the moment of the action — a code you used to log in cannot be reused to authorize a mutation.

Change the seeded admin password from the account settings screen as your first post-enrollment action if you didn't set a custom one via INITIAL_ADMIN_PASSWORD.

5. After first login

Once TOTP is enrolled and confirmed, you're on the admin dashboard. From here:

  • Mint your API keys (site key for your storefront, and optionally a reporting key / action key if you plan to connect the ZuPayments hosted dashboard) — see key-rotation.md.
  • Confirm your pool and treasury settings match your expected volume.
  • If you're migrating an existing funded Zucoins wallet in as your treasury, see backup-restore.md for the import flow.
  • Bookmark incidents.md — it covers what the node does automatically after a crash or restart, and when something needs your attention versus resolves itself.

Reference: full environment variable table

Env var Required Default Purpose
DATABASE_URL yes Postgres connection (Railway-wired)
VAULT_MASTER_KEY yes Vault decryption key (≥32 bytes, rejected if placeholder)
SESSION_SECRET yes Admin session signing (≥64 chars)
INITIAL_ADMIN_USERNAME / INITIAL_ADMIN_PASSWORD first boot only Seeds first admin account
NODE_PUBLIC_URL yes (prod) Base URL for checkout, push receiver, discovery
SPLITCHAIN_GATEWAY_URLS no production gateway Comma-separated failover list
ZUCOINS_PUSH_API_BASE no wallet.zucoins.com push API Push-notification delivery
ZUCOIN_TOKEN_TYPE_ID no Zucoin constant Should not normally be changed
PRICE_API_URL no Zutopia price feed Fiat price display
POOL_FLOOR no 5 Minimum sub-wallets always kept available
POOL_CAP no 50 (max 500) Autoscale ceiling
POOL_HEADROOM_PCT no 10 Autoscale headroom target
TREASURY_WALLET_COUNT no 1 (max 10) Number of treasury wallets / sweep lanes
SESSION_TTL_SECONDS no 300 Checkout session expiry
WEBHOOK_SECRET yes if using webhooks HMAC signing key for merchant webhooks
MERCHANT_API_KEYS bootstrap only Seeds initial API keys; managed via admin after
REPORTING_ENABLED no false Opt-in reporting sync to the hosted dashboard
REPORTING_KEY / PLATFORM_REPORTING_URL if reporting on Reporting sync auth + target
VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY yes Web Push identity for payment detection
PORT / BIND_HOST no 3000 / :: HTTP bind
LOG_LEVEL no Log verbosity
SENTRY_DSN no Optional error reporting

None of these ever transit ZuPayments' servers — they live only in your Railway project.