Zum Inhalt springen

Datenzugriff, Rechte und Bedingungen

dataAccess (reads, external, retention), Rechte für Team und Betreiber, unlocks und connects, der legal-Block: was eine App vor der Installation offenlegt.

16 Minuten LesezeitApps-SDKStand: 25.10.2026

Auf dieser Seite

Bevor Sie loslegen

Für wen
Entwickler, die eine App so beschreiben, dass der Installationsdialog die richtige Auskunft gibt.
Voraussetzung
ein Manifest nach Das Manifest; das SDK @szenera/apps-sdk aus dem Quellbaum der Plattform (nicht öffentlich veröffentlicht). Für die beiden Abfragen am Ende genügt ein Ausweis Ihrer Organisation.

Vier Blöcke des Manifests legen offen, was eine App mit Daten tut, wer sie im Team bedienen darf, was sie im Portal aufsperrt und welchen Bedingungen eine Organisation zustimmt: dataAccess, permissions, unlocks mit connects und legal. Die Tabellen nennen je Block Format, Grenzwerte und die Wirkung im Portal.

Vier Blöcke, vier Fragen

Block Frage der Organisation Wo das Portal es zeigt
dataAccess Was tut die App mit meinen Daten — vor der Installation und danach? Abschnitt Daten & Zugriff der Store-Seite und der Installationsdialog
permissions Wer in meinem Team darf die App bedienen? Rollen & Rechte — eingeklappt am Ende der Store-Seite, ausgeschrieben im Installationsdialog
unlocks, connects Was schaltet die App im Portal frei, welches fremde Konto braucht sie? Schaltet im Portal frei
legal Welchen Bedingungen stimme ich zu? Wichtiger Hinweis im Installationsdialog, Store-Seite, Fußzeile des Widgets

Die Blöcke sind bewusst getrennt: permissions beschreibt, wer im Team die App bedienen darf — nicht, worauf die App zugreift. Das Portal sagt es der Organisation bei den Rechten der App: „Sie steuern, wer im Team die App bedienen darf.“ Worauf die App zugreift, steht getrennt unter Daten & Zugriff.

dataAccess

Fehlt der Block, gilt die zurückhaltendste Lesart: Die App liest nichts, ruft nichts ab und behält ihre eigenen Daten beim Deinstallieren. Er ist deshalb nur dort nötig, wo es etwas zu sagen gibt.

Feld Format Bedeutung Beschriftung im Portal
reads Liste aus tenant-profile, tenant-branding, media-library, team, player-location, fleet-locations, location-hours; Vorgabe leer Bestehende Bestände der Organisation, die die App liest. Liest vorhandene Daten — je Bestand Stammdaten der Organisation, Logo der Organisation, Mediathek, Team und Konten, Standort der Displays, Alle Standorte mit Anschrift und Lage, Öffnungszeiten der Standorte mit Feiertagen
writes Liste, max. 8 Einträge, je Eintrag <bestand>:create-<sache>, ≤ 60 Zeichen; Vorgabe leer Schreib-Fähigkeiten auf fremde Bestände. Ändern und Löschen gibt es nicht; eine App kann nur beitragen. Fügt Daten hinzu — „Ändern oder Löschen ist ausgeschlossen. Eine App kann nur beitragen.“
external Liste, max. 8 Einträge mit name (1–60), purpose (1–200) und optional sends (≤ 200) Fremde Dienste, die die App erreicht: wen, wozu, und was dabei hinausgeht. Verbindungen nach außen — „Übertragen wird: …“ oder „Es werden keine Daten Ihrer Organisation übertragen.“
retention kept oder deleted; Vorgabe kept Verbleib der App-eigenen Daten beim Deinstallieren. Beim Deinstallieren — mit den Namen der resources: „Standorte und Dienstpläne bleiben erhalten. Sie können sie danach endgültig löschen lassen.“ oder „Mit der Deinstallation werden … gelöscht.“, ohne resources „Diese App hinterlässt keine eigenen Daten.“

