Manifest Reference

Complete manifest.json specification and field reference

This is the field-by-field reference for manifest.json — for app developers writing or reviewing a manifest. If you're new to apps and haven't seen a complete manifest in context yet, read App Model Concept first; the example there shows how manifest.json, schema.json, and cockpit.json fit together.

manifest.json is the contract between your app and the platform. It answers four questions:

  • Identity — who built this, what's it called, what version is this?
  • Dependencies — which other apps does this one depend on, and which ones does it expect (peer dependencies) without auto-installing?
  • Permissions — which platform capabilities does this app need access to? The platform's default is deny-all; you opt in here.
  • Events — which events does this app publish, and which does it subscribe to?

The platform validates manifest.json against a schema at deploy time. A manifest that requests permissions it shouldn't, depends on apps that don't exist, or declares events with the wrong shape will fail deploy with a clear error. You can validate locally too with the CLI.

Complete Example

Before the field-by-field breakdown, here's a full manifest for the Orders app, with every section populated. It's worth reading once as a whole — the rest of the page pulls these pieces apart one at a time, and it's easier to follow each in isolation once you've seen how they sit together.

JSON
{
  "$schema": "https://revenexx.com/schemas/manifest.schema.json",
  "name": "orders",
  "vendor": "revenexx",
  "version": "1.0.0",
  "title": "Orders",
  "description": "Manage B2B orders, checkout, and order lifecycle",
  "dependencies": {
    "customers": "^1.0",
    "pricing": "^1.0",
    "inventory": "^2.0"
  },
  "peerDependencies": {
    "markets": "^1.0"
  },
  "permissions": [
    { "entity": "orders", "access": ["read", "create", "update", "delete"] },
    { "entity": "order_items", "access": ["read", "create", "delete"] },
    { "entity": "customers", "access": ["read"] }
  ],
  "policies": [
    {
      "name": "outbound-access",
      "attrs": { "host": "{{tenant_domain}}", "path": "/api/erp/*" }
    },
    {
      "name": "event-publish",
      "attrs": { "events": ["order.created", "order.status_changed", "order.cancelled"] }
    },
    {
      "name": "event-subscribe",
      "attrs": { "events": ["inventory.stock_changed", "pricing.updated"] }
    },
    {
      "name": "storage-read-write",
      "attrs": { "bucket": "order-attachments" }
    }
  ],
  "events": {
    "emits": ["order.created", "order.status_changed", "order.cancelled"],
    "listens": ["inventory.stock_changed", "app.installed", "app.upgraded"]
  }
}

Field Reference

Basic Metadata

FieldTypeRequiredDescription
$schemastringYesJSON Schema reference. Always: https://revenexx.com/schemas/manifest.schema.json
namestringYesUnique app identifier (kebab-case, lowercase, no spaces)
vendorstringYesApp publisher. Use revenexx for core apps, your company name for partners
versionstringYesSemantic version (e.g., 1.0.0, 2.3.1). Increment for every deploy.
titlestringYesHuman-readable display name (e.g., "Orders", "Payment Gateway")
descriptionstringNoLong-form description (2–3 sentences)

Dependencies

Your app rarely stands alone — it reads customers, prices, inventory. The manifest declares those relationships in two blocks that differ only in who decides to install them.

dependencies

Anything in dependencies is installed automatically the moment your app is activated; the customer never sees a choice. Each entry pairs an app name with a version constraint:

JSON
"dependencies": {
  "customers": "^1.0",      // ✓ Auto-installed
  "pricing": "^2.0"         // ✓ Auto-installed
}

This is the right block for apps yours genuinely can't function without — the data dependencies and shared services you call directly. The version constraint follows the usual SemVer operators:

  • ^1.0.0 — Compatible with 1.x.x (default, recommended)
  • ~1.0.0 — Only patch updates (1.0.x)
  • 1.0.0 — Exact version only
  • >=1.0.0 — 1.0.0 or newer

In almost all cases you want ^, which accepts new features and fixes but stops at the next major version, where breaking changes are allowed to land.

peerDependencies

A peer dependency is also required, but it is not installed for the customer — Console prompts them to install it themselves. Use it when the dependency carries a cost or a choice the customer should own, such as a paid app or an optional capability:

JSON
"peerDependencies": {
  "markets": "^1.0"         // Customer prompted to install
}

The distinction matters at install time: a missing plain dependency is solved silently, while a missing peer dependency turns into a question the customer answers.

Permissions

Where dependencies say which apps you rely on, permissions say which data you touch and what you may do with it. Each entry names an entity and the access levels you need on it:

JSON
"permissions": [
  { "entity": "orders", "access": ["read", "create", "update", "delete"] },
  { "entity": "order_items", "access": ["read", "create"] },
  { "entity": "customers", "access": ["read"] }
]

