A reference Node.js application that proves a simple point: you can run an AI-powered application on Cloudways Managed Node.js Hosting, using Jev — a structured decision model — through the OpenRouter API.
To test that out, we built a small sample app with Node.js and Express: paste a messy client request, and Jev instantly returns a structured decision — category, priority, assigned team, and whether to escalate.
Clone it, add your OpenRouter API key, deploy to Cloudways, and you have a working AI decision layer in under 30 minutes.
Digital agencies receive dozens of unstructured client requests every day:
"The checkout on our WooCommerce website stopped working." "Can you change the button color?" "The website is down and we have a campaign launching in two hours."
Each message needs to be categorized, prioritized, assigned to a team, and — in some cases — escalated. Doing this manually is slow and inconsistent.
This application lets a user paste a client request and immediately receive a structured analysis:
| Field | Example output |
|---|---|
| Category | Technical Issue |
| Priority | Critical |
| Assigned Team | Developer |
| Escalation | Yes |
| Overall Confidence | 100% |
The decisions are made by Jev, a structured decision model that returns typed, probability-weighted answers — not generated prose. This is not a chatbot: the typed output plugs directly into application logic. priority === 'critical' is something your code can branch on. A paragraph of generated text is not.
Jev is TypeSafe's first "System One" model. Instead of generating text, it evaluates application state against typed questions and returns structured answers with calibrated probabilities.
Jev answers three kinds of question:
| Primitive | What it answers | Returns |
|---|---|---|
| Choice | Which one of these options? | Selected option + probability per option + confidence |
| Noul | Does this condition hold? | Probability of yes (0–1) |
| Score | Where does this fall on a scale? | Probability-weighted position + confidence |
This app uses three Choice questions (category, priority, team) and one Noul question (escalate?).
Jev is available via OpenRouter. You do not need a TypeSafe account — an OpenRouter API key is all you need. Requests are billed per input token to your OpenRouter account; output tokens are free. A triage request in this app costs about $0.00003 — every response includes a usage.cost field showing the exact amount.
The Decisions API is in beta. See the Jev model page for current pricing, limits, and status.
The app is intentionally minimal so the Jev integration stays front and center:
| Layer | Technology | Why |
|---|---|---|
| Runtime | Node.js 18+ | Native fetch() — no HTTP client dependency needed |
| Backend | Express | The most widely known Node.js web framework — one file, ~150 lines |
| Frontend | Vanilla HTML / CSS / JavaScript | No build step, no framework, nothing to compile |
| AI | Jev via the OpenRouter Decisions API | Structured decisions instead of generated text |
| Config | dotenv | Environment-variable configuration |
| Hosting | Cloudways Managed Node.js Hosting | Git-based deployments, managed nginx + PM2, environment variable panel |
Total production dependencies: 2 (express, dotenv).
Express does three jobs here:
- Serves the static frontend (
public/folder) - Exposes one API route —
GET /analyze— which validates input, calls Jev, and returns clean JSON - Keeps the API key server-side — the browser never sees it
User
│
▼
┌─────────────────────┐
│ Browser │
│ HTML + CSS + JS │
└────────┬────────────┘
│ GET /analyze?message=...
▼
┌─────────────────────┐
│ Node.js + Express │ ← hosted on Cloudways
│ src/server.js │ (nginx + PM2, managed)
└────────┬────────────┘
│ POST /api/alpha/decisions
│ Authorization: Bearer OPENROUTER_API_KEY
▼
┌──────────────────────┐
│ Jev (OpenRouter) │ ← external API
│ ~typesafe/jev-latest│
└────────┬─────────────┘
│ Structured JSON
│ { category, priority, team, escalate }
▼
┌─────────────────────┐
│ Node.js + Express │
│ Parses + returns │
└────────┬────────────┘
│ Clean JSON
▼
┌─────────────────────┐
│ Browser │
│ Renders result │
└─────────────────────┘
Cloudways hosts the Node.js application. Jev is an external API. They are separate services.
flowchart TD
A[User Browser] -->|GET /analyze| B[Express on Cloudways]
B -->|POST /api/alpha/decisions| C[Jev via OpenRouter]
C -->|typed answers| B
B -->|clean JSON| A
Everything lives in src/server.js. The backend sends a state (the client message, with context) and four questions to Jev's Decisions API:
POST https://openrouter.ai/api/alpha/decisions
{
"model": "~typesafe/jev-latest",
"state": "Client support request for a digital web agency: The checkout on our WooCommerce website stopped working...",
"questions": {
"category": {
"type": "choice",
"instructions": "What is the primary category of this client request?",
"criteria": {
"technical_issue": "Technical Issue - something on the site is broken or malfunctioning",
"outage": "Outage - the site or a critical service is completely down",
"design_change": "Design Change - visual or styling adjustments",
"new_feature": "New Feature - new functionality to be built",
"performance": "Performance - the site is slow or resource-heavy",
"content_change": "Content Change - text, images, or copy updates",
"other": "Other - anything that does not fit the above"
}
},
"priority": {
"type": "choice",
"instructions": "What is the urgency of this client request? Consider business impact and deadlines.",
"criteria": {
"low": "Low - cosmetic or minor, no business impact",
"medium": "Medium - should be handled soon but nothing is broken",
"high": "High - significant impact or an approaching deadline",
"critical": "Critical - revenue is being lost or a hard deadline is at risk"
}
},
"team": {
"type": "choice",
"instructions": "Which team or role should handle this request?",
"criteria": {
"developer": "Developer - code, bugs, technical work",
"designer": "Designer - visual design and styling",
"content": "Content - copywriting and content updates",
"project_manager": "Project Manager - planning, scoping, coordination"
}
},
"escalate": {
"type": "noul",
"instructions": "Should this request be escalated to a senior team member? Escalate if critical, revenue-affecting, or deadline-driven."
}
}
}
⚠️ Important format detail: in a Choice question,criteriamust be a record (object) mapping each option ID to a description — not an array. If you send an array of{id, label}objects, Jev rejects the request with400: "Invalid input: expected record, received array". This is the single most common integration mistake.The descriptions also matter: richer criteria ("revenue is being lost or a hard deadline is at risk") give Jev more signal and produce noticeably better decisions than bare labels.
Jev returns typed answers keyed by question ID — no text to parse, no regex, no prompt-output cleanup:
{
"model": "typesafe/jev-1.13-20260917",
"answers": {
"category": { "type": "choice", "choice": "technical_issue", "confidence": 0.99 },
"priority": { "type": "choice", "choice": "critical", "confidence": 1.0 },
"team": { "type": "choice", "choice": "developer", "confidence": 1.0 },
"escalate": { "type": "noul", "noul": 0.94 }
},
"usage": { "input_tokens": 705, "output_tokens": 177, "cost": 0.00002961 },
"provider": "TypeSafe"
}The backend converts this into clean JSON for the frontend. One line captures the whole idea of the demo:
escalate: { value: answers.escalate.noul >= 0.5, ... }An AI judgment used directly as application logic. In a real system, this is where you'd trigger a Slack alert, assign a ticket, or page an on-call engineer.
The API key never leaves the server.
- Node.js 18 or later (
node -vto check — nativefetchrequires 18+) - npm (bundled with Node.js)
- OpenRouter API key — see Step 1 below
- Git and a GitHub account
- Cloudways account (only needed for deployment)
Jev is accessed through OpenRouter. There is no separate TypeSafe signup.
- Go to openrouter.ai and create a free account.
- Open openrouter.ai/settings/keys.
- Click Create Key, give it a name (e.g.
agency-triage), and copy the key — it starts withsk-or-v1-. - Add a small amount of credit at openrouter.ai/settings/credits — even $1 covers tens of thousands of triage requests.
🔒 This key must never appear in frontend code, in Git, or in any file you commit. It lives in exactly two places: your local
.envfile (which is gitignored) and the Cloudways Environment Variables panel.
# Clone
git clone https://github.com/cloudways/cloudways-jev-nodejs-demo.git
cd cloudways-jev-nodejs-demo
# Install dependencies (express + dotenv)
npm install
# Create your environment file
cp .env.example .envOpen .env and set your key:
OPENROUTER_API_KEY=sk-or-v1-xxxxxxxxxxxxxxxxxxxxxxxx
PORT=3000
Never commit
.env. It is already in.gitignore.
Start the server:
npm startYou should see:
Running on 0.0.0.0:3000
API key set: true
Open http://localhost:3000, click one of the example chips, and hit Analyze Request.
You can also hit the API directly:
curl "http://localhost:3000/analyze?message=The%20checkout%20stopped%20working"{
"result": {
"category": { "id": "technical_issue", "label": "Technical Issue", "confidence": 99 },
"priority": { "id": "critical", "label": "Critical", "confidence": 100 },
"team": { "id": "developer", "label": "Developer", "confidence": 100 },
"escalate": { "value": true, "probability": 94 },
"overallConfidence": 100
},
"usage": { "input_tokens": 705, "output_tokens": 177, "cost": 0.00002961 }
}The steps below reflect the Cloudways Node.js hosting interface as tested with this app. Field names may evolve — check the Cloudways documentation if something looks different.
Fork this repository, or create your own repo and push the code. A private repo works fine — Cloudways connects to it via GitHub authorization.
- Log in at unified.cloudways.com.
- Dashboard, on the left side bar, click on Velocity. Then Click Add Server. Here you need to chose the plan and click Continue
3. Name the server and select the region where you want to deploy the server.
4. Now Click **Continue**.
5. Your server will be deployed in few minutes with NodeJS application.
- Open your application → You can connect it with GitHub, GitLab and Bitbucket
- Connect your GitHub account and select your repository.
- Set the branch to
main. - Enable Auto-deployment — every push to
mainwill then deploy automatically. Click Continue.
Go to Deployment Management → Settings and set:
| Field | Value |
|---|---|
| Framework Preset | Express |
| Branch | main |
| Node version | 18 or newer |
| Root Directory | ./ |
| Package Manager | npm |
| Entry File | src/server.js |
Click Save & Redeploy.
⚠️ Entry File is the setting that trips people up. The default value isnpm run start, but this field expects a file path. Set it tosrc/server.js.
- In the left sidebar, open Environment Variables.
- Click + Add Variable and add exactly one variable:
| Key | Value |
|---|---|
OPENROUTER_API_KEY |
sk-or-v1-... (your key) |
- Save.
⚠️ Do not add aPORTvariable on Cloudways. The platform manages the port itself and injects it into the process. The app readsprocess.env.PORT || 3000, which works in both environments. SettingPORTmanually can conflict with the platform's webserver configuration.
- Go to Deployment Management → Deployments and click Redeploy (or just push to
mainif auto-deploy is on). - Wait for the ✅ Success status. The build log should end with:
[info] Application started with PM2 (process 'node-app', port 3000) and enabled on boot.
Status : SUCCESS
- Verify the app is alive by opening the health endpoint in your browser:
https://your-app-url.cloudwaysnodeapps.com/health
You should see: {"ok":true}
- Open the app URL itself, click an example chip, and hit Analyze Request. If you see the result cards, the full stack is working:
Browser → Express on Cloudways → Jev on OpenRouter → Express → Browser
Here you can see the sample app that we built