Eine fünfte Karte steht im Portal-Block, kommt aber nicht aus diesem Block. Unter Daten & Zugriff führt das Portal vor den vier Karten die resources des Manifests auf — die eigenen Datenbereiche, die die App für die Organisation anlegt, beschriftet mit Legt für Sie an (siehe Das Manifest). Die Trennung ist gewollt: resources sagt, was hinzukommt, dataAccess sagt, was mit Vorhandenem geschieht. Die Handlungskarte des Store-Eintrags zählt die Bereiche als Eigene Daten.

Zwei Regeln verbinden den Block mit dem Code:

  • Kein Zugang ohne Deklaration. Nur wenn reads einen Bestand nennt, bindet die Plattform das Token APP_TENANT_DATA an den Server-Teil (siehe Server-Teil: Resolver, Fetcher und Jobs). Bezieht eine Klasse das Token, ohne dass das Manifest einen Bestand nennt, bricht der Start mit Klartext ab und nennt die fehlende Zeile. Auch player-location bindet das Token: Der Standort DES Displays kommt zwar im Kontext des Widget-Resolvers (siehe unten), ein fest gewählter Standort aber über location() dieses Tokens. location-hours ohne player-location oder fleet-locations bricht den Start ebenfalls ab: Öffnungszeiten hängen an einem Standort, den die App dann nicht sähe.
  • Keine erfundene Schreib-Fähigkeit. Jeder Eintrag unter writes muss im Katalog der Plattform stehen. Der Katalog ist heute leer; die Plattform weist damit jede erklärte Schreib-Fähigkeit ab — und das ist die richtige Auskunft: Es gibt keine.

Beispiel aus der Wetter-App: Sie liest die Stammdaten der Organisation (für die Sprache, in der Wetterlage und Wochentage beschriftet werden) und den Standort des Displays und erreicht einen fremden Dienst. Der Ort kommt aus der Widget-Konfiguration, ein fester Ort oder der Standort des Displays.

dataAccess: {
  reads: ['tenant-profile', 'player-location'],
  external: [
    {
      name: 'OpenWeather',
      purpose: 'Wir holen Wetterlage und Vorhersage für den gewählten Ort oder den Standort des Displays.',
      sends: 'Nur die auf etwa einen Kilometer gerundeten Koordinaten des gewählten Orts oder des Standorts, an dem das Display steht.',
    },
  ],
},

Wer eine Adresse abruft, die ein Nutzer im Portal eingetragen hat, beschreibt sie als Dienst mit sprechendem Namen — die RSS-App nennt „Die eingetragene Feed-Adresse“, die Kalender-App „Die eingetragene ICS-Adresse“ — und ruft sie im Server-Teil nur über APP_GUARDED_FETCH ab.

Der Standort des Displays: player-location

Eine Szene läuft oft in allen Filialen einer Organisation. Mit player-location zeigt ein Widget an jedem Display dessen Ort, etwa das Wetter vor der Tür oder die Ortszeit, ohne dass jemand eine Szene je Filiale pflegt. Der Standort ist das, was die Organisation im Portal am Display als Standort hinterlegt hat.

Er kommt nicht über APP_TENANT_DATA, sondern im Kontext des Widget-Resolvers als ctx.location, weil nur dort feststeht, für welches Display gerade aufgelöst wird:

Feld Inhalt
id Kennung des Standorts (loc_…), etwa um ihn mit Daten der App zu verbinden
name Name des Standorts, wie die Organisation ihn führt („Filiale Altona“)
city, countryCode Ort und Ländercode (ISO 3166-1, DE) der Anschrift, je null ohne Anschrift. Straße und Postleitzahl werden nie übergeben.
latitude, longitude Koordinaten des Standorts, gemeinsam null ohne Koordinaten
timeZone Zeitzone des Standorts (Europe/Berlin)
hours nur mit location-hours, siehe Öffnungszeiten

Die Koordinaten stammen aus der Geokodierung der Anschrift und sind gebäudegenau. Wer sie an einen fremden Dienst schickt, rundet sie auf das, was der Zweck braucht, und sagt es in sends und in den Bedingungen: Die Wetter-App schickt zwei Nachkommastellen, etwa einen Kilometer, dieselbe Zelle wie ihr Cache-Schlüssel.

Drei Zustände muss ein Resolver unterscheiden:

