The manifest
Every field of the app manifest with whether it is required, its format, and its effect, from id and version to legal, unlocks, and connects.
8 minute readApps SDKUpdated: 10/25/2026
On this page
Before you start
- For
- Developers who write a manifest or read an existing one.
- Prerequisite
- the
@szenera/apps-sdkSDK from the platform’s source tree (not publicly released).
The manifest is the one binding description of an app: defineAppManifest in the SDK, the check at load time, and the storage in the catalog all read exactly the same shape. The tables list each field with whether it is required, its format, and its effect. Examples follow below.
The fields
| Field | Required | Format | Effect |
|---|---|---|---|
id |
yes | Lowercase letters with hyphens (^[a-z][a-z0-9]*(-[a-z0-9]+)*$). core is reserved and rejected. |
Canonical identifier of the app. It must match the catalog entry. |
version |
yes | Semver (1.0.0) |
The version that a release freezes. |
displayName |
yes | 1–60 characters | Name in the store and in the portal. |
description |
yes | max. 300 characters | Short description in the store. |
category |
yes | zeit · information · web-video · gestaltung · geschaeft |
The single classification: filter in the store and section in the app area of the scene editor. Widgets do not carry one of their own. |
tier |
default standard |
standard · pro · premium · addon |
Class: standard carries no badge in the store, pro carries the Pro badge, and addon carries the Add-on badge and is booked as an add-on. No app carries premium today. |
vertical |
no | pharmacy |
Industry facet of the store. Cross-industry apps omit it. |
publisher |
no | { name: 1–80 characters } |
Publisher shown in the store listing. |
schemaName |
yes | ^apps_[a-z][a-z0-9_]+$ |
Name of the app database: apps_<id>, with the hyphens of the identifier turned into underscores (qr-code → apps_qr_code). Required even without a database. |
apiCompatibility |
yes | { appsSdk: semver range, apiVersion?: string, rendererHost?: semver range } |
appsSdk applies to the backend: the loader checks the running SDK version against it and does not load the app if it does not match. rendererHost names the host contract of the display software that the runtime and authoring bundles need, and it belongs in every manifest with a widget: the display software and the scene editor load a bundle only if their contract version falls within the range. It is set from the SDK’s RENDERER_HOST_RANGE, never by hand, so that it moves with the contract. |
widgets |
yes | List of widget definitions | The app’s widgets — by the platform’s rule at most one, and none for gate apps. How their configSchema comes out in the catalog is covered in Configure widgets. |
resources |
no | List of resource types (id, appId, slug, name, description, three JSON schemas, listEndpoint, authoringPath) |
Data types that the organization maintains in the portal. slug is the address segment of the app area. |
permissions |
yes | { tenant: [], platform: [] }, each entry with key, label, description, defaultRoles |
Permissions that the installation adds to the organization’s permission catalog. platform with empty defaultRoles is purely operator work. |
models |
no | List of model names | Allowlist of the models that the backend may access in the app database. |
critical |
default false |
boolean | For reporting only in the operations diagnostics. Whether a load error aborts startup is decided by the server configuration, not by the manifest. |
criticalForTenant |
default false |
boolean | The portal warns before uninstalling: This app is part of the core setup. |
signing |
default optional |
optional · required |
No effect on delivery: The device always checks the signature of every version. The value stays because it is in the manifests of published versions. |
preview |
yes | { iconUrl } |
The icon for the listing. The store derives the images from the widgets’ variants and serves them from the version’s artifact. It no longer reads the former screenshots field, and a new manifest omits it. |
store |
no | tagline (≤ 140), longDescription (≤ 3000), highlights (≤ 8), setupSteps (≤ 8, optionally with resourceType) |
Detailed store content. The portal only renders it. |
dataAccess |
no | { reads[], writes[], external[], retention } |
The disclosure shown before installation. If the block is missing, the most restrictive reading applies. |
unlocks |
no, max. 8 | Keys in lowercase with hyphens, ≤ 40 characters | Portal features that the installation unlocks. Every key must appear in the platform’s catalog. |
connects |
no, max. 4 | Keys in lowercase with hyphens, ≤ 40 characters | Storage providers whose connection management the app brings onto its own page. |
legal |
no | see below | Terms that trigger consent and a notice in the widget. |
compatibility |
yes (in the SDK) | { hosts, orientations, offline }, see below |
Where the app runs. The store shows it under “Runs on”, and assigning the app to a display warns if the display’s host is missing. Optional in the format so that manifests of earlier versions stay readable. |
translations |
no (for the bundled apps: en-US required) |
one block of texts per language, see below | The texts in further languages. Without a block, the store shows German. |
unlocks and connects are omitted rather than set empty: an empty field would shift the canonical hash of every existing version although nothing has changed.
The compatibility block
defineAppManifest requires the block both in the type and at runtime. Without it, the function throws with a sentence that names the three fields. Each list names a value at most once.
| Field | Values | Meaning |
|---|---|---|
hosts |
szenera-os · fire-tv · tizen · webos · web-display, at least one |
The hosts the app runs on: SzeneraOS on the platform’s own device, the app for Fire TV and Android, the apps for Samsung Tizen and LG webOS, and a browser without a host of its own. |
orientations |
landscape · portrait, at least one |
The orientations of the display, landscape and portrait, for which the app is suited. |
offline |
full · last-known · none |
What the app shows without a network. full keeps showing everything because the app renders solely from its configuration and the device’s local stock (“static” in the store). last-known shows the state that the server last put into the display’s snapshot. none shows nothing because the app loads live from the network. |
The translations block
The texts of the manifest are written in German, the required language. Each additional language
lives under translations as its own block, which carries texts only: displayName and
description, store (tagline, long text, features, and setup steps in the order of the German
ones), widgets by identifier with name, description, and variants by view, resources by
identifier, permissions by key, dataAccess.external in the order of the services, and
legal.disclaimerSummary. Identifiers, values, and rules never appear in a block.
A block is either complete or absent: defineAppManifest checks it with
appManifestTranslationIssues() and throws at build time, naming the location. The store answers
in the language that the portal requests and falls back to German. English terms follow the portals:
display, never screen, and the video wall is called video wall. pnpm app:check <id> checks
the texts of both languages against the house rule, in the Texte line.
The legal block
| Field | Required | Meaning |
|---|---|---|
disclaimerKey |
yes | Key of the version that a consent refers to (<group>.bedingungen.v1). A change to the substance of the notice bumps it (v1 → v2). |
disclaimerSummary |
yes, max. 500 characters | Short summary in plain text. The installation dialog, the store page, and the widget footer show it unchanged. It represents the version, and rewording it under the same key does not require consent. |
termsPath |
no | Path of the full text on the public website (^\/[a-z0-9-]+(\/[a-z0-9-]+)*$, ≤ 200 characters, e.g., /app-bedingungen/notdienst). A path, not an address — the portal prepends its base. |
requireAcceptanceAt |
default install |
install: consent at installation and afterward with every new version of the notice. upgrade: consent with every new version of the app, even if the notice is unchanged. never: no consent. |
A new disclaimerKey makes a release subject to consent: as soon as the new version is published as stable, the installation moves to Update waiting. Anyone who changes the substance of the notice therefore bumps the key, in the same pass as the short summary, the full text, and the app version. The platform silently adopts a reworded short summary under the same key. A manifest change without a version bump does not reach existing installations.
What is not in the manifest
- Fetchers and jobs. An app declares them as decorators on classes in its backend (
@Fetcher,@JobHandler,@WidgetResolver), and the platform discovers them at startup. The manifest remains a pure data format. - Whether a load error aborts startup. That is a property of the installation and lives in the server configuration of the operations side.
criticalin the manifest is only a report.
Example: the manifest of the Pharmacies on duty app (abridged)
import { defineAppManifest, RENDERER_HOST_RANGE } from '@szenera/apps-sdk/manifest';
export const manifest = defineAppManifest({
id: 'pharmacies-on-duty',
version: '1.3.0',
displayName: 'Notdienst',
description:
'Zeigt die Apotheken im Notdienst rund um Ihren Standort, regelmäßig aktualisiert für ganz Deutschland.',
tier: 'pro',
category: 'geschaeft',
vertical: 'pharmacy',
schemaName: 'apps_pharmacies_on_duty',
apiCompatibility: { appsSdk: '^0.2.0', rendererHost: RENDERER_HOST_RANGE },
widgets: [PharmaciesOnDutyDisplayWidget],
resources: [
{ ...PharmaciesOnDutyLocationResource, slug: 'locations' },
{ ...PharmaciesOnDutyScheduleResource, slug: 'schedules' },
],
permissions: {
tenant: [
{
key: 'pharmacies-on-duty.location.read',
label: 'Standorte lesen',
description: 'Hinterlegte Apotheken-Standorte ansehen, für die Notdienstpläne geladen werden.',
defaultRoles: ['OWNER', 'MANAGER', 'MEMBER'],
},
// … further permissions for the team …
],
platform: [
{
key: 'pharmacies-on-duty.crawler.config',
label: 'Crawler konfigurieren',
description: 'Betreiber-Recht: Quellen, Abrufintervalle und Parser des Notdienst-Crawlers einstellen.',
defaultRoles: [],
},
],
},
publisher: { name: 'Szenera' },
signing: 'required',
legal: {
disclaimerKey: 'notdienst.bedingungen.v1',
disclaimerSummary: 'Notdienstangaben stammen aus öffentlichen Verzeichnissen … und werden ohne Gewähr angezeigt …',
termsPath: '/app-bedingungen/notdienst',
requireAcceptanceAt: 'install',
},
preview: { iconUrl: '/apps/pharmacies-on-duty/icon.svg' },
dataAccess: {
external: [
{
name: 'Öffentliche Notdienst-Verzeichnisse (u. a. aponet.de)',
purpose: 'Wir rufen die Dienstpläne für Ihre Standorte ab und halten sie aktuell.',
sends: 'Postleitzahl und Umkreis der Standorte, die Sie angelegt haben.',
},
],
},
// The server fetches the duty schedules, and the display keeps the last state in its snapshot.
compatibility: {
hosts: ['szenera-os', 'fire-tv', 'tizen', 'webos', 'web-display'],
orientations: ['landscape', 'portrait'],
offline: 'last-known',
},
});
Form validation and cross-field rules
defineAppManifest validates the shape against the schema, applies default values (tier, critical, criticalForTenant, signing), requires the compatibility block, and throws on every shape error. Three rules that arise from several fields at once are checked only by the platform at load time: the appId of each widget must equal the id of the manifest, each resource must carry the app’s prefix, and no two widgets may share an identifier. A manifest that contradicts the schema is rejected by release creation with Manifest.ValidationFailed (HTTP 422).
Related
- Widget, configuration, and preview — the widget definition in the
widgetsfield. - Data access, permissions, and terms —
dataAccess,permissions,unlocks,connects, andlegalin detail. - Signing and the path into the catalog — what follows from
signingat release and on the device.