Skip to content

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-sdk SDK 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.

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. critical in 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).