ctx.location Wann
fehlt Die App nennt player-location nicht unter reads, oder die Organisation hat dem Bereich mit der installierten Fassung noch nicht zugestimmt. Nur wer liest, bekommt.
null Die App darf lesen, aber es gibt keinen Standort: Kein Display ist im Spiel (Vorschau im Editor ohne gewähltes Display) oder das Display hängt an keinem Standort.
Objekt Display mit Standort, auch ohne Anschrift und ohne Koordinaten. Wer einen Ort nachschlägt, prüft latitude und behandelt einen Standort ohne Koordinaten wie null.

Kommt player-location mit einer neuen Fassung hinzu, verlangt die Fassung wie jeder neue Datenzugriff die Zustimmung der Organisation (Update wartet). Bis zur Annahme fehlt ctx.location.

Für null braucht das Widget einen Ausweg, etwa einen Ersatzort aus seiner Konfiguration oder einen Platzhalter mit Hinweis, nie ein leeres Widget. Das Wetter nimmt den Ersatzort auch für einen Standort ohne Koordinaten.

Einen fest gewählten Standort der eigenen Organisation, etwa einen Ersatz-Standort für Displays ohne Standort, liest der Server-Teil mit APP_TENANT_DATA.location(tenantId, locationId) in derselben Form. Eine Kennung einer anderen Organisation ergibt null. Die Vorschau im Editor löst für ein bestimmtes Display auf, wenn sie playerId mitschickt (POST /v1/widget-data/preview). Im Test setzt mockWidgetResolverContext({ location: mockWidgetLocation() }) aus @szenera/apps-sdk/testing einen Standort, { location: null } den leeren Fall.

Öffnungszeiten: location-hours

Öffnungszeiten sind Stammdaten des Standorts. Die Organisation pflegt sie im Portal am Standort (Reiter Öffnungszeiten), über PATCH /v1/locations/{id} oder das MCP-Werkzeug set_location_opening_hours, und jede App, die sie zeigt, liest dieselben. Der Bereich ist von player-location getrennt, weil Öffnungszeiten keine Ortsangabe sind: Eine App, die nur den Ort braucht, bekommt sie nicht, und eine eigene Zustimmung bleibt verständlich.

Mit location-hours trägt jeder Standort, den die App liest (ctx.location und location(), mit fleet-locations auch locations()), zusätzlich hours:

Feld Inhalt
region Bundesland nach ISO 3166-2 (DE-HH) oder null. Ohne Bundesland gibt es keine Feiertage.
weekly Wochenplan, je Wochentag (mon … sun) die Fenster { opens, closes }. Liegt closes nicht nach opens, endet das Fenster am Folgetag, gleiche Uhrzeiten heißen 24 Stunden.
specialDays die Sondertermine, die den Zeitraum berühren, { startDate, endDate, label, closed, windows }
holidays die gesetzlichen Feiertage im Zeitraum, { date, id, name }; id ist stabil (easter-monday), damit eine Anzeige den Namen in ihrer Sprache nennen kann
from, until der Zeitraum: 21 Tage ab dem heutigen Tag in der Zone des Standorts

hours fehlt ohne den Bereich oder ohne Zustimmung, null heißt: Die App darf lesen, aber am Standort sind keine Zeiten gepflegt. Was an einem Tag gilt, rechnet openingDayOf aus @szenera/contracts/fleet mit der Rangfolge Sondertermin vor Feiertag (geschlossen) vor Wochenplan. Ändert die Organisation die Zeiten, das Bundesland oder die Zeitzone, baut die Plattform die Displays des Standorts neu. Den Tageswechsel und das Ende eines Fensters trägt die App selbst über validUntil ihres Resolvers.

dataAccess: {
  reads: ['player-location', 'location-hours'],
},

Im Test setzt mockWidgetLocation({ hours: … }) die Zeiten, mockWidgetLocation({ hours: null }) den Standort ohne Zeiten.

Alle Standorte: fleet-locations

Ein Abruf, der vorher wissen muss, für welche Orte er Daten holt, liest das Verzeichnis der Standorte: APP_TENANT_DATA.locations(tenantId) liefert alle Standorte der Organisation nach Namen geordnet, mit voller Anschrift (street, postalCode, city, countryCode), Koordinaten und Zeitzone, mit location-hours samt hours. Ein Resolver darf nicht nach außen, also braucht ein Job die Orte vorab, der Notdienst etwa die Postleitzahlen. Wer die Anschrift an einen fremden Dienst schickt, nennt in dataAccess.external genau das, was hinausgeht.

