Deine api hat
längst einen Mobile-Client.

Eine Mobile-App ist der dritte Konsument der api, die du ohnehin geschrieben hast — kein zweites Backend, keine zweiten Endpunkte, keine handgeschriebene Fetch-Schicht. Ein Befehl scaffoldet eine Expo-App, die dieselben typisierten Prozeduren spricht wie deine Web-App, sich über denselben Supervisor neu verbindet und offline öffnet, weil ein Telefon einen Teil seines Lebens dort verbringt.

terminal
bash
# The api you already have — plus a phone.
voltro add-app mobile --template=mobile-app --to acme

cd apps/mobile && pnpm ios      # Expo owns Metro, not `voltro dev`

# The same call your web screens make:
#   const notes  = useSubscription('notes.list')
#   const create = useMutation('notes.create')
#
# What you did NOT write:
#  - a mobile-only endpoint layer
#  - row types copied from the server
#  - a reconnect loop
#  - an offline queue

Was du bekommst, wenn du eine Mobile-App ergänzt.

Dieselben Hooks, auf dem Telefon

`useSubscription('notes.list')` und `useMutation('notes.create')` sind dieselben Aufrufe wie in deinen Web-Screens, gebunden an dieselbe generierte Prozedurkarte. Der Tag vervollständigt sich, ein Tippfehler ist ein Compile-Fehler, und der Zeilentyp wird aus dem Schema der api abgeleitet statt zweimal deklariert.

Ein Reconnect-Supervisor, keine mobile Kopie

Backoff, Generationsverfolgung und die Regel, die verhindert, dass die Zeilen eines angemeldeten Nutzers in den Screens des nächsten auftauchen, sind geteilter Code mit dem Web-Client. Eine zweite Implementierung wäre eine zweite Stelle, an der diese Regel subtil falsch sein kann — und niemand würde die beiden auf Drift beobachten.

Offline ist die Grundhaltung

Local-first an, optimistische Writes, Sync beim Vordergrundwechsel, ein Retry-Plan und ein Verbindungsstatus, den du in die UI stellen kannst. Mobile Nutzer erwarten, dass eine App im Zug aufgeht; die Defaults sagen das, statt es jedes Team neu entdecken zu lassen.

Persistierter Zustand, der einen Kaltstart überlebt

Persistierte Stores funktionieren auf dem Gerät: der Zustand wird aus dem Speicher des Telefons geladen, bevor der erste Screen rendert, danach synchron gelesen und im Hintergrund durchgeschrieben. Writes auf denselben Schlüssel werden zusammengefasst — ein Feld, das bei jedem Tastendruck speichert, kostet einen Roundtrip.

Typisierte Deep Links und Geräteregistrierung

Deklariere Linkmuster und Handler gemeinsam, und ein Matcher routet jede eingehende URL — Universal Link, eigenes Schema, Tap auf eine Benachrichtigung. Die Geräteregistrierung schreibt den Push-Token in eine Framework-Tabelle; die Serverseite von Benachrichtigungen ist damit eine Query statt eines Integrationsprojekts.

Native SDKs, wenn React Native nicht die Antwort ist

Für ein Team, das echtes Swift oder Kotlin will, erzeugt die CLI ein typisiertes natives SDK-Paket aus derselben api-Oberfläche. Es wird aus deinen Prozeduren generiert und kann deshalb nicht von ihnen abdriften, wie ein handgeschriebener Client es tut.

Metro gehört Expo. Wir sagen das.

Das eine, das wir bewusst nicht übernehmen

Metro ist der Dev-Server von Expo, und `voltro dev` startet ihn nicht. Deine api läuft unter `voltro dev`; die App startet mit `pnpm ios` und verbindet sich über das Netz. So zu tun, als gehörte uns diese Schleife, hieße einen Bundler nachzubauen, den wir nicht pflegen wollen — und die Naht ist ehrlich: zwei Dev-Server, eine api, ein Satz Typen.

Dieselben drei Aufrufe, auf einem Telefon.

voltro.mobile.ts
ts
// voltro.mobile.ts — which apis this app talks to
export default {
  apis: { app: { package: '@acme/api' } },
}

// $ voltro codegen
// wrote .framework/mobileApis.generated.ts (apis app)

