Wenn neue Features in alte Systeme greifen

Ein wiederkehrender Ausfallmodus in vibe-codierten React-Native-Apps ist, dass ein neues Feature einen bestehenden Service direkt importiert, in fremden State greift und damit einen nicht verwandten Flow zerlegt. Auth ist oft das erste Opfer, weil es schnell zur gemeinsamen Abhängigkeit aller wird.

In Apps, die mit Cursor und Claude Code schnell zusammengesetzt wurden, wird jedes Feature in lokalem Kontext generiert. Der Code kompiliert. Vielleicht besteht er sogar den Happy Path. Aber ohne Service Boundaries fallen unabhängige Dateien gemeinsam um.

Das ist das Kernproblem von Vibe Coding im Maßstab. KI-Coding-Assistenten sind Architekten ohne Kontext. Sie generieren, worum du bittest - nicht das, was deine Codebase braucht. Ohne explizite Service Boundaries ist jedes neue Feature eine potenzielle Breaking Change, die nur darauf wartet, zuzuschlagen.

Die Interface-Regel

Hier ist die Regel: Jeder Service in deiner App exponiert ein Interface. Das Interface lebt in deiner Codebase. Die Implementierung - ob Supabase, Firebase, RevenueCat oder ein Mock - sitzt hinter diesem Interface. Der Rest deiner App weiß nicht, welche davon gerade aktiv ist.

// src/services/auth/auth.interface.ts
// Your app owns this. Not Supabase. Not Firebase. You.

export interface User {
  id: string
  email: string
  displayName: string | null
  avatarUrl: string | null
}

export interface Session {
  accessToken: string
  refreshToken: string
  expiresAt: number
  user: User
}

export interface AuthService {
  signIn(credentials: EmailCredentials): Promise<Session>
  signOut(): Promise<void>
  getSession(): Promise<Session | null>
  onAuthStateChange(
    callback: (session: Session | null) => void
  ): () => void
}

Das ist der Seam. Alles auf der App-Seite spricht mit AuthService. Alles auf der SDK-Seite implementiert ihn. Wenn du Supabase gegen Firebase austauschen willst, schreibst du genau eine neue Implementierungsklasse. Der Login-Screen ändert sich nicht. Die Route Guards ändern sich nicht. Die Hooks ändern sich nicht. Claude Code muss nicht einmal wissen, dass ein Wechsel stattgefunden hat.

Der Composition Root

Eine Datei konstruiert den gesamten Dependency Graph. Überall sonst konsumierst du nur.

// src/services/create-services.ts
// ONE file. Cursor never touches this. You own it.

import { config } from '@/constants/config'
import { SupabaseAuthService } from './auth/supabase'
import { RevenueCatService } from './payments/revenuecat'

export type Services = ReturnType<typeof createServices>

export function createServices() {
  const storage = createStorageServices(config.storageMode)

  return {
    storage,
    auth: new SupabaseAuthService(storage.secureStorage),
    payments: new RevenueCatService(),
    analytics: new SegmentAnalyticsService(),
    crashReporting: new SentryService(),
    network: createNetworkService(),
  }
}

Du willst Supabase gegen Firebase tauschen? Ändere eine Zeile. Du willst von Segment zu PostHog wechseln? Eine Zeile. Genau so sieht skalierbare React-Native-Architektur aus, wenn du mit KI-Unterstützung vibe codest.

Warum das in KI-codierten Apps noch wichtiger ist

Wenn ein Mensch Code schreibt, trägt er die Architektur im Kopf. Er weiß, dass er supabase nicht direkt in eine Screen-Komponente importieren sollte. KI-Coding-Assistenten tragen keine Architektur im Kopf. Sie haben Context Windows. Und Context Windows vergessen.

// src/context/auth-provider.tsx
// This provider doesn't know what auth SDK you use.
// Claude Code generates against the interface, never the implementation.

export function AuthProvider({ children }: PropsWithChildren) {
  const { auth } = useServices()
  const [session, setSession] = useState<Session | null>(null)

  useEffect(() => {
    auth.getSession().then(setSession)
    return auth.onAuthStateChange(setSession)
  }, [auth])

  const signIn = useCallback(
    async (creds: AuthCredentials) => {
      const s = await auth.signIn(creds)
      setSession(s)
    },
    [auth]
  )

  return (
    <AuthContext.Provider
      value={{ session, user: session?.user ?? null, signIn }}>
      {children}
    </AuthContext.Provider>
  )
}

Diese Datei hat drei verschiedene Backend-Migrationen überlebt, ohne sich zu ändern. Sie weiß nicht, wie supabase.auth.signInWithPassword aussieht. Sie kennt nur auth.signIn(). Genau darum geht es.

Das Autotomy Expo Starter Pack bringt diese Architektur bereits mit - Service Interfaces, Composition Root, Barrel Exports -, alles so verdrahtet, dass Cursor und Claude Code von Tag eins an Code generieren, der deine Boundaries respektiert.