Im Test liefert createMockAppContainer({ locationsMock: [...] }) die Standorte für location() und locations().

permissions

Die Installation trägt jedes Recht in den Rechte-Katalog der Organisation ein. Zwei Listen, dieselbe Form:

Liste Wer sie trägt defaultRoles
tenant Das Team der Organisation Rollen, die das Recht mit der Installation erhalten, z. B. ['OWNER', 'MANAGER', 'MEMBER']
platform Der Plattformbetrieb leer ([]) — kein Teammitglied erhält das Recht

Jeder Eintrag ist ein Quartett:

Feld Format Bedeutung
key <ns>.<verb> oder <ns>.<resource>.<verb> (^[a-z][a-z0-9-]*(\.[a-z][a-z0-9-]*)+$), z. B. pharmacies-on-duty.location.read Der Schlüssel, den der Server-Teil über APP_PERMISSION_GUARD prüft; die Plattform stellt ihm apps:<id>: voran.
label Text, mindestens 1 Zeichen Beschriftung im Rechte-Katalog und im Installationsdialog.
description Text, mindestens 1 Zeichen Ein Satz, was das Recht erlaubt.
defaultRoles Liste von Rollen Startbelegung; die Organisation kann sie im Team-Bereich ändern.

Der Helfer definePermission(key) prüft nur das Format eines Schlüssels und gibt ihn unverändert zurück; die mitgelieferten Apps schreiben die Quartette direkt ins Manifest.

Neue Rechte in einer neuen Fassung übernimmt die Plattform nicht still: Sobald die Fassung als stable veröffentlicht ist, steht die Installation auf Update wartet, die Widgets der App lassen sich im Szenen-Editor nicht einfügen, und der Reiter Update der App zeigt die hinzugekommenen Rechte mit Annehmen und Ablehnen. Ein Aufruf, der die App vorher nutzt, scheitert mit Grant.PendingReacceptance (HTTP 409); eine Zustimmung zu einer anderen Manifest-Fassung mit App.ManifestHashMismatch (HTTP 409).

unlocks und connects

Eine App muss keine Fläche mitbringen. unlocks und connects beschreiben den zweiten Sinn einer App: nicht etwas liefern, sondern etwas aufsperren. Die Funktion lebt in der Plattform; die App ist das Tor davor.

Feld Format Katalog der Plattform Löst Zustimmung aus?
unlocks max. 8 Schlüssel, kleingeschrieben mit Bindestrichen, ≤ 40 Zeichen video-walls (Leinwände), library-places (Orte in der Mediathek) Nein — die Zeile gibt der Organisation etwas, sie nimmt ihr nichts und liest nichts von ihr.
connects max. 4 Schlüssel, kleingeschrieben mit Bindestrichen, ≤ 40 Zeichen heute microsoft-graph Die Verbindung selbst stellt die Organisation im Portal her; Erwerb, Erneuerung und Widerruf der Vollmacht liegen einmal in der Plattform. Die App sieht weder Vollmacht noch Zugriffs-Wort.

Ein Schlüssel, der nicht im Katalog steht, lässt die App nicht laden — sonst könnte eine App eine Funktion oder Anbindung versprechen, hinter der nichts steht. Beide Felder werden ausgelassen statt leer gesetzt: Ein leeres Feld verschöbe den kanonischen Hash jeder bestehenden Fassung.

Die beiden Tor-Apps zeigen die Muster: video-wall trägt unlocks: ['video-walls'], onedrive trägt unlocks: ['library-places'] und connects: ['microsoft-graph']. Der Store-Eintrag zeigt die Freischaltung im Reiter Daten & Zugriff unter Schaltet im Portal frei. Eine App, die nur eine Kernfunktion der Plattform aufsperrt, steht im Store nicht als App: video-wall erscheint dort als Kachel Kernfunktion · Leinwand (coreFeatures der Übersicht) und fehlt in „Meine Apps“.

