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.rayInside 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:
- Pack runs nothing. It packages what is already built and never runs a build, an install or any code of the application. You build first; there is no
--build. - It is deterministic. The same prepared files and flags give the same bytes, from any directory and at any time.
- It checks before it writes. Pack checks everything before it writes anything, and a refusal leaves no file behind.
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.raybundle 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.pemWhoever 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-signatureA 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 signputs 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:
- A reviewed plan is valid for 30 minutes. A plan that expired, or whose inputs changed in the meantime, is refused, and you plan again.
- A deploy that refuses changes nothing: the previous version stays active, and the answer names the cause.
- A schema change that committed is never reversed. The deploy is finished forward.
- A bundle that changes stores is packed
--againstthe spec the environment runs. The target regenerates the change itself and refuses any difference; a destructive change needs a reviewed allowlist. - The server needs no source tree. The bundle carries everything the application runs, and a bundle deploy reads its configuration from the environment only, never from a
.envfile.
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 includedAn 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-runrayspec 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.
| Switch | What it does | What it does not do |
|---|---|---|
RAYSPEC_MIGRATION_DATABASE_URL, with the shipped role setup | Role 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=true | One 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=managed | A 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_PROXIES | Behind 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:
- Team notes is CRUD and a static UI, with no model credential and no custom code. Its 1.0.0 spec is 38 lines of YAML, comments included. It comes in three releases; the third drops a column and is refused.
- Document intake is a durable workflow over uploaded files. It runs on the deterministic extraction provider, which reads labelled lines and is not for production extraction.
- Asset catalog is custom code with one third-party dependency carried in the bundle and one declared outbound host.
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 --versionWhat this release does not do#
Stated plainly, because each of these is easy to assume:
- The runtime is not a sandbox. Handlers and extensions run inside the serving process and can read what it holds. Contain code you do not trust with a VM or container boundary, its own databases and host egress rules.
- The managed-posture receipt is evidence of tests, not a compliance certificate.
- No egress enforcement in the runtime. An application declares the hosts it calls; the host network policy must enforce it.
- No encryption at rest in the runtime. Disks, the blob volume and backups need their own. Only the migration bundle is encrypted, for the move.
- The managed posture boots only the
openaiagent backend. Theanthropic,codexandpibackends remain available outside it. - A removed member keeps read access until their access token expires, 480 seconds by default. Writes, run starts and administrative actions recheck at once.
- The runtime image is not pushed to a registry. It is a definition in the repository for linux/amd64, built and tested for the release. You build it yourself.
- The release manifest is unsigned, and the npm packages carry no provenance attestation. The 34 npm tarballs equal the tarballs attached to the GitHub release, byte for byte.
- Bundles target linux/x64 and Node 22, and the bundle contract is revision
1.0.0-rc.2. - Export and import move one organization, at most 500,000 objects and 2 GiB, to an empty target.
- A migrated database cannot be downgraded.
- 22 advisories remain open against three packages (undici, brace-expansion, protobufjs) pinned by the Pi agent SDK's own shrinkwrap. They are recorded as scanner exceptions that expire on 2026-10-31.
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 lineThen 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:
- Quickstart: a reference application deployed from its bundle.
- Upgrading to 1.9: every behavior change and what you do about it.
- Changelog: the full entry for 1.9.0.
- Release notes on GitHub: the tarballs, the manifests and the SBOM.
The application is one file now. Read it before you run it.