2026-10-05by RaySpec9 minapplication bundle

RaySpec 1.9.0: the application is a file you can hand over

RaySpec 1.9.0 packs a built application into one .ray bundle. You can read it without running it, deploy it against a plan you reviewed, move a whole deployment as one encrypted file, and host it in a stricter posture. Here is what that covers, and what it does not.

Until now you deployed a RaySpec application from a source tree: a spec, and next to it the code and static files the spec points at. That is fine while the person who wrote the application is also the person who runs it. It gets awkward the moment those are two people.

RaySpec 1.9.0 makes the built application a file. One .ray bundle that can be checked without running it, deployed with a plan you review first, moved to another host encrypted, and hosted in a stricter posture. The release notes put it in five verbs: package, check, deploy, move and host. This post takes them in that order and ends with what the release does not do.

If you only upgrade and set nothing new, your deployment keeps working as it did. Everything below is a command you choose to run or a setting you turn on.

The bundle#

rayspec pack turns an application that is already built into one file. With team notes, the reference application of the quickstart:

npx rayspec pack --spec team-notes/rayspec.yaml --output team-notes-1.0.0.ray

Inside are the spec, the compiled code and static files it needs at run time, the third-party packages that code imports, a dependency lock, an SBOM (CycloneDX 1.5) and the license notices. Never inside: a secret value, a database row, a cloud account. Secrets reach an application as bindings at deploy time, and pack refuses a file that carries a PEM private-key header.

Three properties are worth knowing before you rely on it:

A bundle holds at most 9,999 files, a path of at most 4,096 bytes, and 512 MiB. Bundles target linux/x64 and Node 22. Packing an application covers what goes in, what never does, and how to fix each refusal.

Reading before running#

A file someone hands you should be readable before anything in it executes. Two commands do that:

npx rayspec bundle inspect team-notes-1.0.0.ray
npx rayspec bundle verify team-notes-1.0.0.ray

bundle inspect is passive: nothing in the archive is extracted, imported, evaluated or run, and nothing is written anywhere. It reports what the bundle is: the application and its version, the runtime it pins, the bindings it needs, the outbound hosts it declares. bundle verify checks everything inspect checks, then the runtime, the target, the capabilities and the spec, scans for secrets, and checks the signature. Its verdict is deployable or not-deployable.

Read deployable narrowly. It means every check passed for this runtime. It never vouches for the code the bundle carries.

Signing#

A publisher can sign a bundle with their own key. The signature is Ed25519 over the bundle's SHA-256, in a small detached file next to the bundle; the bundle itself is not changed. From the packing guide, where the bundle is called notes-1.4.0.ray:

openssl genpkey -algorithm ed25519 -out publisher.pem
chmod 600 publisher.pem
openssl pkey -in publisher.pem -pubout -out publisher.pub.pem
rayspec bundle sign notes-1.4.0.ray --key-file publisher.pem

Whoever deploys it checks with the public key and refuses an unsigned bundle:

rayspec bundle verify notes-1.4.0.ray --trusted-key publisher.pub.pem --require-signature

A verified signature shows that the archive is exactly the one the holder of the private key signed, and nothing more. It does not show that the code is safe.

# note

bundle sign puts your key on your application. It is not a signature on RaySpec itself: the release manifest of 1.9.0 is unsigned, and the npm packages carry no provenance attestation. More under the limits below.

Deploy against a plan you reviewed#

A bundle deploy takes two steps, on purpose:

npx rayspec deploy team-notes-1.0.0.ray --dry-run > plan.json
npx rayspec deploy team-notes-1.0.0.ray --plan-digest "$(node -p 'require("./plan.json").data.planDigest')"

The dry run plans the deploy against the live database and changes nothing: no SQL changes anything and nothing from the bundle runs. The plan says which bindings the bundle needs, what happens to the schema, which outbound hosts and capabilities change, and what blocks it. It ends in a digest. The second command applies exactly that plan, extracts the bundle into a read-only version directory and serves it, on loopback and port 8080 unless you say otherwise.

What holds around those two lines:

Deploying a bundle on your own server walks one bundle from inspection to a served, updated and recovered deployment.

Moving a deployment#