Feld Pflicht Bedeutung
disclaimerKey ja Schlüssel der Fassung, auf die sich eine Zustimmung bezieht (<gruppe>.bedingungen.v1). Eine inhaltliche Änderung des Hinweises hebt ihn (v1 → v2).
disclaimerSummary ja, max. 500 Zeichen Kurzfassung in Klartext; Installationsdialog (Wichtiger Hinweis), Store-Eintrag und Widget-Fußzeile zeigen sie unverändert. Sie stellt die Fassung dar, eine neue Formulierung bei gleichem Schlüssel verlangt keine Zustimmung.
termsPath nein Pfad des Volltexts auf der öffentlichen Website, ≤ 200 Zeichen, nur Kleinbuchstaben, Ziffern, Bindestriche und Schrägstriche (/app-bedingungen/<gruppe>). Das Portal setzt seine Basis davor und verlinkt ihn als Vollständige Bedingungen der App. Ohne termsPath zeigt der Dialog allein die Kurzfassung.
requireAcceptanceAt Vorgabe install install: Zustimmung bei der Installation und danach mit jeder neuen Fassung des Hinweises. upgrade: Zustimmung bei jeder neuen Fassung der App, auch bei gleichem Hinweis. never: keine Zustimmung. Die mitgelieferten Apps tragen install.

Apps mit gleichem Rechtsthema zeigen auf dasselbe Blatt. Die mitgelieferten Apps verwenden sieben Gruppen:

Gruppe (termsPath) Apps
/app-bedingungen/fremdinhalte webpage, youtube, stream, embed
/app-bedingungen/eigene-inhalte notice-board, ticker, qr-code, menu-board
/app-bedingungen/wetter weather
/app-bedingungen/nachrichten rss, tagesschau, bbc, bild, ndr, spiegel, deutschlandfunk
/app-bedingungen/verbindungen calendar, onedrive
/app-bedingungen/notdienst pharmacies-on-duty
/app-bedingungen/oeffnungszeiten opening-hours

Ohne legal bleiben clock, countdown, countup, world-clock, word-clock und video-wall. Die Öffnungszeiten zeigen zwar eigene Eingaben der Organisation, die Feiertage rechnet aber die Plattform, und dazu sagen ihre Bedingungen etwas.

Eine neue Fassung der Bedingungen — ein anderer disclaimerKey — macht ein Release zustimmungspflichtig: Sobald die neue Fassung als stable veröffentlicht ist, steht die Installation auf Update wartet, genau wie bei neuen Rechten. Wer den Hinweis inhaltlich ändert, hebt deshalb den Schlüssel, im selben Arbeitsgang wie Kurzfassung, Volltext und Fassung der App. Eine neu formulierte Kurzfassung bei gleichem Schlüssel übernimmt die Plattform still. Eine Manifest-Änderung ohne Versionssprung erreicht bestehende Installationen nicht.

Den Stand einer Installation abfragen

Was aus Manifest, Rechten und Bedingungen geworden ist, liest eine Organisation über zwei Routen ihres eigenen Bestands. Beide verlangen einen Ausweis der Organisation, aber kein eigenes Recht: Eine gültige Anmeldung genügt, und ein API-Schlüssel erreicht sie unabhängig davon, welche Rechte in seinem scope stehen. Ein Betreiber-Ausweis erreicht sie nicht — beide antworten immer über genau eine Organisation und weisen einen Ausweis ohne Organisation mit 403 Permission.Denied ab.

Route Was die Antwort enthält
GET /v1/me/apps Unter apps je Installation eine Zeile: grantId (Kennung tag_…), appId, installedVersion, installedAt, dazu die Anzeigefelder der installierten Fassung (displayName, tagline, iconUrl, coverUrl, category, tier, bundleSizeBytes), die Datenbereiche als resources und sceneCount — in wie vielen Szenen die App tatsächlich steckt. Drei Zustände stehen nebeneinander: status (active oder pending-reacceptance), platformStatus (active, beta, disabled) und effect mit status (active, locked) und reason (ok, pro-required, premium-required, booking-required, exempt). Deinstallierte Apps fehlen in der Liste, abgeschaltete und nicht mehr im Paket enthaltene bleiben darin stehen, sie sind Bestand. Name, Einzeiler und Datenbereiche stehen in der Sprache aus locale oder Accept-Language, wo die Fassung sie trägt.
GET /v1/me/apps/{appId}/installed Die Seite der installierten App: entry beschreibt die installierte Fassung, scenes die Szenen, die sie verwenden, und history die Installation in zwei Blöcken. history.grant trägt status, installedVersion, acceptedManifestHash, pendingManifestHash, eine Ablehnung mit Fassung und Zeitpunkt (declinedVersion, declinedAt) und die Konten hinter Installation, Zustimmung und Deinstallation. history.acceptanceHistory ist die anhängende Liste jeder Zustimmung, neueste zuerst, je Eintrag manifestHash, die Fassung mit Kennung und Nummer (releaseId, version, beide null, wenn die Fassung schon vor der Nummer den Katalog verlassen hatte), acceptedVia (manual oder auto), reasons, acceptedAt und die Listen permissions und resourceTypes, denen damals zugestimmt wurde. Eine App ohne Installation ergibt 404 App.NotInstalled.

