Docs Sign in Start building

Install Hyperscale in your bank

A self-hosted install runs the same release Hyperscale runs: Arc, the boundary and webhook workers, the portals, Postgres and TigerBeetle on one Linux machine you own. It sends mail through your SMTP server, answers on your hostnames, keeps backups where you say, and needs nothing from Cloudflare or from hyperscale0.ai once it runs. Each release arrives as a signed payload, and the installer refuses one your trusted release key did not sign.

This page is for the ops team that installs and runs it. The Enterprise page covers what people do once it runs: Hyperscale Enterprise. Your developers reach it through the same HTTP API and MCP server as the hosted Hyperscale, on your hostnames.

Prerequisites

  • One machine, aarch64. The machine is the one infra/vm/deployment.yaml describes: Ubuntu 26.04 on aarch64 (Arm64). Hyperscale runs it under Lima on Apple silicon; its bank lab runs on 8 CPUs, 8 GiB of memory and a 60 GiB disk. The provision scripts are plain shell for Ubuntu 26.04.
  • x86_64 does not boot today. The provision scripts fetch the aarch64 builds of Bun and TigerBeetle, and no release has been installed on x86_64. Plan on aarch64 hardware or an Arm cloud instance.
  • Pinned platform packages. Postgres 18 from Ubuntu's repositories, Bun 1.4.4 and TigerBeetle 0.17.9, installed by the provision scripts. Every release is built against them.
  • OpenSSH's ssh-keygen, which Ubuntu ships, for ssh-keygen -Y verify.
  • An SMTP server that accepts mail from the machine, with implicit TLS, STARTTLS or, on loopback only, no TLS.
  • Hostnames for the API and the console, and the TLS in front of them. Hyperscale serves plain HTTP on the machine; your load balancer or reverse proxy terminates TLS and runs on the same machine, since the portals listen on loopback. It must append the client's address to X-Forwarded-For (nginx $proxy_add_x_forwarded_for): Arc keys its sign-in and rate budgets on that address, and they are the abuse control left when Turnstile is off.
  • A backup target: a directory (ideally on another disk or a mount) or any S3-compatible endpoint, such as MinIO, Ceph or a cloud bucket.

What Arc calls at your bank

Arc reaches your bank through ADL adapters, one per provider. A bank adapter implements the kinds your products use, by these names:

Kind What Arc asks for
account.provision Open an account under a master, or a collection master; answer the reference and IBAN
account.lookup Read one account back
account.post Mirror a Hyperscale move between two of the bank's accounts
account.balance The booked and available balance now, and the day's opening balance
account.statement One finished day's bookings
account.intraday The current day's bookings after a cursor
credit.observe Credits and debits booked on a master's accounts after a cursor
payout Send a payout to its payee from a named account

KYB is a separate provider (organization-verification.ts), not a bank kind. There is no close-account kind and no KYC kind.

The config contract

Everything an install needs is one env file: <install root>/secrets/deployment-secrets.env, mode 0600, owned by the install's admin user. The install root is /srv/hyperscale unless the machine's INSTALL_ROOT parameter says otherwise. Arc refuses to boot, and the installer refuses to install, on a file that breaks the contract, and each refusal names the setting.

Settings:

Setting Value
HYPERSCALE_DEPLOYMENT_PROFILE self-hosted
HYPERSCALE_PUBLIC_BASE_URL The API origin, such as https://hyperscale.bank.example. Required. Discovery, OAuth and mail links name it
HYPERSCALE_CONSOLE_URL The console origin
HYPERSCALE_CORS_ORIGINS Comma-separated browser origins your own front end calls the API from
HYPERSCALE_EMAIL_ADAPTER smtp
HYPERSCALE_EMAIL_FROM, HYPERSCALE_EMAIL_REPLY_TO The sender address (required) and the reply-to address
HYPERSCALE_SMTP_HOST, HYPERSCALE_SMTP_PORT Your SMTP server
HYPERSCALE_SMTP_TLS implicit, starttls, or none for a loopback relay
HYPERSCALE_SMTP_USER, HYPERSCALE_SMTP_PASSWORD Set both or neither
HYPERSCALE_TURNSTILE off: the sign-in forms render no challenge and Arc skips it. The rate budgets stay
HYPERSCALE_EGRESS_PRIVATE_ALLOW Private CIDRs your bank's endpoints live in (see egress below)
HYPERSCALE_BACKUP_TARGET An absolute directory, or s3
HYPERSCALE_BACKUP_S3_ENDPOINT, _BUCKET, _REGION, _ACCESS_KEY_ID, _SECRET_ACCESS_KEY The S3-compatible target, when the target is s3
HYPERSCALE_BACKUP_AGE_RECIPIENT The age public key every backup is encrypted to

Secrets, each generated by you and never reused from another install: HYPERSCALE_SYSTEM_KEY_SANDBOX, HYPERSCALE_SYSTEM_KEY_LIVE, HYPERSCALE_DATA_ENCRYPTION_KEYS and the three database passwords (HYPERSCALE_DB_BOOTSTRAP_PASSWORD, HYPERSCALE_DB_MIGRATION_PASSWORD, HYPERSCALE_DB_RUNTIME_PASSWORD). Leave HYPERSCALE_INSTANCE_SIGNING_KEY out: the installer mints it into the env file on the first install and keeps it on every update after. HYPERSCALE_BACKUP_AGE_IDENTITY is optional on the machine: a backup needs only the recipient, and you may keep the identity offline until a restore.

.env.example in the release lists every name with a comment.

