Cuando una feature nueva se mete en sistemas viejos

Un fallo recurrente en apps React Native hechas con vibe coding es que una feature nueva importa un servicio existente directamente, toca estado que no le pertenece y rompe un flujo no relacionado. Auth suele ser la víctima porque termina convirtiéndose en la dependencia compartida de todo el mundo.

En apps ensambladas deprisa con Cursor y Claude Code, cada feature se genera en contexto local. El código compila. Incluso puede pasar el happy path. Pero sin boundaries de servicio, archivos independientes fallan juntos.

Este es el problema fundamental del vibe coding a escala. Los asistentes de código con IA son arquitectos ciegos al contexto. Generan lo que les pides, no lo que tu codebase necesita. Sin boundaries de servicio explícitos, cada feature nueva es un cambio potencialmente rompedor esperando a ocurrir.

La regla de la interfaz

La regla es esta: cada servicio de tu app expone una interfaz. La interfaz vive en tu codebase. La implementación, ya sea Supabase, Firebase, RevenueCat o un mock, se sienta detrás de esa interfaz. El resto de tu app no sabe cuál es.

// 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
}

Este es el seam. Todo lo que está del lado de la app habla con AuthService. Todo lo que está del lado del SDK lo implementa. Cuando quieres cambiar Supabase por Firebase, escribes una sola clase de implementación nueva. La pantalla de login no cambia. Los route guards no cambian. Los hooks no cambian. Claude Code ni siquiera necesita saber que hubo un cambio.

La composition root

Un archivo construye todo el grafo de dependencias. En todas las demás partes, solo consumes.

// 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(),
  }
}

¿Quieres cambiar Supabase por Firebase? Cambia una línea. ¿Pasar de Segment a PostHog? Una línea. Así es como se ve una arquitectura React Native escalable cuando haces vibe coding con asistencia de IA.

Por qué esto importa aún más en apps codificadas con IA

Cuando una persona escribe código, lleva la arquitectura en la cabeza. Sabe que no debe importar supabase directamente dentro de un componente de pantalla. Los asistentes de código con IA no llevan la arquitectura en la cabeza. Llevan ventanas de contexto. Y las ventanas de contexto olvidan.

// 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>
  )
}

Este archivo no ha cambiado en tres migraciones distintas de backend. No sabe qué forma tiene supabase.auth.signInWithPassword. Sabe auth.signIn(). Ese es todo el punto.

El Autotomy Expo Starter Pack trae esta arquitectura incorporada: interfaces de servicio, composition root y barrel exports, todo cableado para que Cursor y Claude Code generen código que respete tus boundaries desde el día uno.