pendingManifestHash ist die maschinenlesbare Fassung von Update wartet: Steht dort ein Wert, verlangt eine neue Fassung die Zustimmung, und status meldet pending-reacceptance. Bis zur Entscheidung läuft die Installation in ihrer bisherigen Fassung weiter. Welche Installationen darauf warten, sammelt GET /v1/me/apps/updates (mit state: "pending", daneben stehen Angebote höherer Fassungen mit state: "offer"). Je App liefert GET /v1/me/apps/{appId}/update die Entscheidung in Klartext: Fassung von und nach, Gründe, neue und entfallene Rechte und Datenzugriffe, Bedingungen bisher und neu, die Frist und ob Ablehnen noch geht. Angenommen wird mit POST /v1/me/apps/{appId}/update/accept, abgelehnt mit POST /v1/me/apps/{appId}/update/reject, beide mit dem manifestHash der Fassung, auf die das Update führt, und dem Recht marketplace.app.accept. Eine Ablehnung gilt bis zur nächsten höheren Fassung.

curl -sS "$SZENERA_API/v1/me/apps?locale=en-US" \
  -H "Authorization: Bearer $SZENERA_API_KEY"

Die Zustimmungs-Historie ist der Beleg, gegen den sich eine geänderte Fassung prüfen lässt: Jeder Eintrag hält fest, welchem Manifest-Hash mit welchen Rechten wann zugestimmt wurde — und ob ein Mensch das tat oder die Plattform es automatisch übernahm, weil sich weder Rechte noch Bedingungen bewegt hatten.

Was die Organisation sieht

Der Installationsdialog trägt den Untertitel „Prüfen Sie, was die App mit Ihren Daten tut, und bestätigen Sie die Installation.“ Er zeigt den Block Daten & Zugriff, führt darunter die Rechte als Rollen & Rechte mit ihrer Anzahl, zeigt die Kurzfassung der Bedingungen unter Wichtiger Hinweis, verlinkt den Volltext als Vollständige Bedingungen der App und verlangt das Häkchen Ich habe die Rechte und die Bedingungen der App gelesen und stimme ihnen zu., bevor Installieren wirkt.

Dieselben Angaben stehen schon im Eintrag der App im Store, im Reiter Daten & Zugriff. Wer abwägt, ob er die App überhaupt will, soll ihre Bedingungen vor dem Klick lesen können. Der Reiter nennt, welche eigenen Bereiche die App anlegt (aus resources), welche Bestände sie liest, was sie hinzufügt, welche Verbindungen sie nach außen hält und was dabei übertragen wird und was beim Deinstallieren mit ihren Daten geschieht. Darunter stehen Datenschutz mit den Empfängern aus dataAccess.external und die Kurzfassung der Bedingungen mit dem Verweis auf den Volltext, den das Portal aus legal.termsPath setzt. Was das Manifest auslässt, sagt das Portal ausdrücklich („Diese App greift auf keine Ihrer bestehenden Daten zu.“), statt die Karte wegzulassen.

Den Eintrag liefert GET /v1/me/apps/store/{appId} als StoreEntryView, fertig entschieden: die Lage der App für die Organisation, genau eine Handlung und den Grund mit seinen Zahlen. Die Sätze dazu bildet das Portal, gerechnet wird nur im Server.

Die Sicht der Organisation auf Installieren, Rechte und Zustimmung beschreibt das Handbuch: Apps im Handbuch.

Weiterführendes