Developers
Trigger a real-browser preflight from CI, gate the deploy on the decision, and pull structured evidence back. The decision is explainable, the evidence is deterministic, and every artifact stays private behind a short-lived signed URL.
queue a run against the preview build, then stop the release when a critical flow fails
The CI gate
When your preview build goes live, submit the claim, approve the plan Vraelis derives from it, then run that approved plan and ship only when the decision is Verified. Failed and Blocked stop the release; a run that merely finished is not a pass. Approval is a separate step on purpose, so a first submit prepares a plan rather than spending on one nobody read.
# .github/workflows/verify.yml
name: Vraelis Verification
on: [deployment_status]
jobs:
verify:
if: github.event.deployment_status.state == 'success'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: node scripts/verify-gate.mjs
env:
VRAELIS_API_KEY: ${{ secrets.VRAELIS_API_KEY }}
VRAELIS_CLAIM: ${{ vars.VRAELIS_CLAIM }}
PREVIEW_URL: ${{ github.event.deployment_status.target_url }}
# The plan a person approved for this claim. Until it is set, the job stops at step 1.
VRAELIS_REVIEWED_PLAN_ID: ${{ vars.VRAELIS_REVIEWED_PLAN_ID }}// scripts/verify-gate.mjs -- ship only when the deployed build keeps its promise.
// 0 verified 1 failed 2 blocked 3 no decision reached 4 the plan still needs a person to approve it
import { randomUUID } from "node:crypto";
const API = "https://vraelis.com/api/v1/verifications";
const headers = {
"content-type": "application/json",
"x-api-key": process.env.VRAELIS_API_KEY,
"idempotency-key": randomUUID(),
};
const request = { deployment_url: process.env.PREVIEW_URL, claim: process.env.VRAELIS_CLAIM };
const post = (body) => fetch(API, { method: "POST", headers, body: JSON.stringify(body) });
// 1. Submit the claim. With no reviewed_plan_id this answers 202 "review_required": Vraelis derived a plan,
// nothing ran, nothing was charged, and there is NO verification_id yet, only a reviewed plan to approve.
const planId = process.env.VRAELIS_REVIEWED_PLAN_ID;
if (!planId) {
const prepared = await post(request);
if (!prepared.ok) { console.error("Vraelis request failed: " + prepared.status); process.exit(3); }
const plan = await prepared.json();
console.error("A person has to approve this plan before it can run: " + plan.reviewed_plan_id);
(plan.requirements || []).forEach((r) => console.error(" " + r));
process.exit(4); // nothing ran, so this is not a verdict and must not be read as one
}
// 2. Approval happens outside this script: the Review queue in the console, or
// POST /v1/verifications/plans/{id}/approve. Holding the id is not approval.
// 3. Resubmit the SAME deployment and claim with the approved plan. This runs exactly what was reviewed,
// and it is the first response that carries a verification_id.
const started = await post({ ...request, reviewed_plan_id: planId });
if (!started.ok) { console.error("Vraelis refused the run: " + started.status); process.exit(3); }
const { verification_id } = await started.json(); // this response also carries human_reviewed: true
// 4. Poll until a DECISION lands. While the run is going the body is just the id and the state.
let decision = null, out;
for (let i = 0; i < 120 && decision === null; i++) {
await new Promise((r) => setTimeout(r, 5000));
const res = await fetch(API + "/" + verification_id, { headers });
if (!res.ok) { console.error("Vraelis request failed: " + res.status); process.exit(3); }
out = await res.json();
decision = out.decision ?? null; // "verified" | "failed" | "blocked", absent while still running
}
// 5. Gate on the DECISION, never the run state. A finished run is not a pass; only "verified" ships.
switch (decision) {
case "verified": console.log("Verified"); process.exit(0);
case "failed": console.error("Failed: the claim did not hold"); (out.failures || []).forEach((f) => console.error(" " + f.title)); process.exit(1);
case "blocked": console.error("Blocked: no verdict was reached"); process.exit(2);
default: console.error("No decision within the polling window"); process.exit(3);
}The request and response shapes above are copied from the shipped route, POST /api/v1/verifications. Running the whole path from a CI runner is what is still opening, which is why the notice above stands.
The verification, as data
Every run returns the same shape, whether you read it in the app, over the API, or in CI. It is built for a machine to act on, not a human to interpret.
Private by construction
Screenshots live in a private bucket. There is no public URL. A request for an artifact is owner-checked against the run, then answered with a short-lived signed URL that expires in minutes. The rest of the evidence, the step record and the console and network activity, is read through the same owner-checked report. API keys are server-side secrets, shown once and stored only as a hash.
You choose what a run can reach. Point it at a preview or staging deployment and keep Stripe in test mode. Vraelis drives your app from the outside, so a run can touch whatever that environment touches.
Deterministic and explainable
The pass or fail of every flow is what the browser actually did, not a model's opinion. The decision follows one rule you can read: any critical flow that fails means the verification is Failed. When Vraelis suggests a cause, it is labeled as interpretation and never counts as the gate.
See the decision rule →Where we are
The run and report data plane is real: a Postgres-backed queue, a worker that drives an isolated browser, and private, owner-scoped evidence. API-key triggers and CI gating are opening in early access, and the deeper connections for GitHub, Vercel, Supabase, and Stripe build out from there.
We will not document an endpoint we have not shipped. When a surface is live, it appears in the signed-in console with a real example you can run.