Publish and install
revenexx deploy app publishes and installs by default, and it looks redundant to publish and install a version of an app the tenant already has. It is not.
Routes are written at install time
A new capability, a changed route, a new schedule — none of them are reachable until the install step runs for that version.
That is the fact behind the most common "my app deployed but the endpoint 404s" report. A build that succeeded proves your declarations were accepted; it does not put anything on the gateway. The install is what writes your routes for that tenant.
So --no-install leaves you with a built, published version that answers nothing new. It is useful for exactly one thing — separating "does this build" from "does this go live" in a pipeline — and confusing for everything else.
The same is true of a schedule: a tick starts firing for a tenant when the version declaring it is installed there.
Publish
Publishing lists the app in the Marketplace, and a tenant who installs it gets your latest published version. It applies to public apps only — a private app is never listed, and its owner installs it without publishing anything.
revenexx deploy app # builds, publishes, installs
revenexx deploy app --no-publish # builds and installs, no Marketplace publish
revenexx deploy app --no-publish --no-install # builds only
A private app needs --no-publish. The bare command publishes by default, the gateway refuses to publish a private app, and the deploy stops there — the install never runs. Use --no-publish until the app is public.
Unpublishing takes the app off the Marketplace so nobody new can install it. It does not uninstall it from the tenants that already have it, and it does not stop their traffic. Treat it as "stop offering this", not "recall this". It is a switch on the app rather than on one version, so it withdraws your last good version along with the bad one.
That distinction matters when you find a bad version: unpublishing stops the bleeding for new installs, and fixing the tenants already on it means shipping a new version and installing it.
Install
Installing puts a published version onto one tenant. Three things happen that are worth knowing individually:
- The routes are written. Your capabilities become live paths under
/v1for that tenant, and appear in that tenant's/v1/openapi.json. - The grants take effect. The access register is enforced from here on, and the merchant has consented to it.
- The dependencies are resolved. A
dependenciesentry is installed automatically; apeerDependenciesentry becomes a prompt. See Dependencies.
revenexx deploy app --owner acme-staging
--owner names the vendor the app is registered under, not the tenant it lands on. The tenant comes from your project configuration and credentials, so this installs onto your tenant — there is no flag that installs onto somebody else's.
A Marketplace install is different in one way that bites
When a merchant installs your app from the Marketplace, everything above still happens. What you cannot rely on is the event:
app.installed does not reliably fire on a Marketplace install.
So an app that seeds its defaults only in an install-event handler ships to a merchant with an empty configuration and no obvious reason why. The reliable pattern:
- Seed on the
app.installedevent. - Have the app ensure its defaults on first use as well, so a missed event costs nothing.
- Expose that seed as an idempotent
/defaultscapability, which is how you call it yourself on a tenant you own.
An API key identifies one tenant, so no credential you hold reaches a merchant's tenant and nobody will call a seeding route there for you. On a merchant's install, steps 1 and 2 are the whole mechanism; step 3 is for your own verification:
curl -X POST https://api.revenexx.com/v1/serials/defaults \
-H "X-Revenexx-Tenant: <YOUR_TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..." \
-H "Content-Type: application/json" \
-d '{}'
It is not a workaround for a bug you can wait out — it is the reliable path. See Verifying live and Events you receive.
Make the seed idempotent by checking before you write. A seed that duplicates rows on a second call is worse than none, because both paths may land.
Uninstall
A tenant can uninstall your app. app.uninstalled is delivered to apps that listen for it, and it is the one chance you get to do anything.
What to do in that handler is usually nothing destructive: cancel outbound work, stop expecting to be called, and leave the rows alone, so a merchant who uninstalls and reinstalls still has their data. That is a choice you are making, not a platform retention rule and not an answer to an erasure request — if a merchant asks you to delete their data, that is yours to handle.
Upgrades
Whether and when an installed tenant moves from one version of your app to the next is decided outside your app, and the rules are not something we can state accurately today.
Do not design around an assumed auto-upgrade window, and do not assume a tenant will be on your latest version.
What you can rely on is the contract discipline. Because a breaking change is always a new capability key on a new route, a tenant on an older version keeps working whether or not anybody upgrades them. That is the property to design for:
- Never remove a capability a tenant might still be calling. Add the replacement; leave the original serving.
- Never assume a caller has your latest contract. Additive changes are safe precisely because old callers ignore what they do not know.
- Do not rely on every tenant getting a fix at the same time. If a fix matters, deploy it per tenant and confirm it.
Next steps
- Deploying — the command that runs all of this.
- Verifying live — proving the install worked.
- Consent — what the merchant agrees to at install.
- Versioning — why an old tenant keeps working.
- Publish on the Marketplace — the sequence these pages each cover one slice of.