rayspec export writes a deployment's complete state as one migration bundle: the deployed application, its application database, its workflow system database and every stored blob, encrypted with age to one recipient. The key pair is made on the machine that will import, not on the source:

age-keygen -o migration-identity.txt   # keep this file private (mode 0600)
age-keygen -y migration-identity.txt   # prints the recipient: age1...

Only the recipient goes to the source. There is no passphrase mode.

rayspec export --deployment 3f9c0a1b2c3d4e5f \
  --recipient age1… \
  --output /srv/handover/app-migration.ray \
  --run-history included

An export is offline for writes. Reads keep answering, mutations and uploads are refused, and the source stays fenced after a successful export. On the other side, check first and restore second:

rayspec import /srv/handover/app-migration.ray \
  --target /srv/app/.rayspec-state \
  --identity-file migration-identity.txt \
  --dry-run

rayspec import checks everything before it restores anything. It restores into a new, empty target, never merges into a database that holds anything, verifies the result against the snapshot, and leaves the target fenced. The target serves only after you release it with a cutover token that works once, within 15 minutes.

A move resets credentials, by design. The new environment mints its own boot secrets, so sessions, API keys and invites are gone. Users, memberships and password hashes carry over: members sign in again with the passwords they had. An owner who holds no password gets a one-time way back in with rayspec tenant recover-owner.

Keep the scope in mind. An export is not a backup tool, not a replication stream and not a way to merge two deployments. It moves one organization, at most 500,000 objects and a bundle of at most 2 GiB, to an empty target. Keep the source fenced for the recovery window, seven days by default; it is not a way back once the target has accepted a write. The guides are Exporting a deployment and Importing a deployment.

A stricter posture, if you turn it on#

The default has not moved: the core is built for trusted, self-hosted, single-node deployment. 1.9.0 adds a hardened posture for a runtime that people you do not fully trust can reach. It is four switches. Each is off by default and each can be turned on alone.

SwitchWhat it doesWhat it does not do
RAYSPEC_MIGRATION_DATABASE_URL, with the shipped role setupRole separation and row-level security. A supervisor alone holds the migration connection; a child process serves the application as a runtime role that cannot bypass the tenant policies.It does not stop code inside the serving process from setting another tenant's id itself.
RAYSPEC_SINGLE_TENANT=trueOne organization per runtime. A second one is refused on every path.It picks and hides nothing: a database that already holds more than one organization refuses the boot.
RAYSPEC_HOSTING_POSTURE=managedA default for every execution bound, only the openai agent backend, declared rights for every handler, no agent trace export unless you ask for it.It refuses the anthropic, codex and pi backends rather than containing them, and it enforces no egress.
RAYSPEC_TRUSTED_PROXIESBehind a reverse proxy, names the proxy addresses whose forwarding headers are believed.It does not terminate TLS or cap request bodies. The proxy does.

A release's managed-posture receipt names which protections its certification lane tested. It is evidence of tests, not a compliance certificate. Start with Hosting in the hardened posture and Database roles and row-level security; the threat model says what the host around the runtime has to enforce itself.

Three applications and a quickstart#

Three reference applications ship under examples/, each packed and deployed from its bundle by the repository's own tests:

The quickstart goes from npm install to a deployed application in about ten commands, with the published CLI and no build of RaySpec itself. It needs Node >=22.21.0 and PostgreSQL 16, and it starts like this:

npm install rayspec
npx rayspec --version

What this release does not do#

Stated plainly, because each of these is easy to assume:

The threat model lists every residual risk this release accepts.

Upgrading from 1.8#

An existing deployment needs nothing new. Check Node first:

node --version   # v22.21.0 or later on the 22 line

Then back up both databases, install 1.9.0 and start the deployment the way you started it before. The first boot runs the platform migrations 0012 to 0017; each one is additive. Take the backup seriously: a 1.8.x runtime does not detect a database that 1.9.0 has migrated, so going back is unsupported and must start from that backup. If a script treats exit code 2 as the only failure, change it: an unexpected internal error of the CLI now exits 7. The upgrade itself is checked with data before every release, from 1.7.0 and from 1.8.0.

Where to go from here:

The application is one file now. Read it before you run it.