Der Screen ist derselbe Code, den eine Web-App ausführt. Die Boot-Reihenfolge ist der mobile Teil und sie ist tragend: persistierter Zustand wird vor dem ersten Render hydriert, weil ein Store WÄHREND des Renderns gelesen wird und ein Render nicht awaiten kann.

Wie eine Mobile-App in ein Voltro-Projekt passt.

Brauche ich ein eigenes Backend für die Mobile-App?

Nein — und das ist die ganze Form der Sache. Die api, die du schon betreibst, bedient deine Web-App über einen WebSocket, und die Mobile-App verbindet sich mit demselben, ruft dieselben Prozeduren mit denselben Autorisierungsregeln auf. Es gibt keine mobile Endpunktschicht, kein zweites Serialisierungsformat und kein BFF, das im Gleichschritt bleiben muss.

Die Bindung zwischen beiden wird generiert. Eine kleine Datei nennt die apis, mit denen die App spricht; die CLI macht daraus das Modul, das die App importiert — mit der Prozeduroberfläche und den Cache-Metadaten, die optimistische Updates steuern. Die Typen selbst reisen über den Import des api-Pakets, eine Schema-Änderung erreicht das Telefon also ohne Neugenerierung.

Was das wegnimmt, ist die Schicht, die die meisten Teams doppelt schreiben: ein handgepflegter Mobile-Client, dessen Zeilentypen Kopien der Servertypen sind. Kopien driften, und die Drift zeigt sich als Absturz auf einem Gerät statt als roter Build.

Was passiert, wenn die Verbindung abreißt?

Der Supervisor wählt mit exponentiellem Backoff neu und tauscht die neue Verbindung ein, sobald sie offen ist. Screens werden dabei nicht leer: der neue Cache wird mit den Zeilen des vorigen vorbelegt, ein Tunnel oder ein Aufzug degradiert also zu leicht veralteten Daten statt zu einer Seite voller Skelette.

Dieses Vorbelegen endet an genau einer Grenze, und diese Grenze ist der Grund, warum der Code geteilt und nicht kopiert ist. Wird der Reconnect durch eine Anmeldung, eine Abmeldung oder einen Mandantenwechsel ausgelöst, werden die Caches stattdessen geleert — der nächste Nutzer darf womöglich strikt weniger sehen, und Zeilen über diesen Tausch zu tragen hieße, die Daten einer Person in die Screens einer anderen zu malen.

Verbindungsgesundheit ist eine UI-Oberfläche, kein versteckter Zustand. Ein Status aus connected, degraded oder offline entsteht aus dem echten Erreichbarkeitssignal des Geräts plus den Fehlern, die die App meldet — ein Offline-Banner erscheint also, weil das Telefon offline ist, und nicht, weil zufällig ein Request fehlschlug.

Wie verhält sich Offline tatsächlich?

Die App geht auf. Persistierter Zustand wird vom Gerät geladen, bevor der erste Screen rendert — bewusst awaited, denn ein Store wird während des Renderns gelesen, und ein Render kann nicht auf eine Platte warten. Zuerst rendern und danach hydrieren ist genau das, was das Aufblitzen leeren Zustands erzeugt, das jede handgebaute Variante hat.

Writes sind per Default optimistisch: ein Tap aktualisiert den Screen sofort und gleicht ab, wenn der Server bestätigt. Die Haltung ist ein Wert, den du lesen und überschreiben kannst, statt eines Satzes versteckter Defaults — Local-first, Sync beim Vordergrundwechsel, ein Takt, ein Retry-Plan.

Eine bewusste Einschränkung, ausgesprochen statt vergraben: ein Gerät hat keinen synchronen Speicher, persistierter Zustand wird also aus einem Arbeitsspeicher bedient, der beim Start gefüllt wurde. Die Folge ist die awaited Hydrierung oben — und das Framework verweigert eine Persistenzschicht, die es nicht aufzählen kann: ein leerer Cache ist von einem Erststart nicht zu unterscheiden, und das ist die am schwersten zuzuordnende Art von Datenverlust.

Was ist in unserem Repository nicht verifiziert?

Dass die Schleife auf einem Gerät läuft. Jedes Stück oben ist ohne Simulator unit-getestet, und nur ein Simulator kann beweisen, dass die App unter Metro bootet und eine Live-Query streamt. Im Framework-Repository gibt es keine iOS- oder Android-Toolchain — das Booten der App ist ein Schritt in deiner Expo- oder EAS-Pipeline.