Click each example chip, or paste your own client messages. Expected behavior:
| Test message | Category | Priority | Team | Escalate |
|---|---|---|---|---|
| "The checkout on our WooCommerce website stopped working. Customers can't complete their orders." | Technical Issue | Critical | Developer | Yes |
| "The client's website is completely down and they have a campaign launching in two hours." | Outage | Critical | Developer | Yes |
| "Can you change the button color on the homepage from blue to green?" | Design Change | Low | Designer | No |
| "The client wants us to add a booking form to the website." | New Feature | Medium | Developer / PM | No |
| "Several pages take more than 10 seconds to load." | Performance | High | Developer | Varies |
Also worth testing:
- Empty input → the Analyze button simply refocuses the textarea
- A message over 4,000 characters → friendly "message too long" error
- API-level testing →
GET /analyze?message=...returns raw JSON;GET /healthconfirms the process is up
Here you can see the result based on the decision made by Jev

cloudways-jev-nodejs-demo/
│
├── public/
│ ├── index.html ← Single-page frontend
│ ├── styles.css ← All styles (dark SaaS-style dashboard)
│ └── app.js ← Frontend logic (fetch, render)
│
├── src/
│ └── server.js ← Express server + entire Jev integration
│
├── .env.example ← Environment variable template
├── .gitignore ← Excludes .env and node_modules
├── package.json ← 2 dependencies: express, dotenv
├── LICENSE
└── README.md
The entire integration — questions, API call, response parsing, error handling — is one readable file: src/server.js.
- A Node.js/Express application runs on Cloudways Managed Node.js Hosting.
- The application communicates with Jev through the OpenRouter Decisions API.
- Jev turns unstructured text into structured, typed decisions.
- Application code can branch on those decisions — display them, route a ticket, trigger a workflow.
- Another developer can clone this repo, add an API key, deploy to Cloudways, and have it working
MIT — see LICENSE.
- Jev on OpenRouter
- Decisions API reference
- Jev model page + pricing
- TypeSafe documentation
- Cloudways Managed Node.js Hosting
- OpenRouter API keys