Deploy with the CLI

The full deploy-and-verify loop for a revenexx App — validate locally, sign in, deploy with the CLI, and smoke-test against the gateway, plus caching for read-heavy capabilities.

This tutorial is the deploy-and-verify loop on its own: 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. 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.

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.

Before you deploy: validate locally

A deployment triggers a build. Catch the avoidable failures on your laptop first:

Pre-push checks
node scripts/capabilities.js   # capability list in sync with schema + routes
npx appsdk generate --schema ./schema.json --manifest ./manifest.json --out ./src --target js
npm test                       # the suite must be green

Then run the checklist:

  • Route paths are all-lowercase and namespaced under the app prefix.
  • No tenant_id/org_id columns in schema.json.
  • The manifest version is bumped if you changed the schema or capabilities.
  • src/db.generated.js and the capabilities were regenerated after the last change.

1. Sign in

Authenticate with your API key and tenant slug. The CLI saves the session to ~/.revenexx, so every later command is scoped to that tenant against the gateway at https://api.revenexx.com.

Sign in
revenexx login --token "$REVENEXX_API_KEY" --tenant "$REVENEXX_TENANT"

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, publishes the version, and installs it on your tenant. 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.

Deploy
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. Default 600.
  • --runtime <runtime> (default node-25) and --specification <spec> — used only on a first-time registration.
  • --owner <tenant> — install on a tenant other than the active one.

Run revenexx deploy app --help for the live list. If deploy isn't a known command in your install, the deploy plugin isn't built in — see Scaffolding.

Don't run 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. Your authored capability contract is unaffected, so the SDK and gateway surface stay correct — clean up any stale column in a later release.

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.

Set your key and tenant once:

Env
export REVENEXX_API_KEY="rvxk_..."     # your machine-to-machine key
export REVENEXX_TENANT="<TENANT_SLUG>" # your tenant slug

Seed defaults, then exercise CRUD and the custom route:

Smoke test
# 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 the previous tutorial).
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:

Confirm routes
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'

You can also drive ad-hoc calls through the CLI's client, pointed at the gateway:

CLI client
revenexx client --endpoint https://api.revenexx.com --key "$REVENEXX_API_KEY"

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.json — the next regenerate would wipe it). Attach it to the list and get capabilities for locations:

scripts/capabilities.js — cache block
// 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: tenant means 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 default scope: principal (a separate entry per caller) or don't cache it.
  • invalidate_on lists 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:

Verify the cache
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 summary over 255 characters, a route path that isn't all-lowercase, or a dependency key that isn't vendor/app.
  • 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-run node scripts/capabilities.js, confirm the route appears in the list, and deploy again.
  • X-Cache never reads HIT. The capability has no cache block (or it was dropped by a regenerate). Confirm the block lives in capabilities.js, diff the manifest after regenerating, bump the version, and deploy again.
  • A cached response is stale. An invalidate_on entity is missing. List every entity the response derives from, including cross-app dependencies with their {app}. prefix.

Where to go next

Was this page helpful?