Eine App ist mehr als eine App – Frontend, Backend, API, Infrastruktur
Unter dem Eisberg: Warum die App ohne Infrastruktur nicht mehr als ein Musik-Player wäre, was das Backend alles macht und warum wir unsere eigene API hosten.
Teil der Serie "Behind the App" — Was unter der Wasseroberfläche einer Radio-App steckt
Der Eisberg
Ein Nutzer öffnet die App, tippt auf Play, hört Radio. Aus seiner Sicht ist das die App. Genau so soll es sich auch anfühlen: ein Knopf, ein Stream, fertig.
Was er dabei nicht sieht, ist alles andere. Der Stream kommt nicht direkt vom Sender ins Handy. Der Songtitel taucht nicht magisch im Lock-Screen auf. Die Liste der über 40.000 Sender liegt nicht in der App. Das Logo, das links neben dem Sendernamen erscheint, wird nicht vom Sender selbst geliefert. All das passiert irgendwo dazwischen, auf Servern, in Datenbanken, hinter APIs.
Wenn man die App als Spitze des Eisbergs sieht, dann liegen darunter ungefähr noch einmal sieben Achtel. Dieser Post ist ein kurzer Tauchgang.

Über Wasser: die App
Die sichtbare Spitze ist das, woran die meisten Commits gehen: React Native, Expo, ungefähr 720 Commits in 15 Monaten. Player, Visualizer, Suchen, Favoriten, Themes, CarPlay, Android Auto, Push, In-App-Käufe.
Das ist die Hälfte, die man auf Screenshots und im App Store sieht. Sie bekommt die meiste Aufmerksamkeit, weil sie sichtbar ist. Sie ist aber nicht zwingend die anstrengendere.
Schicht 1: das eigene Backend
Direkt unter der App liegt ein eigener Server: Node, Express, MariaDB, alles in Docker verpackt. Was macht der eigentlich?
Stream-Proxy. Viele Icecast- und Shoutcast-Streams kommen ohne CORS-Header, mit kaputtem HTTPS, mit Session-IDs in der URL oder über ungewöhnliche Ports. Der Proxy nimmt das in die Mitte und gibt der App etwas, das sie zuverlässig abspielen kann. Er liest auch die ICY-Metadaten aus dem Stream heraus, also die Information „welcher Song läuft gerade", die ein normales <audio>-Element nicht ohne Weiteres bekommt.
Songtitel anreichern. Aus „Künstler – Titel" wird im Backend ein Datensatz mit Cover, Album und optional Spotify- und Apple-Music-Link. Die App müsste das selbst machen, wenn das Backend es nicht täte. Sie würde dabei jedes Endgerät einzeln Anfragen an dieselben Drittdienste schicken lassen.
User-Accounts, Verlauf, Favoriten. Wer einen Account hat, will seine Lieblingssender und gehörten Songs auf jedem Gerät wiederfinden. Das braucht eine Datenbank, eine Authentifizierung und ein paar Endpoints. Klingt klein, ist über die Zeit aber gewachsen: Passwort-Recovery, Account-Löschung, DSGVO-Export, Push-Token-Verwaltung.
Push-Nachrichten, Auth-Webhooks, Referral-Codes. All die Dinge, die nicht beim ersten Build der App entstehen, sondern später dazukommen, weil Nutzer, Stores und Compliance es verlangen.
Im ersten Post dieser Serie habe ich erzählt, wie aus einem Netlify-Setup mit Supabase in zwei Wochen ein eigenes Backend wurde. Das war kein Selbstzweck. Es war die Voraussetzung dafür, dass die App diese ganzen Dinge überhaupt zuverlässig kann, ohne von einem Serverless-Limit oder einer Drittanbieter-Preisliste abhängig zu sein.
Schicht 2: die eigene Sender-API in Rust
Die App selbst hat keine Liste der Sender. Sie fragt sie ab. Genauer: bei einem Dienst, den es als Open-Source-Projekt schon lange gibt — radio-browser.info. Über 40.000 Sender, kostenlos, ohne API-Key, von einer Community gepflegt.
Die offensichtliche Frage: Warum nicht einfach die öffentliche Instanz nutzen?
Die ehrliche Antwort: Weil ich schreiben können wollte, nicht nur lesen.
Eine Sender-Datenbank, die von Tausenden Nutzern befüllt wird, sieht nach einer Weile aus wie jede solche Datenbank: Es gibt sehr viele Duplikate desselben Senders unter leicht anderen Namen, es gibt tote Stream-URLs, und es gibt viele kaputte oder fehlende Logos. Letzteres haben sogar frühe Tester der App gemeldet — „warum hat der Sender X kein Logo, obwohl er groß ist?". Die Antwort ist meistens: Es gibt eines, aber die hinterlegte URL liefert seit Jahren einen 404.
Mit einer öffentlichen API kann ich solche Datensätze nicht reparieren. Ich kann eine Korrektur vorschlagen, aber ich habe keine Kontrolle darüber, wann sie übernommen wird, und ich kann nicht garantieren, dass meine Nutzer die korrigierte Version sehen. Mit einer eigenen Instanz schon.
Also: gleiche Software, eigener Server, eigene Datenhoheit. Konkret heißt das:
- Logos pflegen. Wenn ein Sender ein neues, hochauflösendes Logo hat, kann ich es eintragen, ohne auf einen Upstream-Merge zu warten.
- Stream-URLs aktuell halten. Wenn ein Sender den Anbieter wechselt und der alte Stream tot ist, kann ich den neuen direkt eintragen.
- Duplikate und Spam entfernen. Drei Einträge für denselben Sender mit unterschiedlicher Schreibweise werden zu einem. Werbe-„Sender", die nichts spielen, fliegen raus.
- Sender hinzufügen. Lokale, kleine, neue Sender, die noch nicht im Index sind, kann ich aufnehmen.
Der Witz an dieser Schicht ist die Sprache: Die radio-browser-API ist in Rust geschrieben. Im Rust-Ökosystem bin ich nicht zu Hause. Vor ein paar Jahren wäre das ein Grund gewesen, das Projekt nicht selbst zu hosten, sondern eine Alternative zu suchen.
Heute eher nicht. Mit KI-Unterstützung — bei mir typischerweise als eine Art Pair-Programming, bei der ich die Architektur und die Entscheidungen führe und das Modell die Sprach- und Bibliotheks-Details beisteuert — ist eine fremde Sprache für eine begrenzte Aufgabe (kompilieren, Konfigurationen verstehen, kleine Patches lesen, gelegentlich etwas anpassen) deutlich weniger einschüchternd. Es ist nicht so, dass plötzlich alles trivial wäre. Aber die Hürde, ein gut strukturiertes Open-Source-Projekt in einer fremden Sprache zu betreiben, ist niedriger als früher.
Wie sich AI-assisted Development insgesamt anfühlt, gerade auch bei den weniger glücklichen Momenten, wird ein eigener Post in dieser Serie. Hier soll es nur die Pointe sein, dass diese Schicht ohne KI-Unterstützung wahrscheinlich gar nicht existieren würde.
Bis hierhin haben wir also zwei Forks: einen Web-Player und eine Sender-API. Zwei Open-Source-Projekte, zwei Mal die Entscheidung, lieber selbst zu hosten als sich auf etwas zu verlassen, worauf man keinen Einfluss hat. Es ist kein Zufall, dass dieses Muster sich wiederholt. Es ist eine ziemlich bewusste Architektur-Entscheidung: Lieber mehr Verantwortung als weniger Kontrolle.
Fundament: die Infrastruktur
Unter all diesen Diensten liegt das, was am wenigsten sexy ist und am wenigsten aufhört: Server, Docker-Compose-Dateien, MariaDB, regelmäßige Backups, Logs, Reverse-Proxy, Zertifikate, Firewall-Regeln, ein Skript, das auffällige IP-Adressen blockt, ein Rebuild-Skript, das das Ganze nach einem Deploy wieder zusammensetzt.
Nichts davon erscheint in Release-Notes. Nichts davon merkt der Nutzer, wenn es funktioniert. Wenn etwas davon nicht funktioniert, hört der Sender plötzlich auf zu spielen, und der Nutzer denkt: „Die App ist kaputt." Aus seiner Sicht ist das auch korrekt. Für ihn ist die App alles, was er auf dem Bildschirm sieht.
Fazit: Eine App ist mehr als eine App
Wenn ich die Zeit ehrlich aufrechne, geht ungefähr die Hälfte der Arbeit in das, was Nutzer sehen, und ungefähr die andere Hälfte in das, was sie nie sehen, aber jeden Tag benutzen. Vielleicht ist es sogar weniger als die Hälfte, die sichtbar ist. Schwer zu sagen, weil die unsichtbaren Teile selten in einem einzelnen Sprint stecken, sondern sich über Monate verteilen.
Was ich aus diesen 15 Monaten gelernt habe: Eine App, die ernsthaft mehr sein soll als ein Spielzeug, ist immer ein System. Frontend, Backend, APIs, Infrastruktur — und an jeder dieser Stellen die Entscheidung, was man selbst macht, was man auslagert und wo der Preis dafür liegt.
Im nächsten Post geht es zurück an die Oberfläche: zur Visualizer-Odyssee. Vom geerbten Web-Visualizer über den iOS-MVP bis zum nativen Audio-Proxy, der am Ende immer läuft. Inklusive der Phase, in der ich ernsthaft mit Fake-Daten experimentiert habe, weil Safari nichts anderes hergab.
Radio Adfree ist eine werbefreie Radio-App für iOS und Android mit über 40.000 Sendern, Song-Erkennung, Podcasts und Stream-Aufnahme.