Das sagen wir lieber, als eine Abdeckung anzudeuten, die wir nicht haben. Der Unterschied zählt bei der Stack-Wahl: Transport, Reconnect-Regeln, Persistenzadapter und Codegenerierung sind durch Tests abgedeckt, die du lesen kannst — die Integration mit einer nativen Laufzeit deckst du ab.

Die zwei Dinge, die beim ersten Lauf am häufigsten schiefgehen, sind aus demselben Grund dokumentiert. `localhost` auf einem Telefon ist das Telefon, deshalb wird die Verbindungsadresse von der Maschine abgeleitet, auf der Metro läuft, statt eingebacken zu sein. Und das Erreichbarkeitssignal muss vom Netzwerkmodul des Geräts kommen — die Browser-API, die das Web nutzt, existiert in React Native nicht und meldete ein Telefon im Flugmodus als online.

Was mitkommt — und was nur ein Gerät beweisen kann.

Die mobile Oberfläche und wo jeder Teil bewiesen wird
FähigkeitWas es ist
Typisierte HooksDieselben `useSubscription` / `useMutation` wie im Web, gebunden an die generierte Prozedurkarte der api.
VerbindungGeteilter Reconnect-Supervisor: Backoff, Generationsverfolgung und Cache-Leeren bei Subjektwechsel.
PersistenzPersistierte Stores über den Gerätespeicher, vor dem ersten Render hydriert, durchgeschrieben und zusammengefasst.
ErreichbarkeitEine injizierte Quelle — das Netzwerkmodul des Geräts — treibt den Status connected / degraded / offline.
Deep Links + PushEine typisierte Linktabelle mit Matcher; Geräteregistrierung in eine Framework-Tabelle für serverseitiges Fan-out.
Geräte-BootDein Expo- oder EAS-CI-Schritt — im Framework-Repository gibt es keine mobile Toolchain.

Häufige Fragen

Kann ich meine bestehende Voltro-api mit React Native nutzen?

Ja. Ergänze das Projekt um eine Mobile-App und zeig sie per Paketnamen auf die api; die CLI generiert die Bindung, und die App verbindet sich über denselben WebSocket wie dein Web-Client. Serverseitig ändert sich kein Endpunkt.

Startet `voltro dev` die Expo-App?

Nein, und das ist Absicht. Metro gehört Expo. Die api läuft unter `voltro dev`; die App startet mit ihrem eigenen Befehl und verbindet sich über das Netz — zwei Dev-Server, eine api, ein Satz Typen.

Funktioniert das offline?

Die App öffnet offline mit vor dem ersten Screen geladenem Zustand, schreibt optimistisch und gleicht ab, wenn die Verbindung zurückkommt. Der Sync-Status ist eine erstklassige UI-Oberfläche statt etwas, das du aus fehlgeschlagenen Requests ableitest.

Was ist mit Push-Benachrichtigungen?

Geräteregistrierung und die serverseitige Tabelle sind gebaut — Tokens speichern und an sie ausspielen ist also Framework-Code. Die Sende-Adapter für Apple und Google nicht, weil sie deine eigenen Entwickler-Credentials brauchen; dieser Teil ist wirklich extern, und wir tun nicht so, als wäre er es nicht. Web Push ist die Ausnahme und durchgehend first-party: Payloads nach RFC 8291 verschlüsselt, VAPID-Auth nach RFC 8292, ein geminteter Schlüssel und ein mitgelieferter Service Worker — auf iOS erreicht es allerdings nur installierte Home-Screen-Web-Apps, nie einen Browser-Tab.

Was, wenn wir lieber natives Swift oder Kotlin wollen?

Die CLI erzeugt für beide ein typisiertes SDK-Paket aus derselben api-Oberfläche, generiert aus deinen Prozeduren und daher nicht driftfähig. Das erzeugte Paket zu kompilieren ist ein Schritt in deiner eigenen Mobile-CI.

Womit sich eine Mobile-App verbindet.

Öffne das Framework. Schau es dir selbst an.

Jede Primitive auf dieser Seite ist heute im Framework. Klone den Starter, lass `voltro dev` laufen, in zwei Minuten ist es auf dem Bildschirm.