Install

  1. Provision the machine with the provision scripts in infra/vm/deployment.yaml. They install the pinned packages and lay out the tree: app, ops and secrets under the install root.

  2. Trust the release key. Copy infra/release-keys/allowed_signers and revoked_keys from a release you obtained out of band into <install root>/release-keys/. Check the key's fingerprint against the one Hyperscale gives you in writing.

  3. Write the env file at <install root>/secrets/deployment-secrets.env.

  4. Unpack the payload into <install root>/incoming and check its signature before anything in it runs:

    ssh-keygen -Y verify -f /srv/hyperscale/release-keys/allowed_signers \
      -I [email protected] -n hyperscale-release \
      -s /srv/hyperscale/incoming/release-manifest.json.sig \
      -r /srv/hyperscale/release-keys/revoked_keys \
      < /srv/hyperscale/incoming/release-manifest.json
    
  5. Run the installer with the release identity Hyperscale publishes:

    bun --no-env-file /srv/hyperscale/incoming/infra/vm/guest-install.js \
      --commit <commit> --tree <tree> --manifest-digest <sha256> --fresh
    

The installer checks the signature again, then every file's digest against the signed manifest, and refuses an unknown principal, a revoked key or a changed file. It creates one system user per service (hyperscale-postgres, hyperscale-tigerbeetle, hyperscale-arc, hyperscale-boundary, hyperscale-webhooks, hyperscale-portals, hyperscale-mock-bank, hyperscale-backup, hyperscale-tunnel), renders the systemd units for the install root, initializes Postgres and TigerBeetle, applies the migrations, starts every unit, waits for each one to answer, and writes the readiness record. Each service reads only its own data; systemd hands each one its environment as root.

When the env file carries no HYPERSCALE_INSTANCE_SIGNING_KEY, the installer generates one (ed25519) and writes it into the file. Copy the file off the machine after the first install: the key is the instance's identity.

Upgrade

Unpack the new payload into incoming, stage the env file you want it to run on as deployment-secrets.env.incoming, and run the installer without --fresh. When the staged file has no instance key, the installer carries the installed one across, so the instance keeps one key for its life. It stops the code units, swaps the application and the env file, runs the migrations and starts the units. When the new release never comes up, it puts the previous application and env file back and starts them.

After a release installs, its infra/release-keys become the trusted ones. That is how a new release key or a revocation reaches you: inside a release the current key signed.

Backup and restore

backup.timer runs backup.service every night at 00:30 UTC. The backup pauses the writers for under a second, dumps Postgres and copies the TigerBeetle data file, encrypts both to HYPERSCALE_BACKUP_AGE_RECIPIENT, and writes a dated set to the target. Sets older than 30 days are deleted after each upload. Run one now with sudo systemctl start backup.service.

Each set's manifest carries an anchor: the fold of every audit chain's head at the moment of the backup, signed with the instance key. A restore drill recomputes the fold from the restored database and checks the signature, so a set restored from the wrong instance, or edited after it was taken, fails the drill.

To drill a set on any machine with Postgres 18:

bun infra/backup/drill.ts --target local <directory holding the sets> \
  --secrets <env file with the age identity and the instance key>

The drill restores Postgres into a scratch cluster, checks every table's row count against the manifest and verifies the anchor. It proves a set restores; it does not restore the machine. Recovering an install from a set is a manual job: install the same release with --fresh, stop the units, decrypt the set with the age identity, pg_restore the dump, copy the TigerBeetle data file into place, and start the units. Point-in-time recovery is not covered.

The keys your bank holds

  • The release public key, in <install root>/release-keys/allowed_signers. It decides which payloads install. Hyperscale holds the private half.
  • The instance key, HYPERSCALE_INSTANCE_SIGNING_KEY. It signs the backup anchor, and Assure certificates from r442. Its public half and fingerprint are served in /release.json, so anyone can check a signature against the install that made it.
  • The age keys. The recipient encrypts backups; the identity decrypts them. Keep the identity off the machine and somewhere you can reach during an outage.

Hyperscale never holds the instance key or the age identity.

Egress

Arc makes these outbound connections, and no others:

  • your SMTP server;
  • each provider endpoint an ADL adapter names, your bank's included;
  • Vertex AI, only when VERTEX_API_KEY is set (Architect authoring).

Provider calls refuse private, loopback and link-local addresses by default. When your bank's endpoints sit on a private network, list their ranges in HYPERSCALE_EGRESS_PRIVATE_ALLOW (for example 10.20.0.0/16,192.168.40.0/24). Loopback and link-local stay refused whatever the list says. Webhook delivery to your customers follows the same public-only rule.

Provisioning the machine downloads the pinned packages once (Ubuntu's mirrors, the Postgres apt repository, the GitHub releases of Bun and TigerBeetle, and cloudflared from pkg.cloudflare.com, which a self-hosted install leaves stopped and never configures). After that the machine needs no internet access beyond the list above.

What ops reads

Each portal answers GET /release.json:

{
  "portal": "console",
  "release": "r441",
  "version": "1.0.441",
  "releaseCommit": "<40 hex>",
  "status": "ok",
  "turnstile": { "mode": "off" },
  "instancePublicKey": "<base64>",
  "instanceKeyFingerprint": "<sha256 hex>"
}

Arc answers GET /v1/health with the same releaseCommit.

The installer writes <install root>/ops/readiness.json after every install: the release commit, the digest of the contract and of the migrations it applied, and one probe per unit (postgres, tigerbeetle, arc, boundary, webhooks, portals) with when it answered and how long it took. A record that names another commit, or misses a unit, means the last install did not finish.

Logs are in the journal: journalctl -u arc.service, and the same for each unit.