Zurück zur Übersicht
Flutter, Architektur, CDN, Mobile

Eine Vorlage ausliefern, ohne die App auszuliefern

Titelbild

Vier Vorschaufolien von Layout-Vorlagen der Exposure App als Reihe auf dunklem Grund, oben und an beiden Seiten angeschnitten, darunter die Zeile „Ausgeliefert ohne App-Release“

Das Wichtigste in Kürze

  • Die Layout-Templates der Exposure App lagen im App-Bundle. Jedes neue Template brauchte damit ein Release, ein Store-Review und ein Update beim Nutzer, obwohl es keine neue Funktion bringt, sondern neuen Content.
  • Der erste Entwurf war ein eigenes Backend mit Adminpanel auf einem gemieteten Server. Gebaut wurde davon nichts: Templates sind unveränderliche statische Dateien, also wurde aus dem Adminpanel ein Ordner im Git-Repo und aus dem Server Static Hosting.
  • Bezahlt haben wir mit einem Kompatibilitätsvertrag. minAppVersion wird aus der Git-Historie abgeleitet, damit kein Template bei einer App landet, der die Assets dazu fehlen. Und offline-first, ein ausdrückliches Ziel der App, gilt für Templates nicht mehr.
  • Heute liegen 54 Templates im Bundle und sechs auf dem CDN, in der Galerie nicht unterscheidbar. Seit dem Start im Juni 2026 gibt es keine Crashlytics-Meldung, die dem CDN zuzuordnen wäre.

Zum Beitrag

Veröffentlicht
14. Sept. 2026
Lesezeit
7 Min.
Autor
Ruben Röhner

Exposure ist eine App für visuelles Storytelling: Bilder, Texte und Sticker werden darin zu mehrseitigen Social-Media-Posts zusammengesetzt. Die Agentur Vertical Meters bringt Idee, Design und Vermarktung ein, ich die technische Seite, und gemeinsam betreiben wir die App seit Januar 2024 in beiden Stores. Das ganze Projekt beschreibt die Fallstudie zur Exposure App.

Hier geht es um einen Teil davon: die Layout-Templates, also die fertigen Layouts, aus denen ein Post startet. Bis Juni 2026 lagen sie komplett im App-Bundle. Heute kommt ein Teil davon über ein CDN. Der Beitrag beschreibt, warum wir dafür kein Backend gebaut haben, wie eine abgeleitete Mindest-App-Version verhindert, dass ein Template bei einer App landet, die es nicht darstellen kann, und warum das Herauslösen eines einzigen Templates aus dem Bundle drei getrennte Mechanismen gebraucht hat.


Jede neue Vorlage brauchte ein Release

Templates entstehen bei uns arbeitsteilig. Vertical Meters baut ein Template im Template-Editor der App selbst und exportiert es dort als Bundle-Datei mit Preview-Bildern. Ich prüfe es mit einer Ingest- und Validierungs-Pipeline und lege es in die App.

Dieses „lege es in die App“ war bis Juni 2026 wörtlich zu nehmen: ein Ordner im Repo, ein Eintrag in einer Manifest-Datei, ein Seeder, der beim ersten Start in die lokale Datenbank schreibt. Das Bundling war Absicht. Die App sollte offline vollständig sein, und Assets im Binary garantieren genau das: ein frisch installiertes Gerät im Flugmodus zeigt trotzdem die ganze Library.

Der Preis dafür ist eine Kette, die sich nicht abkürzen lässt: Ein neues Template braucht ein App-Release, das Release ein Store-Review, und danach sieht es nur, wer die App aktualisiert. Zwischen „fertig“ und „beim Nutzer“ liegen Wochen, für etwas, das keine neue Funktion bringt, sondern neuen Content.

