Your First App in 15 Minutes
This is the canonical first-app walkthrough. It takes you from an empty directory to a capability answering a real request at https://api.revenexx.com/v1 — no hand-written files. You will read two of the generated files along the way, because they are the two you will spend the rest of your time in. If the vocabulary here is new — tenant, app, capability, Studio — Concepts defines it on one page and takes five minutes.
Budget about fifteen minutes with your account already in hand. The clock does not include signing up — during the soft launch that takes hours to days, so sort it out first (see Prerequisites). Before you start you need a revenexx account and a tenant you may install an app on, an API key to call your app in the last two steps, and Node.js 22 or newer to run the scaffolded app's install and tests.
1. Install the CLI
Option A: npm
npm install -g @revenexx/cli
revenexx -v
The package is public, so no registry configuration and no token are needed. See Installation for every option, including Windows.
Option B: Homebrew (macOS / Linux)
brew install revenexx-sdks/cli/revenexx
revenexx -v
Option C: Download a binary (any platform)
Grab the asset for your platform from the latest release — this is the route to use on Windows if you don't have npm:
curl -fsSL -o revenexx https://github.com/revenexx-sdks/cli/releases/latest/download/revenexx-darwin-arm64
chmod +x revenexx && sudo mv revenexx /usr/local/bin/revenexx
revenexx -v
2. Sign in
Run login with no arguments. The CLI opens your browser, you sign in with ID, and your session is saved to ~/.revenexx/prefs.json so later commands don't ask again.
revenexx login
Then pick the tenant you're working in. The line printed after login names the tenants your account can access, and revenexx tenants list shows them again at any time (the same slug appears in Cockpit's tenant switcher, top left):
revenexx tenants use <your-tenant-slug>
There is no project file and no init step — context comes from your login session, environment variables, and the active tenant.
In CI there is no browser, so pass a gateway API key and the tenant directly. Create the key in Cockpit under your avatar menu → Account → API-Keys:
revenexx login --token "$REVENEXX_API_KEY" --tenant "$REVENEXX_TENANT"
A supplied key always wins over a stored browser session, so a pipeline never opens a browser. See CLI authentication for the full resolution order.
3. Scaffold the app
revenexx create app locations --entity locations
One entity, plural snake_case. --entity is repeatable or comma-separated.
That writes the complete app contract and runs the codegen in-process:
locations/
├── manifest.json identity, type, permissions, events
├── manifest.capabilities.json the generated capability register — never hand-edit
├── schema.json your entities → platform tables
├── cockpit.json the admin UI: navigation + a list view
├── billing.json Marketplace listing and pricing
├── package.json one dependency: @revenexx/app-sdk
├── scripts/capabilities.js regenerates the capability register from schema.json
├── src/main.js the function entrypoint
├── src/db.generated.js the typed data client (generated — never hand-edit)
├── src/db.generated.d.ts its type declarations (generated too)
├── test/locations.test.js node --test against an in-memory store
├── README.md
└── .gitignore
Scaffolding validates before it writes: the app name must match ^[a-z][a-z0-9-]*$, the entity name ^[a-z][a-z0-9_]*$, and names that collide with SQL or JavaScript keywords (order, user, select, function, public, …) are refused outright. A non-empty target directory aborts the scaffold rather than merging into it.
cd locations && npm install
4. Read the two files that matter
schema.json — the tables you own
{
"$schema": "https://schemas.revenexx.com/schema.schema.json",
"entities": {
"locations": {
"scopeable": ["market"],
"columns": {
"id": { "type": "uuid", "pk": true, "default": "gen_random_uuid()" },
"name": { "type": "text", "notNull": true, "check": "length(name) > 0" },
"metadata": { "type": "jsonb" },
"created_at": { "type": "timestamptz", "notNull": true, "default": "now()" },
"updated_at": { "type": "timestamptz", "notNull": true, "default": "now()" }
},
"indexes": [
{ "columns": ["tenant_id", "name"] }
]
}
}
}
Four things to notice, because they are the four that surprise people:
typeis a raw PostgreSQL type, written verbatim into the DDL. There is nostring, nodecimal, nofloat.defaultandcheckare raw SQL expressions — so when you add a literal string default, it needs SQL quotes inside the JSON string:"'warehouse'".- There is no
tenant_idcolumn. The platform injects it and scopes every read and write for you — you may name it in an index, as above, but never declare it. scopeable: ["market"]lets a row belong to one market or stay global — see Permissions and scope.
Full detail in the schema reference.
src/main.js — your function
'use strict';
const { createApp, mountCrud, badRequest } = require('@revenexx/app-sdk/router');
const { createDb, ENTITIES } = require('./db.generated');
// Idempotent per-tenant seeding — runs on the app.installed lifecycle event.
async function ensureDefaults(db) {
return { created: [], existing: [] };
}
function buildApp(db) {
const app = createApp({
name: 'locations',
healthExtra: () => ({ entities: Object.keys(ENTITIES) }),
});
mountCrud(app, db.locations, {
path: '/locations/locations',
columns: ENTITIES.locations.columns,
});
return app;
}
module.exports = async (context) => {
const { req, res } = context;
const headers = req.headers || {};
const jwt = headers['x-revenexx-context'] ?? headers['X-Revenexx-Context'] ?? '';
const path = (String(req.path || '/').replace(/\/+$/, '')) || '/';
const trigger = headers['x-revenexx-trigger'] ?? '';
const event = headers['x-revenexx-event'] ?? '';
if (trigger === 'event' && jwt) {
const db = createDb({ adapter: 'runtime', context });
if (/(^|\.)installed$/.test(event)) {
const result = await ensureDefaults(db);
return res.json({ event, ...result });
}
return res.json({ event, ignored: true });
}
if (path !== '/' && !jwt) {
return res.json({ error: 'missing tenant identity — this route needs the x-revenexx-context header' }, 503);
}
const db = createDb({ adapter: 'runtime', context });
return buildApp(db).handler()(context);
};
module.exports.buildApp = buildApp;
module.exports.ensureDefaults = ensureDefaults;
createApp is the router. mountCrud wires one entity to the five standard REST routes — note the path is /locations/locations: every route lives under the app's own prefix (the app name), then names the entity. createDb with the runtime adapter reads the invocation context, so you configure no endpoint, no tenant and no credential. The scaffold also wires lifecycle events (ensureDefaults runs on app.installed) and a health route at /. See the router and the typed client.
5. Regenerate and test
Any time you change schema.json, run the same three steps:
revenexx apps capabilities --write
revenexx apps generate
npm test
apps capabilities --writeregeneratesmanifest.capabilities.jsonfromschema.jsonand strips the capability register out ofmanifest.json. Without--writeit prints a dry run.apps generateregenerates the typed data client undersrc/.npm testruns the scaffoldednode --testsuite against an in-memory store, so it passes with no infrastructure.
Both generators are idempotent — re-running them on an unchanged schema produces no diff. The scaffolded npm run capabilities script emits identical output, so use whichever fits your workflow.
6. Preview the admin UI (optional)
Nothing below depends on this step — skip it on a first run if you would rather see the app live first.
cockpit.json is already populated with a navigation entry and a list view over your entity. Preview it locally with the devkit before you deploy, so a bad column name shows up on your laptop rather than in a merchant's sidebar. The devkit and the local preview loop are documented with the rest of the Cockpit surface — see Preview locally.
7. Deploy
revenexx deploy app
One command runs the whole path: register the app if it is new, upload the directory, wait for the platform build, publish the version, install it on the active tenant.
The install step is the one people skip and regret: routes are written at install time. --no-install leaves you with a built version that answers nothing new. See Deploying.
To deploy from a connected Git repository instead, push to the production branch — a git-wired App builds on push. Do not run revenexx deploy app against one: it uploads a tarball, and the active deployment loses its commit.
8. Call it for real
No API key yet? Confirm it is live from the CLI
Your login session is enough to invoke the app directly and read its health route. Find the app's id with revenexx apps list (apps get a generated id — the name won't work), then:
revenexx apps create-execution --function-id <app-id> --path / --method GET
status : completed
responseStatusCode : 200
responseBody : {"app":{"id":"6a98224e4f5ed184dbde","name":"locations","deployment":"6a98224e7b1ff30fdef4","runtime":"Node.js-25"},"status":"ok","routes":[{"method":"GET","path":"/locations/locations"},{"method":"POST","path":"/locations/locations"},{"method":"DELETE","path":"/locations/locations/{id}"},{"method":"GET","path":"/locations/locations/{id}"},{"method":"PUT","path":"/locations/locations/{id}"}],"timestamp":"2026-09-02T13:20:58.324Z","entities":["locations"]}
That proves the build, publish and install all happened: the five routes in routes were written at install time. It stops there, though — a direct execution skips the gateway, so the entity routes answer 503 missing tenant identity this way. To exercise them you go through api.revenexx.com, and that needs a key.
Through the gateway
Your capability now answers on the public gateway. The two headers carry your API key and your tenant slug — if you have not created a key yet, do it now in Cockpit under your avatar menu → Account → API-Keys (see Prerequisites), then export both:
export REVENEXX_API_KEY="<your-api-key>"
export REVENEXX_TENANT="<your-tenant-slug>"
Seed the default row, then list:
curl -s -X POST https://api.revenexx.com/v1/locations/defaults \
-H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" \
-H "X-Revenexx-Tenant: $REVENEXX_TENANT" \
-H "Content-Type: application/json" \
-d '{}'
curl -s "https://api.revenexx.com/v1/locations?limit=50" \
-H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" \
-H "X-Revenexx-Tenant: $REVENEXX_TENANT"
The -d '{}' is not decoration. Always send a body on a POST whose capability declares a request schema — the gateway validates the body against the contract before your function is invoked, and no body at all fails that check.
Call /defaults explicitly rather than trusting the install event to have seeded anything: app.installed does not reliably fire on a Marketplace install. Every shipped app exposes an idempotent /defaults route for exactly this reason.
9. Confirm it is in the published contract
The same declaration that produced the route produced the contract. Your tenant's merged OpenAPI document should now contain your operation:
curl -s https://api.revenexx.com/v1/openapi.json \
-H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" \
-H "X-Revenexx-Tenant: $REVENEXX_TENANT" \
| grep -o '"/v1/locations[^"]*"'
You should see "/v1/locations/locations" and "/v1/locations/locations/{id}" — the document keys carry the /v1 prefix. If your path is in there, so is your app — in the generated SDKs, in the API Explorer, and in the MCP tool list. That document is the only place any of them read from, which is why a wrong summary is fixed in the manifest and redeployed rather than anywhere else. See The published OpenAPI document.
When something does not answer
404on your path. The install step did not run, or the route does not sit under the app's own prefix. Re-runrevenexx apps capabilities --writeandrevenexx deploy app.- A list never returns past page one. The list capability declares no
limit/offsetparameters, so the gateway strips them. See Pagination and filtering. 400on aPOSTyou thought was fine. You sent no body to a capability that declares arequestschema. Send{}.403on a write.manifest.permissionsdoes not grant that operation on that entity. Add it, regenerate, redeploy. See Entity access.- Token expired? Re-authenticate:
revenexx login, or in CIrevenexx login --token <new-token> --tenant <your-tenant-slug>. - Deployment failed? Find your app's id with
revenexx apps list(apps get a generated id — the name won't work), then review the build:revenexx apps list-deployments --function-id <app-id>. Build logs are in Cockpit under App Studio → My Apps. - Can't find the CLI? Verify installation:
which revenexx(Unix) orwhere.exe revenexx(Windows). If missing, reinstall.