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:
- Validates every environment variable (rejects placeholder/example values in production — the node will refuse to boot rather than run insecurely).
- Runs database migrations.
- 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. - Loads and decrypts the vault into memory.
- Seeds your initial admin account from
INITIAL_ADMIN_USERNAME/INITIAL_ADMIN_PASSWORD, if no admin exists yet. - Runs crash recovery (a no-op on a genuinely fresh node — see
incidents.mdfor what this does after a restart). - Starts the background engine (sweep, autoscale, webhook delivery crons).
- Binds the HTTP server and flips
/health/readyto 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:
- Log in with your username and password.
- 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. - Enter a real code from your authenticator app to confirm enrollment. Until
this confirmation succeeds, your account stays in the
mustEnrolTotpstate. - 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.mdfor 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.