Dazu kam die Größe. Die Asset-Library einer Collage-App wächst nur in eine Richtung. Im Dezember 2025 haben wir sie von PNG und JPG auf WebP umgestellt: in drei Tagen von 362,2 MB auf 114,6 MB, ein Minus von 68 Prozent an Repository-Bytes. Der billige Schritt war damit verbraucht. Die Templates machen rund 31 MB aus, davon etwa 97 Prozent Preview-Bilder. Wer weiter kleiner werden will, muss aufhören, Bytes zu verkleinern, und anfangen, sie woanders hinzulegen.


Ein Ordner im Git-Repo statt eines Backends

Der erste Entwurf war ein richtiges Backend: eine Dart-Shelf-API und ein Flutter-Web-Adminpanel auf einem gemieteten Server hinter Cloudflare. Gebaut haben wir davon nichts.

Templates sind unveränderliche statische Dateien. Ein Bundle ist etwa 20 KB JSON plus zwei bis zehn WebP-Previews. Daran ist nichts zu rechnen, nichts in einer Datenbank zu halten und nichts zu authentifizieren. Dem stand ein Server mit laufenden Kosten gegenüber, dazu Adminpanel, Docker, Nginx und Cloudflare als Wartungslast. Also haben wir das Adminpanel zu einem Ordner im Git-Repo gemacht: versioniert, reviewbar, ohne Login.

Ausgeliefert wird seit Version 2.5.0 über Firebase Hosting, auf dem Projekt, das bereits existierte. Das Auswahlkriterium war der Preis: im Free Tier null Euro, mit globalem CDN, sofortigem Rollback und einem Kommandozeilenaufruf zum Deployen.

Zwei Festlegungen in der Struktur tragen den Rest:

Die Schemaversion steht im Pfad, /v1/. Ein Bundle-Format, das nicht mehr abwärtskompatibel ist, kann später als /v2/ erscheinen, ohne alte Clients zu brechen.

Dateien sind content-addressed. Bundles und Previews tragen einen Hash im Namen (guide-1.c4636521.json) und dürfen deshalb unbegrenzt gecacht werden. Kurz gecacht wird nur das Manifest. Cache-Invalidierung fällt damit weg: Eine geänderte Datei ist eine andere Datei.

Ein Manifest-Eintrag sieht gekürzt so aus:

{
  "id": "guide-1",
  "name": "Guide 1",
  "category": "guides",
  "isPremium": true,
  "minAppVersion": "2.7.0",
  "bundleUrl": "templates/guide-1.c4636521.json",
  "checksum": "c4636521194edf164fbb0f11d56b2862e163b21ad1064747d06f34f814eb0943"
}

Was ein Template von der App verlangen darf

Solange alles im Binary lag, konnte ein Template nichts verlangen, was die App nicht kann. Beide kamen aus demselben Build. Diese Garantie ist mit dem CDN weg. Ein Template, das einen Sticker oder ein Frame benutzt, das erst Version 2.7.0 mitbringt, würde auf einer 2.6.0 nicht rendern.

minAppVersion ist die Antwort darauf, und die Schwelle wird beim Deployen abgeleitet. Das Skript liest jedes referenzierte App-Asset aus dem Bundle und löst es über die Git-Historie auf: gesucht wird das früheste Release-Tag, dessen Tree diese Datei enthält. Der höchste Wert wird zur Schwelle, und Apps darunter überspringen den Eintrag. Ein Bundle, das ein Asset referenziert, das es in keiner Version gibt, lässt das Deployen fehlschlagen.

Von Hand lässt sich die Schwelle anheben, nicht senken:

{
  "stage": "prod",
  "isPremium": true,
  "minAppVersion": "2.7.0"
}

Das braucht es bei Templates, die eine Fähigkeit der App voraussetzen statt eines Assets. Der abgeleitete Wert gewinnt, sobald er höher liegt: Senken hieße, ein Template an Installationen auszuliefern, denen die Assets fehlen.

Eine Lücke bleibt: Eine neue Kategorie braucht weiterhin ein App-Release, weil sie an einem Dart-Enum, an Übersetzungen und an der Sortierung der Galerie hängt. Der Fix dafür ist entworfen und nicht gebaut.


Offline-first hat eine Diskussion verloren

