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.yamldescribes: 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, forssh-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
Provision the machine with the provision scripts in
infra/vm/deployment.yaml. They install the pinned packages and lay out the tree:app,opsandsecretsunder the install root.Trust the release key. Copy
infra/release-keys/allowed_signersandrevoked_keysfrom 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.Write the env file at
<install root>/secrets/deployment-secrets.env.Unpack the payload into
<install root>/incomingand 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.jsonRun 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_KEYis 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.