Deploy and verify an App
This guide owns the deploy-and-verify loop (the page is deploy-with-the-cli; its title is the outcome): how you get a revenexx App from your laptop to a live capability behind the API gateway with the revenexx CLI, and how you confirm it actually works. The other tutorials link here for flags and checks. It picks up where Build your first App and Add a data model leave off — you have a tested app and want to ship it.
The last section covers a separate job, caching read-heavy capabilities, and it ships in the same deploy. This is for backend developers. You need the CLI installed, your revenexx identity, a tenant slug, and a machine-to-machine X-Revenexx-Api-Key from Cockpit. A development key with full access (*) is simplest for the deploy; the smoke test in step 3 alone needs your App's namespace, locations.* in this example.
Before you deploy: validate locally
A deployment triggers a build. Catch the avoidable failures on your laptop first:
node scripts/capabilities.js
revenexx apps generate --schema ./schema.json --manifest ./manifest.json --out ./src
npm test
The first command brings the capability list in sync with your schema and routes, the second regenerates the typed client, and the suite must be green.
Then run the checklist:
- Route paths are all-lowercase and namespaced under the app prefix.
- No
tenant_id/org_idcolumns inschema.json. - The manifest version is bumped if you changed the schema or capabilities.
src/db.generated.jsand the capabilities were regenerated after the last change.
1. Sign in
Set your key and tenant once, then sign in. login checks the key against that tenant and saves the key and endpoint, but not the tenant, so keep REVENEXX_TENANT exported (or pass --tenant on each command). Everything runs against the gateway at https://api.revenexx.com.
export REVENEXX_API_KEY="rvxk_..."
export REVENEXX_TENANT="<TENANT_SLUG>"
revenexx login --token "$REVENEXX_API_KEY" --tenant "$REVENEXX_TENANT"
The key is your machine-to-machine key, and the tenant is your tenant slug. See Authentication for how the tenant resolves.
To target a self-hosted instance, add --endpoint <url>. There is no project file to create — login, environment variables, and an optional .revenexx.yaml carry your context. See Configuration.
2. Deploy
revenexx deploy app is the whole path in one command. Run it from the app directory — the one with manifest.json. It registers the App if it's new, uploads the code, waits for the platform build, then checks the release round (the review of this upload of the version). Once the release is approved, it publishes the version and installs it on your tenant. If the release is still in review, it stops before publish and install; see When the release waits for approval. The build applies your schema.json (creating or updating tables, scoped to the tenant) and registers the capabilities your manifest declares; the install step is what writes your gateway routes, so a normal deploy has to install.
revenexx deploy app
For a new version of an App that already exists, bump the version in manifest.json — app versions are semantic (major.minor.patch) — and run it again.
The flags you'll actually reach for:
--function-id <id>— deploy to a specific existing App instead of matching one by name.--no-publish/--no-install— stop before the Marketplace publish or the tenant install. Remember that routes are written at install time, so an uninstalled version answers nothing at the gateway.--timeout <seconds>— how long to wait for the platform build. Default600.--release-wait <seconds>— how long to wait after the build for the release round to be approved or rejected. Default120. See When the release waits for approval.--runtime <runtime>(defaultnode-25) and--specification <spec>— used only on a first-time registration.--owner <tenant>— install on a tenant other than the active one. This is different from the global--tenant <slug>(CLI 0.5.0 or later), which sets the tenant the whole command runs as. Ship a schema change uses--tenantto point one deploy at a scratch tenant.
Run revenexx deploy app --help for the live list. deploy comes from a CLI plugin, so it exists only when your CLI is built with it. See Scaffolding.
revenexx deploy app against an App connected to a Git repository. The command uploads a tarball, and the active deployment loses its commit reference. Git-connected Apps deploy by pushing to the connected branch instead.List what's registered with revenexx apps list, and review or roll back deployment history with the revenexx apps subcommands (revenexx apps list-deployments, revenexx apps update-deployment). Note there is no apps- prefix on the subcommand — it's revenexx apps create-deployment, not revenexx apps apps-create-deployment.
Schema changes apply additively on each build: a new column or table is added; a renamed column adds the new one without dropping the old. In a release round, the build applies only the part the running version cannot trip over. Dropped columns, a new notNull and constraints on existing rows wait for the approval (see When the version waits for approval). Your authored capability contract is unaffected, so the SDK and gateway surface stay correct — clean up any stale column in a later release.
When the release waits for approval
Where the platform reviews releases, a version goes live only once its release round is approved. A version can have several rounds, numbered rc 1, 2, and so on. To find out whether yours does, run revenexx release status in the directory of an App or theme you have deployed before: it lists the rounds when the platform reviews releases, and fails with "this gateway does not offer release rounds yet" when it does not. A deploy shows the same: after the build it prints a release <version> rc <n> (deploy): <state> line each time the round changes state. As of CLI 0.7.5, deploy app and deploy theme wait for that round and say where it stands. deploy site is unchanged. Older CLIs do not wait, and their publish can be refused with HTTP 409. Upgrade the CLI.
After the build, the command reads the release round and waits up to 120 seconds for it to be approved or rejected. Set the wait with --release-wait <seconds>, a positive whole number.
The outcome depends on the round:
- Approved. The command publishes the version and installs it on your tenant, as described in step 2.
- Rejected. The command fails with
release <version> rc <n> was rejected: <reason> — publish and install did NOT run. It does not publish or install. - Still in review, or no round recorded yet. The build is uploaded but not live. The command warns that publish and install did not run, prints the decision due date when the platform reports one, and prints the command to finish later. This is a wait, not a failure.
While the round is open, the safe part of a schema change is already in the tables and tenants still run the previous approved version (unless they opted in to release candidates); the rest lands at approval (see When the version waits for approval).
Finish a deployment that is waiting with its resume command. It skips the upload and picks up the existing build:
revenexx deploy app --function-id <FUNCTION_ID> --deployment-id <DEPLOYMENT_ID>
For a theme, the resume command takes only the site and the deployment:
revenexx deploy theme --site-id <SITE_ID> --deployment-id <DEPLOYMENT_ID>
The command an App prints keeps the --owner, --no-publish and --no-install flags you used. The command a theme prints does not: run as printed, it publishes and installs. If the first run had --no-publish, --no-install or --owner, add them to the resume command yourself:
revenexx deploy theme --site-id <SITE_ID> --deployment-id <DEPLOYMENT_ID> --no-publish
A preview finishes with the same command under preview.
Check the state at any time with release status. Run it from the app or theme directory, or pass --function-id or --site-id:
revenexx release status --function-id <FUNCTION_ID>
It lists each round with its version, round number (rc), mode, state, gate results, decision due date and rejection reason.
If the gateway does not offer the release endpoints (it answers 404, 405 or 501), the command runs the flow it used before release approval, and publish and install follow the build. The same happens when the round cannot be read for another reason: the command warns that it continues as before release approval. Publish may then be refused with 409. Without release endpoints, release status and preview accept fail with "this gateway does not offer release rounds yet".
In CI
The wait ends without an error, including when the round is still in review, so do not rely on the exit code there. A rejected round does fail the command. With JSON output (--json), read the result instead: published and installed are false while the release waits, and release.state says where the round stands (release is null when no round is recorded, and also when the gateway has no release endpoints, so read published and installed first).
Previews
To check a build before it goes live, deploy it as a preview:
revenexx preview app
revenexx preview theme
A preview runs the same flow in preview mode. The CLI describes a preview app as a candidate that is reachable on its own host until it is accepted and approved; a preview theme does not go live until then. The resume command the command prints starts with preview too.
Once you have checked the candidate, give your go with preview accept:
revenexx preview accept --release-version <VERSION> --rc <N>
--release-versiondefaults to the version inmanifest.jsonortheme.json. Without one, it takes the newest preview candidate.--rcdefaults to the version's current candidate round, which the platform picks.--function-idor--site-idskips the lookup by name.
Your go does not replace the reviewer's approval. The command prints the round's state after accepting. If the round is not approved yet, it says the candidate goes live once a reviewer approves it as well. Once the reviewer has approved it, finish with the resume command the preview printed (revenexx preview app --function-id <FUNCTION_ID> --deployment-id <DEPLOYMENT_ID>, or revenexx preview theme --site-id <SITE_ID> --deployment-id <DEPLOYMENT_ID>). That run publishes and installs; preview accept alone does neither.
3. Smoke-test against the gateway
A deployed app answers at https://api.revenexx.com, scoped to your tenant. Test the live capability the same way any partner would call it — this is the real path, with gateway routing and tenant scoping in play, so it catches what a local test can't.
Use the REVENEXX_API_KEY and REVENEXX_TENANT you exported in step 1. Seed defaults, then exercise CRUD and the custom route:
# Seed the main warehouse (idempotent).
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 '{}'
# Create a location.
curl -s -X POST https://api.revenexx.com/v1/locations \
-H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" \
-H "X-Revenexx-Tenant: $REVENEXX_TENANT" \
-H "Content-Type: application/json" \
-d '{"code":"berlin","name":"Berlin store","type":"store"}'
# List locations.
curl -s https://api.revenexx.com/v1/locations \
-H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" \
-H "X-Revenexx-Tenant: $REVENEXX_TENANT"
# Batch availability (the custom route from Add a data model).
curl -s -X POST https://api.revenexx.com/v1/locations/availability \
-H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" \
-H "X-Revenexx-Tenant: $REVENEXX_TENANT" \
-H "Content-Type: application/json" \
-d '{"items":[{"sku":"WIDGET-1","quantity":5}]}'
Always send a body, even {}, on a POST to a capability that declares a request schema — the gateway validates the body and rejects an empty one. Confirm your routes are in the per-tenant API surface:
curl -s https://api.revenexx.com/v1/openapi.json \
-H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" \
-H "X-Revenexx-Tenant: $REVENEXX_TENANT" | grep '/v1/locations'
4. Cache the read-heavy capabilities
Reference data — locations, the category tree, market config — is read constantly and changes rarely. The gateway can cache those responses so repeat reads skip the round-trip to your app entirely. You write no caching code: it's a cache block on the capability's define, and the platform does the rest.
Declare the block in scripts/capabilities.js (never by hand-editing manifest.capabilities.json — the next regenerate would wipe it). Attach it to the list and get capabilities for locations:
// Tenant-global reference reads: one cached entry shared by every caller,
// dropped automatically when a location changes.
const LOCATIONS_CACHE = {
enabled: true,
scope: 'tenant', // shared across callers — only for tenant-global data
ttl: '1800s', // a staleness floor if an invalidation is ever missed
methods: ['GET'],
vary_by: { query: '*' }, // pagination/filter become part of the cache key
invalidate_on: ['locations'], // every entity this read derives from
};
// Attach it to the reads:
// def('locations.list', 'List stock locations', { method: 'GET', path: '/locations' },
// { parameters: PAGINATION_PARAMS, response: pageOf(LOCATION), cache: LOCATIONS_CACHE }),
// def('locations.get', 'Read one location', { method: 'GET', path: '/locations/{id}' },
// { response: LOCATION, cache: LOCATIONS_CACHE }),
The rules that keep a cache correct:
scope: tenantmeans one shared entry for the whole tenant — use it only for data that's identical for every caller, like reference and config reads. For anything that differs by who's asking, use the defaultscope: principal(a separate entry per caller) or don't cache it.invalidate_onlists every entity the response reads from. When any of them changes, the gateway drops the cached entries. Miss one and you serve stale data. A write to that entity is what triggers the drop — you never mark mutations as cacheable.- Never cache volatile or buyer-specific data on a tenant-shared entry. Stock availability and resolved prices change per request or per buyer; welding them into a cacheable reference read pins the fast thing behind the slow one. Keep them on their own capabilities.
Bump the manifest version and create a new deployment. To confirm it's working, call a cached GET twice — the second response carries X-Cache: HIT:
curl -si https://api.revenexx.com/v1/locations \
-H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" \
-H "X-Revenexx-Tenant: $REVENEXX_TENANT" | grep -i x-cache
Troubleshooting
- The deployment reports a build failure. The platform puts the validation or schema error in the build output. The usual causes: a capability
summaryover 255 characters, a route path that isn't all-lowercase, or a dependency key that isn'tvendor/app. - The warning says the build is not live until it is approved, or that no release round is recorded yet. The round is still in review, or the platform has not recorded it. Check with
revenexx release status, then finish with the resume command the warning prints. - Publish fails with 409
version_not_approved. The registry refused to publish a version that is not approved, and install did not run. With CLI 0.7.5 or later this happens only when the command could not read the release round and went on as before release approval (it warns "continuing as before release approval"); a round it can read stops the command before publish instead. Older CLIs do not check the round, so they hit it whenever the version is still in review. Check withrevenexx release status --function-id <FUNCTION_ID>, then finish with the resume command the error prints once the round is approved. This applies to apps. For a theme, a failed publish or install prints a warning and the command continues. - The command fails with "release … was rejected". The reviewer rejected the round; the text after the colon is the reason, and the
rejectioncolumn ofrevenexx release statusshows it too. Fix that and deploy again; bump the manifest version if the build reports that the version is already registered. - The version is already registered. A previous failed build registered the version. Bump the manifest version and create a new deployment.
- A route is missing from
openapi.json. The capability wasn't declared. Re-runnode scripts/capabilities.js, confirm the route appears in the list, and deploy again. X-Cachenever readsHIT. The capability has nocacheblock (or it was dropped by a regenerate). Confirm the block lives incapabilities.js, diffmanifest.capabilities.jsonafter regenerating, bump the version, and deploy again.- A cached response is stale. An
invalidate_onentity is missing. List every entity the response derives from, including cross-app dependencies with their{app}.prefix.
Where to go next
- Build your first App — the full app from scratch.
- Add a data model — relationships and custom routes.
- CLI — every command in depth.
- App SDK — the data client and router.