Das Bundling war eine Entscheidung für ein Ziel: eine App, die offline vollständig ist. Das CDN kassiert dieses Ziel ein, gezielt für Templates und für nichts anderes.

Der Sync ist entsprechend defensiv gebaut. Er läuft nach dem lokalen Seeding, hinter try/catch und erst nach dem ersten Frame. Offline ist er ein No-Op. Der CDN-Client gibt bei jedem Fehler null zurück, ob Timeout, Statuscode oder zu große Antwort, und der Sync versucht es beim nächsten Start wieder. Ein Template wird erst eingetauscht, wenn Bundle und alle Previews geladen sind und die Checksum stimmt.

Dazu kommt etwas, das es beim Bundling nicht gab: Das CDN ist eine Trust Boundary. Ein Sicherheitsreview am Release-Tag hat zwei Dinge festgezurrt. Preview-Pfade, die absolut sind oder .. enthalten, werden abgewiesen, weil die Slide-Ansicht einen absoluten Pfad wörtlich rendert und ein feindseliges Bundle damit beliebige Dateien aus dem Sandbox-Verzeichnis anzeigen könnte. Und jeder Download läuft im Stream gegen eine harte Obergrenze: 1 MiB Manifest, 2 MiB Bundle, 8 MiB Preview.

Bezahlt haben wir mit einem Fall, den wir aufgeschrieben statt wegkonstruiert haben: Eine frische Installation ohne Internetverbindung zeigt den Abschnitt „Guides“ gar nicht. Die Galerie blendet leere Kategorien aus, dort steht dann eine kürzere Liste und keine Fehlermeldung.


Eine ausgelieferte Datei zu löschen ist ein verteiltes Problem

Bis hierhin war das CDN additiv. Alle 56 gebündelten Templates blieben in der App, das CDN kam obendrauf. Die eigentlich interessante Arbeit begann im August 2026, als wir anfingen, bereits ausgelieferte Templates aus dem Bundle zu entfernen. Das trifft drei Gruppen unterschiedlich, und jede braucht ihren eigenen Mechanismus:

  • Wer die App installiert hat, trägt das Template bereits als Zeile in seiner lokalen Datenbank. Der Manifest-Eintrag legt deshalb keine zweite Kopie an, sondern übernimmt die bestehende Zeile über den Namen. Kostet einen Download pro Installation.
  • Wer nicht aktualisiert, hat das Template weiter im Bundle und darf die Remote-Fassung nicht bekommen. Die Schwelle steht deshalb exakt auf der Version, die das Asset entfernt, und das Deployen erzwingt die Invariante: entweder gebündelt oder remote, nie beides.
  • Wer frisch installiert, bekommt es nur über den Sync, mitsamt Paywall. Die Pipeline leitet isPremium sonst aus dem Bundle-Inhalt ab, also daraus, ob Premium-Sticker, -Frames oder -Grids vorkommen. Beide geplanten Wellen verschieben aber je ein kostenloses Template, das Premium-Inhalte benutzt: Die Ableitung hätte es still hinter die Paywall gestellt, das Migrationsskript nimmt die Stufe deshalb aus der bestehenden Allowlist.

Die erste Welle war bewusst winzig: zwei Templates, 2,1 MB, in einer Kategorie, die bereits halb remote war. Ausgeliefert mit 2.7.0 am 22.08.2026, zwei Tage später in Produktion.

Der Grund für dieses Tempo ist eine Zahl. Jedes Byte, das aus dem Bundle verschwindet, wird zu CDN-Traffic bei jeder Neuinstallation, dazu kommt ein einmaliger Schub über die bestehende Installationsbasis. Der Free Tier gibt 360 MB pro Tag her: Nach Welle 1 trägt das rund 170 Neuinstallationen am Tag, nach Welle 2 noch rund 72, bei Welle 5 beginnt die Decke zu drücken. Deshalb gibt es Wellen, und deshalb werden Store-Rollouts gestaffelt.

Welle 2 ist implementiert und liegt auf einem Branch. Gemerged ist sie nicht: Sie wartet darauf, dass Welle 1 lange genug in Produktion war.


