Manifest Reference
This is the reference for manifest.json. If you have not seen a complete app yet, read App model first — it shows how manifest.json sits next to the other files.
manifest.json is the contract between your app and the platform. It answers five questions:
- Identity — who built this, what is it called, what version is this, and who may install it?
- Dependencies — which other apps does this one need, and which does it merely expect?
- Access — what may this app reach? The default is nothing; you opt in through
permissions. - Contribution — what does this app contribute to the platform: capabilities, scope dimensions, permission keys, roles?
- Activation — what invokes this app besides an HTTP request: events, cron schedules, warmup?
The authoritative JSON Schema is served at https://schemas.revenexx.com/manifest.schema.json. Point your editor at it — the manifest is strict (additionalProperties: false), so a typo'd key fails validation rather than being ignored.
Complete example
{
"$schema": "https://schemas.revenexx.com/manifest.schema.json",
"name": "serials",
"vendor": "acme",
"version": "1.2.0",
"type": "private",
"icon": "assets/icon.svg",
"title": { "en": "Device Serials", "de": "Seriennummern" },
"description": "Tracks device serial numbers from receipt through sale and into service.",
"dependencies": { "revenexx/markets": "^0.1" },
"peerDependencies": { "revenexx/prices": "^1.0" },
"permissions": [
{ "entity": "device_serials", "access": ["read", "create", "update"] },
{ "entity": "service_events", "access": ["read", "create"] },
{ "capability": "orders.get", "compatible": "^1.0" },
{ "outbound": "api.acme-service.example", "paths": ["/v2/*"] }
],
"provides_permissions": [
{
"permission": "serials.register",
"title": { "en": "Register a device serial" },
"capabilities": ["serials.create"]
}
],
"tags": [
{ "name": "serials", "description": { "en": "Serial registry and service history." } }
],
"warmup": false,
"schedules": [
{ "name": "expire-warranties", "cron": "0 3 * * *" }
],
"events": {
"emits": [
{ "name": "serial.registered", "entity": "device_serials", "on": ["insert"] },
{ "name": "serial.serviced", "entity": "service_events", "on": ["insert"], "channel": true }
],
"listens": ["app.installed", "app.uninstalled"]
}
}
capabilities is the one large block missing from that example: apps normally move it into the sibling file manifest.capabilities.json, which the platform splices in before validation. See Capabilities.
Identity
| Field | Type | Required | Description |
|---|---|---|---|
$schema | string | No | https://schemas.revenexx.com/manifest.schema.json. Editor validation only. |
name | string | Yes | App identifier. Lowercase, alphanumeric, hyphens. Becomes the app's route namespace and part of its table prefix. |
vendor | string | Yes | Publisher slug. revenexx for platform apps, your own organisation slug for yours. |
version | string | Yes | Semver, e.g. 1.2.0. Used for dependency resolution and registry versioning. |
title | string or locale map | Yes | Display name in Cockpit and in the Marketplace. |
type | public | private | Yes | Who may install the app. See Private or Marketplace. |
description | string or locale map | No | Short description. Shown in the Marketplace and used as the default OpenAPI tag description. |
icon | string | No | Path to the app icon, relative to the app root. Used in the Cockpit sidebar and the Marketplace. |
name and vendor are the two fields you can never change. Together they namespace your tables ({vendor}__{app}__{entity}), prefix your event names (<vendor>.<app>.<name>), and identify the app in the registry. Choose them once.
version is the app's version, and it must move forward on every successful build. See Versioning.
title and other operator-facing text
title, description and every operator-facing string in the manifest accept either a plain string or a map of BCP-47 language tag to text:
"title": { "en": "Device Serials", "de": "Seriennummern" }
When you use the map form, en is required — the Cockpit falls back to it when its UI locale has no entry, so a half-translated manifest degrades to English rather than to a blank label. The plain-string form stays valid; the Cockpit shows it in every language.
Dependencies
"dependencies": { "revenexx/markets": "^0.1", "revenexx/orders": ">=0.6" },
"peerDependencies": { "revenexx/prices": "^1.0" }
Both declare apps yours needs. A dependencies entry is installed automatically when your app is activated for a tenant; a peerDependencies entry becomes a prompt the merchant answers.
Keys must be vendor/app. A bare app name ("markets") is rejected when the app is applied. Values are semver ranges. Full detail in Dependencies.
permissions — the access register
An app gets no access by default. Everything it may reach is declared in permissions, and the platform renders that list to the merchant at install time as the consent screen.
Each entry is discriminated by its resource key. There are nine kinds:
| Key | Grants | Status |
|---|---|---|
entity | Read/write on one of the app's own entities | Shipped |
identity | Access to the tenant's users, teams or sessions | Shipped |
capability | Permission to call another app's capability through the gateway | Shipped |
outbound | External HTTP egress to one host | Declared and honoured as intent; enforcement is still being built |
storage | File bucket access | Declarable, not yet exercised by any shipped app |
ai | Model inference | Declarable, not yet exercised by any shipped app |
mail | Transactional mail sending | Declarable, not yet exercised by any shipped app |
secret | Read access to a secret path | Declarable, not yet exercised by any shipped app |
inbound | Inbound webhook delivery from a named source | Declarable, not yet exercised by any shipped app |
"permissions": [
{ "entity": "device_serials", "access": ["read", "create", "update"] },
{ "identity": "users", "access": ["read"] },
{ "capability": "orders.get", "compatible": "^1.0" },
{ "outbound": "api.acme-service.example", "paths": ["/v2/*"] }
]
There is no policies block. Everything an app may reach is one entry in permissions. Permissions is the full reference for all nine grant kinds, including which are proven in production and which you should treat as unverified.
What the app contributes
Four blocks declare things the app adds to the platform rather than consumes from it. Each is globally unique — a second app claiming the same name is rejected at publish or apply time.
capabilities
The typed HTTP operations this app contributes. Almost always moved into manifest.capabilities.json. See Capabilities and the capability reference.
provides_scopes
Scope dimensions this app supplies to the platform's scoping engine. Declaring one makes it available platform-wide: other apps can then mark their entities scopeable by it.
"provides_scopes": [
{
"dimension": "market",
"slug_source": "markets.code",
"jwt_path": "scope_context.market",
"match_mode": "single"
}
]
You declare this only if you own a new dimension. Field reference and semantics in Scoping.
provides_permissions
Not to be confused with permissions. permissions says what this app may reach; provides_permissions declares what a person may do.
"provides_permissions": [
{
"permission": "serials.register",
"title": { "en": "Register a device serial" },
"capabilities": ["serials.create"],
"require_principal": false
}
]
Field reference in Person permissions.
provides_roles
Declares this app as the platform's role provider for one kind of principal. Exactly one provider per subject platform-wide.
"provides_roles": {
"subject": "customer_contact",
"resolve_capability": "customers.contacts.resolve"
}
See Person permissions.
tags
Optional. The groups this app's operations are published under in the OpenAPI document.
"tags": [
{ "name": "serials", "description": { "en": "Serial registry." } },
{ "name": "serials.service", "description": { "en": "Service history and warranty." } }
]
Declaring none is the normal case: every operation is grouped under its capability namespace. A tag name must be a capability namespace this app defines or a dotted refinement of one, and a capability naming an undeclared tag is rejected at publish time. See Tags.
events
Event declarations. Both directions are declarative — there is no publish call to make from your code.
"events": {
"emits": [
"serial.audited",
{
"name": "serial.registered",
"entity": "device_serials",
"on": ["insert"],
"payload": { "serial_id": "column:id", "serial": "column:serial" },
"channel": true
}
],
"listens": ["app.installed", "app.uninstalled"]
}
With entity and on present, the platform generates the trigger that publishes the event when those row operations occur. An entry that is a bare string, or an object without them, is catalog-only. Event names must be exactly two dot-separated segments.
listens names the events this app reacts to; app.installed and app.uninstalled are the two shipped apps actually subscribe to. Both directions in full: Events you emit and Events you receive.
schedules and warmup
"schedules": [
{ "name": "expire-warranties", "cron": "0 3 * * *" },
{ "name": "nightly-sync", "cron": "*/15 * * * *", "data": { "mode": "delta" } }
],
"warmup": false
Recurring invocations of your app, run by the platform scheduler: standard 5-field cron, evaluated in UTC, fired per installed tenant. A tick carries no path — only the schedule name, as X-Revenexx-Schedule — so branch on that header before your router sees the request.
warmup: true keeps the app permanently warm so requests never pay a cold start. Default false; it is not free. Both in Scheduled work.
Validation
The manifest is validated when the app is registered and again when a version is applied. additionalProperties is false at every level, so unknown keys fail rather than being silently dropped.
The failures worth knowing in advance:
- A missing required field —
name,vendor,version,titleortype. - A
dependenciesorpeerDependencieskey that is notvendor/app. - A
permissionsentry that carries more than one resource key, or a key that is not one of the nine. - An event name that is not exactly two dot segments.
- A capability
tagthat names a tag not declared in top-leveltags. - A
provides_scopesdimension,provides_permissionskey orprovides_rolessubject already claimed by another app.
Validate locally before you deploy — see Validate for the loop and for the rules that only fail at publish.
Practical guidance
Claim the least you can. permissions is read by a human at install time. An entry the app never exercises makes the consent screen scarier than it needs to be and widens your blast radius for nothing.
Treat the manifest as published documentation. Your capability summaries and descriptions are what end up in /v1/openapi.json, and from there in the SDKs, the API Explorer and the MCP server. A vague description in the manifest is a vague description everywhere.
Bump the version honestly. A breaking change is a new major, a new capability key and a new route — never an edit to an existing one. See Versioning.
Next steps
- Private or Marketplace — the
typefield. - Permissions — all nine grant kinds.
- Validate — checking the file before you ship it.
- Deploying — getting it onto the platform.