The four access levels map directly onto the SQL operations the platform will allow:

Access LevelWhat It Allows
readSELECT queries on the entity
createINSERT rows
updateUPDATE rows
deleteDELETE rows

Because the default is no access at all, this list is also an honest description of your app's data footprint — so grant only what the app actually uses. Note the asymmetry in the example: the Orders app fully owns orders, can add items but never delete an order's history wholesale, and can only read customers, which belongs to another app.

Policies (the allowlist)

Permissions cover your own data; policies cover everything else an app might reach for — the network, the event bus, file storage. All of it is denied until a policy opens it, and each policy is as narrow as you can make it.

outbound-access

This policy lets your app make HTTP calls to an external host, such as a customer's ERP or PIM. Crucially, it doesn't open the whole internet — you pin both the host and a path prefix, and the app can reach nothing outside that window:

JSON
{
  "name": "outbound-access",
  "attrs": {
    "host": "{{tenant_domain}}",  // Template variable
    "path": "/api/erp/*"           // Wildcard supported
  }
}

The host can be a template variable or a literal. {{tenant_domain}} resolves to the customer's own primary domain (e.g. acme.com), which is what you want when each tenant talks to its own systems; for a shared third party you'd write the literal domain instead, such as api.stripe.com. The path accepts wildcards — /api/erp/* matches anything one level under /api/erp/, and /api/v1/** matches nested paths as well.

event-publish

This grants the right to emit specific events onto the platform event bus. You list exactly the events your app is allowed to publish, and the bus rejects anything not declared here:

JSON
{
  "name": "event-publish",
  "attrs": { "events": ["order.created", "order.status_changed"] }
}

event-subscribe

The mirror image: permission to receive events. It's almost always paired with the matching events.listens declaration further down — this policy authorizes the subscription, while listens is what actually wires the handler up:

JSON
{
  "name": "event-subscribe",
  "attrs": { "events": ["inventory.stock_changed"] }
}

storage-read / storage-write / storage-read-write

These grant access to a file-storage bucket, in increasing degrees: read-only, write-only, or both. Scope it to the specific bucket your app uses rather than reaching for the wildcard:

JSON
{
  "name": "storage-read-write",
  "attrs": { "bucket": "order-attachments" }  // Or "*" for all buckets
}

Events

The policies block above decides whether your app may publish or subscribe; the events block decides which events, and it's what the platform reads to actually connect publishers to subscribers.

events.emits

emits lists the events your app publishes. These names are effectively your app's public API to the rest of the platform — other apps subscribe to them by name, so treat them as a contract you don't break casually:

JSON
"events": {
  "emits": ["order.created", "order.status_changed", "order.cancelled"]
}

events.listens

listens lists the events your app reacts to. These can be events emitted by other apps, or the lifecycle events the platform itself emits:

JSON
"events": {
  "listens": ["inventory.stock_changed", "app.installed", "app.upgraded"]
}

The platform-emitted lifecycle events are worth knowing, since most apps subscribe to at least one of them:

  • app.installed — This app was activated for a Tenant
  • app.uninstalled — This app was deactivated
  • app.upgraded — This app was deployed with a new version
  • tenant.provisioned — A new Tenant was created

Validation

You won't discover a malformed manifest in production, because the platform checks it at three points along the way: when you register the app with the revenexx CLI (revenexx apps register --manifest manifest.json), again when a deployment is created, and a final time when a customer installs it. Each gate catches problems earlier than the last.

An invalid manifest blocks deployment outright rather than failing quietly later. The usual culprits are a missing required field (name, vendor, version, or title), a dependency or peer dependency that names an app the platform can't find, a permission on an entity that isn't defined in schema.json, or a policy whose attributes are malformed. In every case the error names the specific problem, so the fix is usually obvious.

Best Practices

A good manifest is a small one — it claims the least it can and says plainly what it does. A few habits get you there.

Name the app for what it is. Keep names singular and descriptive (orders, not order_system), and use the description field to explain anything non-obvious, especially unusual permission requirements a reviewer might otherwise question.

Declare exactly what you use, and nothing more. List only the permissions the app actually exercises and only the dependencies it genuinely calls. Requesting unused access erodes the principle of least privilege and makes the install-time consent screen scarier than it needs to be; declaring phantom dependencies drags in apps the customer didn't need.

Stay narrow with policies and flexible with versions. Avoid wildcards like host: "*" or bucket: "*" unless you can articulate why no tighter scope works. And prefer caret ranges (^1.0.0) over exact pins (1.0.0) for dependencies, so a routine patch in an app you depend on doesn't strand you on a version that no longer exists.

Next Steps

Was this page helpful?