Heute

Stand 14.09.2026, App-Version 2.7.1: 54 Templates liegen im Bundle, sechs auf dem CDN, die Library steht bei Version 18. In der Galerie sind beide nicht zu unterscheiden, der einzige Unterschied ist eine ausgefüllte Spalte in der lokalen Datenbank. Ein neues Template in einer bestehenden Kategorie geht ohne App-Release raus. Eine neue Kategorie nach wie vor nicht.

Seit dem Start im Juni 2026 gibt es keine Crashlytics-Meldung, die dem CDN oder dem Sync zuzuordnen wäre.


Was das für ein Kundenprojekt heißt

Jedes Team, das eine mobile App ausliefert, läuft irgendwann gegen dieselbe Wand: Ein Inhalt ist fertig, die Nutzer sehen ihn Wochen später. Preislisten, Onboarding-Texte, Formularvorlagen, Konfigurationen für einzelne Standorte, all das hängt genauso am Release-Takt wie bei uns ein Template.

Die Frage ist deshalb nicht „CDN oder Bundle“. Sie lautet: Was verlässt das Binary, und was kostet der Vertrag danach? Ein Inhalt außerhalb der App kann mehr verlangen, als die installierte App kann, und etwas muss das abfangen, bevor es die Crash-Reports tun. Kommt er über das Netz, muss entschieden sein, welche Funktion ohne Verbindung fehlen darf. Und sobald Bytes pro Installation ausgeliefert werden, richtet sich die Form des Rollouts nach der Traffic-Rechnung.

Bei uns sind alle drei Antworten vor dem ersten ausgelieferten Template gefallen, und sie haben deutlich mehr Aufwand gekostet als die Auslieferung selbst. Zu kalkulieren ist nicht das Hosting, sondern die Mechanik, die danach dauerhaft dafür sorgt, dass ein Inhalt und eine installierte App zusammenpassen.


FAQ

Wann lohnt es sich, App-Inhalte über ein CDN auszuliefern, und wann bleibt das Bundle richtig?

Die Trennlinie liegt bei der Änderungsrate. Inhalte, die sich seltener ändern als die App selbst, gehören ins Bundle: Sie kosten dort nichts an Kompatibilität und sind offline garantiert da. Sobald ein Inhalt häufiger fertig wird, als Releases erscheinen, zahlt jede Änderung ein Store-Review, und dann lohnt der Schritt nach außen.

Wie verhindert man, dass ausgelieferte Inhalte eine ältere App-Version brechen?

Über eine Mindest-App-Version pro Inhalt, die nicht von Hand gepflegt wird. Bei uns leitet das Publish-Skript sie ab: Jedes im Bundle referenzierte Asset wird über die Git-Historie dem frühesten Release zugeordnet, das diese Datei enthält, und der höchste Wert wird zur Schwelle. Eine App darunter überspringt den Eintrag. Wichtig ist, dass die Ableitung auch fehlschlagen darf: Ein Bundle mit einem unbekannten Asset wird gar nicht erst veröffentlicht.

Was passiert mit Inhalten aus dem CDN, wenn die App offline ist?

Bei uns nichts, und das ist Absicht. Ohne Verbindung ist der Sync ein No-Op und versucht es beim nächsten Start erneut. Was bereits synchronisiert wurde, liegt lokal. Der Preis ist eine frische Installation ohne Verbindung: Sie sieht die remote ausgelieferten Templates noch nicht, und weil die Galerie leere Kategorien ausblendet, fehlt dort ein Abschnitt, statt dass eine kaputte Ansicht erscheint.

Weiterlesen war's?

Inhalte aus Ihrer App herauslösen?

Ob Vorlagen, Konfigurationen oder ganze Inhaltspakete: Ich sehe mir Ihren Auslieferungsweg an und sage Ihnen, was ein Schritt aus dem Binary bringt und was er an Kompatibilität kostet.

Antwort in der Regel binnen 48 Stunden.