Quickstart: a reference application from its bundle
This page takes you from npm install to a deployed application in about ten commands, with the published CLI and no build of RaySpec itself. The application is team notes, a small CRUD application with a web UI: you build it, pack it into a .ray bundle, check the bundle, and deploy it from the bundle alone.
To build RaySpec from source and author your own spec instead, follow Getting started. To run a bundle someone else packed, on a server, see Deploying a bundle on your own server.
Every command in the numbered steps runs in the repository's reference journeys (scripts/journeys/quickstart.mjs), so the page and the CLI cannot drift apart. Two of them reach the network there in a local form: the install installs the release's packed tarballs, and the clone clones a local repository holding the application, tagged at the packed version.
What you need#
- Node
>=22.21.0, withnpmandnpx. PostgreSQL 16 you can reach: an empty database for the deployment, and a server where a throwaway database may be created and dropped while a change is planned (the same server is fine). The commands below expect a user
rayspecwith the passwordrayspecon port5433, with the databasesrayspecandrayspec_shadow. With Docker, this starts one:docker run -d --name rayspec-pg -p 5433:5432 -e POSTGRES_USER=rayspec \ -e POSTGRES_PASSWORD=rayspec -e POSTGRES_DB=rayspec postgres:16 # once it accepts connections (a few seconds): docker exec rayspec-pg createdb -U rayspec rayspec_shadowIn a clone of the repository,
pnpm db:upstarts the same with Docker Compose.gitandcurl.
Run everything below in a new, empty directory.
1. Install the CLI#
npm install rayspec
npx rayspec --version2. Get the application and build it#
The application's source is in the repository, at the tag of the release you installed. Its build needs Node only: it copies the spec and the UI and writes the version the UI shows.
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-notes3. Pack and check the bundle#
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.rayinspect reads the bundle without running anything in it: the application team-notes at version 1.0.0, the RaySpec runtime it pins, and that it needs no binding and declares no outbound host. verify checks every file against the bundle's own digests. See Packing an application.
4. Point at the database and mint the boot secrets#
A bundle deploy reads its configuration from the environment only, never from a .env file.
export DATABASE_URL=postgresql://rayspec:rayspec@localhost:5433/rayspec
export SHADOW_DATABASE_URL=postgresql://rayspec:rayspec@localhost:5433/rayspec_shadow
node -e 'process.stdout.write(require("crypto").generateKeyPairSync("rsa", { modulusLength: 2048 }).privateKey.export({ type: "pkcs8", format: "pem" }).trimEnd())' > jwt.pem
node -e 'process.stdout.write(require("crypto").randomBytes(32).toString("base64"))' > pepper
chmod 600 jwt.pem pepper
export RAYSPEC_JWT_SIGNING_KEY_FILE=$PWD/jwt.pem
export RAYSPEC_API_KEY_PEPPER_FILE=$PWD/pepperjwt.pem signs the access tokens and pepper keys the API-key hashes. Both files are written without a trailing newline, exactly as the deploy reads them. Keep both: a deployment started with other values does not accept the tokens and keys issued under these.
5. Deploy#
The dry run plans the deploy and changes nothing; the deploy applies the plan you reviewed, named by its digest, and serves on port 8080 (set PORT to use another). Leave it running.
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')"6. Use it#
In a second terminal, in the same directory: open http://127.0.0.1:8080/ in a browser, or call the API. A user registers, creates the organization, writes a note and reads it back.
BASE=http://127.0.0.1:8080
field() { node -p "JSON.parse(require('fs').readFileSync(0, 'utf8')).$1"; }
curl -s $BASE/app-version.json
ACCESS=$(curl -s -X POST $BASE/v1/auth/register -H 'content-type: application/json' \
-d '{"email":"you@example.test","password":"a-long-enough-password"}' | field accessToken)
ORG=$(curl -s -X POST $BASE/v1/orgs -H "authorization: Bearer $ACCESS" \
-H 'content-type: application/json' -d '{"name":"My team"}' | field id)
TOKEN=$(curl -s -X POST $BASE/v1/orgs/$ORG/switch -H "authorization: Bearer $ACCESS" | field accessToken)
curl -s -X POST $BASE/api/notes -H "authorization: Bearer $TOKEN" \
-H 'content-type: application/json' -d '{"title":"First note","content":"Hello from the bundle"}'
curl -s "$BASE/api/notes?limit=10" -H "authorization: Bearer $TOKEN"app-version.json names the application version, 1.0.0 — the application's own, not the runtime's. The last call lists the note you wrote.
Stop the deployment with Ctrl-C; its state stays in .rayspec-state and in the database. To serve it again, run the deploy without a plan digest — npx rayspec deploy team-notes-1.0.0.ray — with the same environment: a reviewed plan is valid for 30 minutes, and a restart of the bundle that already runs needs none. See Deploying a bundle on your own server → Update.
Next#
- Update the application to
1.1.0, which adds a field, and see why2.0.0is refused: team notes. - The other reference applications: document intake, a durable workflow over uploaded files, and asset catalog, custom code with a dependency and a declared outbound host.
- Move the deployment to another environment: Exporting and Importing a deployment.