Von der Vorlage bis zur Freigabe
Der ganze Weg einer neuen App: mit pnpm app:new aus der Vorlage anlegen, Widget, Config, Texte und Bilder schreiben, mit pnpm app:check prüfen, testen und über den Pull Request in den Katalog bis zur Freigabe in Stabil.
12 Minuten LesezeitApps-SDKStand: 25.10.2026
Auf dieser Seite
Bevor Sie loslegen
- Für wen
- Entwicklerinnen und Entwickler, die im Quellbaum der Plattform eine neue App bauen. Das SDK ist vorbereitet, aber nicht veröffentlicht. Apps von außen kommen erst mit der Öffnung für Herausgeber.
- Voraussetzung
- ein Arbeitsstand des Quellbaums nach
pnpm installundpnpm build, Node 24. Für die Freigabe zwei Betreiber-Konten mit den Rechtenplatform.app-catalog.importundplatform.app-catalog.promote.
Nach dieser Anleitung steht Ihre App als Fassung im Kanal Stabil und damit im Store jeder Organisation, deren Paket sie enthält. Der Weg beginnt mit einem Befehl, der die App aus einer Vorlage anlegt, und endet mit der Bestätigung einer zweiten Person im Ops-Portal. Dazwischen sagt ein zweiter Befehl jederzeit, was noch fehlt.
Der Weg in einem Bild
pnpm app:new <id> ─ Widget, Config, Formular, Texte, Bilder ─ pnpm app:check <id> ─ Tests
└─► Pull Request mit Notiz ─ Versions-PR ─ Zug: Tag @szenera-app/<id>@<fassung>
└─► release-app.yml: bauen, aufnehmen, packen, prüfen, signieren, ablegen, melden
└─► Staging und Produktion: Release im Kanal Canary
└─► Ops-Portal: Release-Prüfung ⇒ Beta ⇒ Stabil, eine zweite Person bestätigt
Die Wörter dieser Seite folgen dem Glossar: Die Fassung ist die Nummer, die eine Organisation liest (1.0.0), das Release ist dieselbe Fassung im Katalog des Betreibers, und der Kanal sagt, wer sie bekommt.
1. Die App anlegen
pnpm app:new door-sign --name="Türschild" --name-en="Door sign"
pnpm install
pnpm --filter @szenera-app/door-sign build
pnpm app:new <id> legt apps/<id>/ aus der Vorlage an und schreibt sonst nichts: keine Liste, keinen Eintrag in einer Datei der Plattform. Plattform, Katalog, Aufnahmen und Budgets finden die App über ihren Ordner.
- Die Kennung ist kebab-case und englisch (
door-sign, nichttuerschild). Sie gilt für den Ordner, das Paket@szenera-app/<id>, das Manifest, das eine Widget der Vorlage und das Datenbank-Schemaapps_<id>(mit Unterstrich statt Bindestrich). Eine vergebene, eine reservierte (core) oder eine falsch geformte Kennung lehnt der Befehl ab, mit jedem Grund auf einmal, und legt dann nichts an. - Die Namen unter
--name(deutsch) und--name-en(englisch) stehen im Manifest. Ohne sie nimmt der Befehl vorerst einen Namen aus der Kennung, die Texte schreiben Sie ohnehin neu. pnpm installnimmt die App in den Arbeitsbereich auf. Danach steht die Lockdatei kompakt da:pnpm exec prettier --write pnpm-lock.yamlbringt sie in die Form des Baums, undpnpm lizenzhinweiseschreibt die Hinweise Dritter mit dem neuen Stand der Lockdatei.pnpm --filter @szenera-app/<id> buildbaut die App. Der Seed des Katalogs liest ihr gebautes Manifest und überspringt sie bis dahin mit Hinweis.
Was die Vorlage mitbringt:
| Teil | Dateien | Was darin steht |
|---|---|---|
| Manifest | src/manifest/index.ts |
Kennung und Fassung, Texte auf Deutsch und unter translations auf Englisch, Kategorie, Klasse, Store-Block, compatibility (Wirte, Lagen, Verhalten ohne Netz) und apiCompatibility.rendererHost aus RENDERER_HOST_RANGE |
| Widget | src/widgets/<id>/ |
definition.ts, config.ts, config-ui.ts, runtime.tsx, authoring.tsx, layout.ts |
| Einstiege | src/runtime/, src/authoring/ |
registerWidgets und registerAuthoring |
| Tests | tests/, testing/ |
Manifest, Verträglichkeit mit der Grundlinie tests/compat-baseline.json, Widget und Formular über zwei Render-Harnische |
| Bau | package.json, tsup.config.ts, vite.config.*.ts |
exports für Manifest, Laufzeit und Authoring, die Skripte und die Deckel der Bündel unter szenera.bundleBudgets |
| Bilder | icon.svg, thumbnails/ |
Platzhalter, die pnpm app:check rot zeigt, bis Symbol und Aufnahmen echt sind |
Eine App ohne Widget (eine, die eine Funktion des Portals aufsperrt) oder mit Server-Teil baut auf derselben Vorlage auf: ./runtime und ./authoring entfallen, ./backend kommt hinzu, wie unter Server-Teil: Resolver, Fetcher und Jobs beschrieben.
2. Widget, Config und Formular schreiben
config.ts ist der Werte-Vertrag des Widgets. Jedes Feld trägt .default(), damit eine leere Config vollständig liest, und das Schema verwirft unbekannte Felder still (kein .strict()), damit eine ältere Fassung die Config einer neueren liest. Für jede spätere Fassung gilt dieselbe Regel: Felder kommen nur hinzu, mit Vorgabe, und keines verschwindet oder ändert seinen Typ. Die Grundlinie hält das fest (Schritt 6).
.describe('Beschriftung|Satz') trägt Beschriftung und Hilfetext eines Felds. Das Formular in config-ui.ts liest beides mit schemaFieldText() aus @szenera/widget-editor/config, so heißt ein Feld überall gleich. Welche Steuerelemente der Editor kennt, sagt die Feldart:
Feldart (kind) |
Steuerelement im Editor | Wert in der Config |
|---|---|---|
text, textarea |
eine Zeile, mehrere Zeilen | Text |
number, slider |
Zahl, Schieberegler | Zahl |
select, segmented, checkboxGroup |
Auswahl, Umschalter, Kästchen | ein Wert, mehrere Werte aus options |
toggle |
Schalter | ja oder nein |
color |
Farbwähler | #rrggbb |
date |
Kalender | isoDate(): JJJJ-MM-TT, leer erlaubt |
time |
Uhrzeit | timeOfDay(): HH:mm |
dateTime |
Kalender, Uhrzeit und die Zone der Angabe | isoDateTime(): Zeitpunkt mit Versatz |
timezone |
Suche über alle Zonen mit deutschen und englischen Namen | timezoneName(): der Name der Zone (Europe/Berlin), leer für die Zone der Organisation |
address |
Straße, PLZ, Ort und Land als ein Feld | postalAddress() |
location |
Standort des Displays oder fester Ort aus der Stadtsuche, auf Wunsch mit Ersatzort | placeSelection(): mode, place, fallback |
surface |
Flächen als Kacheln mit Farbmuster (options mit value, label, swatch) |
die Kennung der Fläche |
list |
Einträge hinzufügen, entfernen, duplizieren und per Griff oder Pfeiltasten verschieben, eingeklappt mit Kurztitel, ein Medium im Eintrag | z.array(…) von Texten oder kleinen Objekten |
asset, font |
Mediathek, Schriften | Kennung des Mediums oder der Schrift |
Gegliedert wird das Formular in Gruppen (sections) in der festen Folge Vorlage, Quelle, Inhalt, Aussehen, Erweitert und Platzierung. Eine Gruppe mit collapsed: true startet eingeklappt, so steht „Erweitert“ niemandem im Weg. required: true kennzeichnet ein Pflichtfeld, und showWhen mit not: true zeigt ein Feld bei jedem Wert außer den genannten. Höchstens zwei Felder kompakter Art (Auswahl, Umschalter, Schalter, Regler, Farbe, Fläche) nennt quickFields: Die bietet die Leiste am gewählten Widget direkt an. Wer die Beschreibung mit defineWidgetConfigUI() aus @szenera/apps-sdk/config anlegt, erfährt schon beim Bau, wenn eine dieser Regeln nicht stimmt.
Das Einstellungs-Fenster baut sich aus dieser Beschreibung: registerAuthoring trägt sie mit registerConfigUI ein. Ein Bedienelement, das keine Feldart abbildet (etwa eine Stadtsuche), kommt als einzelnes Feld dazu (registerConfigField), nicht als ganzes Formular. Wie das Formular Felder, Kontext und Passt-Bericht verbindet, steht unter Widgets konfigurieren.
Die Werte-Typen aus @szenera/apps-sdk/config hängen nur an zod, der Bau nimmt sie ins Bündel der App. Wie das Widget seine Fläche zeichnet, Platzierungen und Ansichten anlegt, steht unter Widget, Konfiguration und Vorschau.
3. Die Texte schreiben
Die Texte stehen auf Deutsch im Manifest, jede weitere Sprache als Block unter translations, der nur Texte trägt. Englisch ist Pflicht, und defineAppManifest lehnt einen unvollständigen Block mit dem Pfad des fehlenden Texts ab. Dieselben Regeln gelten für jeden Text, den eine Organisation liest, im Store, im Install-Dialog und im Editor: die Sie-Form, kein Semikolon, höchstens ein Gedankenstrich je Text, keine Wörter aus dem Inneren der Plattform und keine Branche in einer App für alle Branchen.
Zwei Angaben ordnen die App ein: category bestimmt den Abschnitt im Store und in der Widget-Palette, tier die Klasse (standard in jedem Paket, pro ab dem Paket Pro, addon eigens gebucht). Der Manifest-Test der App hält beide fest, damit eine verschobene Einordnung nicht still durchläuft. Eine Klasse außer standard braucht zudem ihren Eintrag in der Verkaufsliste EXPECTED_TIERS in services/api/tests/app-tier-classification-sweep.test.ts, etwa 'door-sign': 'pro': Dort steht, was verkauft wird, und ein Test der Schnittstelle hält jede App dagegen. Fehlt der Eintrag, nennt ihn pnpm app:check in der Zeile Manifest. Eine App mit fremder Quelle oder Pflichtangaben trägt dazu einen Block legal (Datenzugriff, Rechte und Bedingungen).
4. Symbol und Aufnahmen
Das Symbol icon.svg zeichnen Sie selbst. Solange das Symbol der Vorlage mit seiner Marke data-placeholder dort liegt, bleibt die Zeile Store-Bilder rot.
Die Aufnahmen entstehen mit dem Werkzeug, das auch die CI benutzt:
node tools/app-screenshots/run.mjs --build --only=door-sign
Es nimmt jede Ansicht unter preview.variants auf, die Hauptansicht ein zweites Mal unter dem Namen aus thumbnailUrl und das Szenenbeispiel, die Hauptansicht in einer typischen Szene. Braucht eine Aufnahme mehr, eine Akzentfarbe der Umgebung oder eine feste Teil-Config, steht es in der package.json unter szenera.screenshots.
5. Mit pnpm app:check prüfen
pnpm app:check door-sign # aus den Quellen, ohne Bau
pnpm app:check door-sign --built # dazu Budgets und Host-Zusage gegen den Bau
Der Befehl zeigt je Prüfung eine Zeile, mit jedem Befund darunter. Lokal und in der CI ist es derselbe Befehl, die Prüfung vor dem Bau fährt pnpm app:check --all über alle Apps.
| Zeile | Was sie prüft |
|---|---|
| Manifest | Das Manifest lädt und erfüllt den Vertrag. Ordner, Paket und Manifest tragen dieselbe Kennung, Paket und Manifest dieselbe Fassung, jeder Teilpfad unter exports hat seine Quelle, das Schema heißt apps_<id>. Eine Klasse außer standard steht in der Verkaufsliste (Schritt 3). |
| Übersetzungen | Deutsch und Englisch sind da. |
| Kompatibilität | Wirte, Lagen und Verhalten ohne Netz stehen im Manifest. |
| Store-Bilder | Symbol, jede Ansicht, die Hauptansicht und das Szenenbeispiel liegen da, je ein PNG in den Maßen seiner Lage und nicht leer. |
| Texte | Jeder Text aus Manifest, Übersetzung und Formular folgt der Sprachregel, und jedes Feld heißt im Formular wie im Schema. |
| Formular | Jede Formular-Beschreibung in src/widgets/<ordner>/config-ui.ts hält die Regeln, die kein Typ ausdrückt (widgetConfigUIIssues): höchstens zwei Schnellfelder kompakter Art, jede Gruppe einmal und jedes Feld in einer, die es gibt, jede Fläche zur Wahl einmal. |
| Zustände | Jedes Widget, das Daten von außen holt, erklärt den Vorschau-Zustand empty ({ id: 'empty', mockData: null } oder die leere Nutzlast), damit das Display die ruhige Fläche zeigt. Eine Ausnahme bis zur eigenen Welle steht mit Begründung in tools/app-kit/src/check/state-exceptions.ts. |
| Grundlinie | Test und Grundlinie liegen da, die Config hält sie ein, und die Grundlinie ist aktuell: Ein Feld, das nicht darin steht, schützt sie nicht. |
| Budgets | Ohne Bau: Die Deckel sind lesbar. Mit --built: jedes Bündel unter seinem Deckel. |
| Host-Zusage | Mit --built: Das Laufzeit-Bündel bezieht nur zugesagte Namen, und das gebaute Manifest trägt den Bereich, der aus den Importen folgt. |
| Verdrahtung | Was außerhalb des Ordners für die App stimmen muss. |
| Handbuch | Die App hat eine Seite im Handbuch. |
| Notiz | CHANGELOG.md nennt die Fassung aus package.json mit einem Abschnitt ## <fassung>. Aus ihm liest der Lieferweg die Notizen des Artefakts, ohne ihn packt er die Fassung nicht. Den Abschnitt schreibt der Versions-PR aus der Notiz (Schritt 7), eine neue App bringt ihn aus der Vorlage mit. |
Eine Zeile mit – ist nicht geprüft, sie zählt nicht als bestanden: Budgets und Host-Zusage brauchen den Bau, und den prüft ohnehin der Bau selbst. Ein Bündel über seinem Deckel bricht vite build, ein Import außerhalb der Zusage ebenso.
Eine frische App aus der Vorlage zeigt drei Zeilen rot, und jede verschwindet mit einem Handgriff:
- Store-Bilder: ein eigenes Symbol und die Aufnahmen aus Schritt 4.
- Verdrahtung:
"@szenera-app/<id>": "workspace:*"inservices/public-site/package.jsonund, für eine App mit Widget, inservices/scene-snapshot/package.json, danachpnpm install. Der Bau beider Pakete liest die gebauten Apps, und erst die Abhängigkeit sagt ihm die Reihenfolge. Danachpnpm codegen:public-app-catalog, das den Katalog der öffentlichen Seite neu schreibt. - Handbuch: eine Seite
docs/de/handbook/apps/<id>.mdvom Typapp. Die öffentliche Seite baut nur, wenn jede App eine hat.
6. Testen
pnpm --filter @szenera-app/door-sign typecheck
pnpm --filter @szenera-app/door-sign test
Die Tests laufen gegen die Quellen, ohne Bau. tests/manifest.test.ts hält Vertrag, Kategorie, Bedingungen, Wirte und Registry, tests/compat.test.ts die Grundlinie und den Host-Bereich, die Tests unter tests/widgets/ Darstellung und Formular. Ändern Sie die Config, schreibt pnpm app-compat --update door-sign das Neue in die Grundlinie. Bricht die Änderung die Regel aus Schritt 2, schreibt der Befehl nichts, und der Test nennt das Feld.
7. Der Pull Request
Der PR trägt eine Notiz: pnpm changeset nennt die App, und eine Notiz für eine App braucht den Abschnitt ## Kunde mit dem Satz, den eine Organisation zu dieser Fassung liest. Ohne Notiz bekommt die App keine Fassung. Die Prüfung des PR fährt pnpm app:check --all mit der übrigen Statik, und in der vollen Tiefe baut, nimmt auf, packt und prüft sie jede berührte App so, wie der Lieferweg es tut. Signiert und abgelegt wird im PR nichts.
Nach dem Merge hebt der Versions-PR die Fassung aus der Notiz, und der Zug setzt den Tag @szenera-app/<id>@<fassung>.
8. Vom Tag in den Katalog
Zum Tag baut release-app.yml die Fassung genau einmal: Bau, Aufnahmen, das Artefakt <id>-<fassung>.szapp, die Prüfung, die Signatur, die Ablage in der Registry. Dann meldet der Lauf die Fassung Staging und Produktion, beide holen dasselbe Artefakt und legen das Release im Kanal Canary an. Dieselbe Fassung trägt damit überall dieselben Bytes. Was an einem Release liegt und wie die Kanäle wirken, steht unter Signieren und der Weg in den Katalog.
9. Prüfen und freigeben im Ops-Portal
- Öffnen Sie im Ops-Portal Apps, die Zeile der App und den Reiter Releases. Die neue Fassung steht in Canary, Prüfung öffnen führt zur Release-Prüfung.
- Die Prüfung zeigt links den Store-Eintrag, wie die Organisationen ihn sehen werden, rechts die Zustimmungsfolge, den Vergleich mit der Vorgängerin, „Neu in dieser Fassung“, die Verbreitung und die Prüfliste. Fehlt der Text für die Organisationen, tragen Sie ihn hier ein.
- Nach Beta hochstufen … dürfen Sie allein. In Beta bekommen die Fassung die Organisationen, die sich für Vorabfassungen eingetragen haben.
- Nach einigen Tagen in Beta: Nach Stabil hochstufen … mit einer Begründung. Das legt eine Anforderung an, die Fassung bleibt so lange in Beta.
- Eine zweite Person öffnet die Prüfung derselben Fassung und bestätigt. Erst damit steht die Fassung in Stabil, und alle Organisationen, deren Paket die App enthält, finden sie im Store.
Ergebnis prüfen
pnpm app:check <id> --builtzeigt jede Zeile mit✓.- Im Ops-Portal steht die Fassung im Reiter Releases in Stabil, und der Verlauf nennt bei der Freigabe beide Personen.
- Im Store einer Organisation mit passendem Paket steht die App unter ihrer Kategorie, mit Symbol, Aufnahmen und Szenenbeispiel aus dem Artefakt.
Wenn etwas nicht klappt
| Was Sie sehen | Was dahintersteht und was hilft |
|---|---|
app:new legt nichts an |
Die Meldung nennt jeden Grund: eine vergebene oder reservierte Kennung, eine Form außerhalb von kebab-case, ein Name mit Anführungszeichen. Den Grund beheben und den Befehl wiederholen. |
| Zeile Manifest: „das Manifest lässt sich nicht laden“ | defineAppManifest hat abgelehnt, der Text danach nennt das Feld, eine unvollständige Übersetzung etwa mit ihrem Pfad. |
| Zeile Manifest: „EXPECTED_TIERS … nennt … nicht“ | Die App trägt eine Klasse außer standard, und die Verkaufsliste kennt sie nicht. Den Eintrag aus dem Befund in services/api/tests/app-tier-classification-sweep.test.ts übernehmen (Schritt 3). |
| Zeile Notiz: „CHANGELOG.md nennt die Fassung … nicht“ | Die Fassung in package.json ist gehoben, ohne dass der Versions-PR sie geschrieben hat. Fassungen hebt nur die Notiz über den Versions-PR, package.json und CHANGELOG.md ändern sich dann gemeinsam. |
| Zeile Grundlinie: „nicht aktuell“ | Die Config hat ein Feld, das die Grundlinie nicht kennt. pnpm app-compat --update <id> schreibt es fest. |
| Zeile Grundlinie: „Das neue Feld hat keine Vorgabe“ | Ein neues Feld ohne .default(). Eine Config einer früheren Fassung kennt es nicht und liest dann nicht mehr. |
| Zeile Store-Bilder: „zu klein“ | Ein Platzhalter der Vorlage oder ein leeres Bild. Die Aufnahme aus Schritt 4 ersetzt ihn. |
vite build bricht am Deckel |
Das Bündel ist größer als szenera.bundleBudgets. Erst schlanker bauen, sonst den Deckel mit Grund im Feld note heben. |
vite build bricht an der Host-Zusage |
Das Laufzeit-Bündel bezieht einen Namen, den die Anzeige nicht zusagt. Den Namen vermeiden oder die Zusage mit einer neuen Fassung des Host-Vertrags erweitern, eine Änderung an der Plattform. |
In der Nacht beweist pnpm app:probe, dass dieser Weg hält: Die Probe legt eine App aus der Vorlage an, fährt ihre Tests und ihren Bau, fragt Seed, Aufnahmen und Budgets, ob sie die App sehen, und erwartet von pnpm app:check genau die drei roten Zeilen einer frischen App.