Manifest Reference

Field-by-field reference for manifest.json — identity, app type, dependencies, the access register, what the app contributes to the platform, and what activates it.

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

manifest.json
{
  "$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

FieldTypeRequiredDescription
$schemastringNohttps://schemas.revenexx.com/manifest.schema.json. Editor validation only.
namestringYesApp identifier. Lowercase, alphanumeric, hyphens. Becomes the app's route namespace and part of its table prefix.
vendorstringYesPublisher slug. revenexx for platform apps, your own organisation slug for yours.
versionstringYesSemver, e.g. 1.2.0. Used for dependency resolution and registry versioning.
titlestring or locale mapYesDisplay name in Cockpit and in the Marketplace.
typepublic | privateYesWho may install the app. See Private or Marketplace.
descriptionstring or locale mapNoShort description. Shown in the Marketplace and used as the default OpenAPI tag description.
iconstringNoPath 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:

manifest.json
"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

manifest.json
"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:

KeyGrantsStatus
entityRead/write on one of the app's own entitiesShipped
identityAccess to the tenant's users, teams or sessionsShipped
capabilityPermission to call another app's capability through the gatewayShipped
outboundExternal HTTP egress to one hostDeclared and honoured as intent; enforcement is still being built
storageFile bucket accessDeclarable, not yet exercised by any shipped app
aiModel inferenceDeclarable, not yet exercised by any shipped app
mailTransactional mail sendingDeclarable, not yet exercised by any shipped app
secretRead access to a secret pathDeclarable, not yet exercised by any shipped app
inboundInbound webhook delivery from a named sourceDeclarable, not yet exercised by any shipped app
manifest.json
"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.

manifest.json
"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.

manifest.json
"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.

manifest.json
"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.

manifest.json
"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.

manifest.json
"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

manifest.json
"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, title or type.
  • A dependencies or peerDependencies key that is not vendor/app.
  • A permissions entry 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 tag that names a tag not declared in top-level tags.
  • A provides_scopes dimension, provides_permissions key or provides_roles subject 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

Was this page helpful?