When New Features Reach Into Old Systems

One recurring failure mode in vibe-coded React Native apps is that a new feature imports an existing service directly, reaches into state it does not own, and breaks an unrelated flow. Auth is a common casualty because it tends to become everyone’s shared dependency.

In apps assembled quickly with Cursor and Claude Code, each feature is generated in local context. The code compiles. It may even pass the happy path. But without service boundaries, independent files fail together.

This is the fundamental problem with vibe coding at scale. AI coding assistants are context-blind architects. They generate what you ask for, not what your codebase needs. Without explicit service boundaries, every new feature is a potential breaking change waiting to happen.

The Interface Rule

Here’s the rule: every service in your app exposes an interface. The interface lives in your codebase. The implementation — whether it’s Supabase, Firebase, RevenueCat, or a mock — sits behind that interface. The rest of your app doesn’t know which one it is.

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

This is the seam. Everything on the app side talks to AuthService. Everything on the SDK side implements it. When you want to swap Supabase for Firebase, you write one new implementation class. The login screen doesn’t change. The route guards don’t change. The hooks don’t change. Claude Code doesn’t need to know a swap happened.

The Composition Root

One file constructs the entire dependency graph. Everywhere else, you consume.

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

Want to swap Supabase for Firebase? Change one line. Switch from Segment to PostHog? One line. This is what scalable React Native architecture looks like when you’re vibe coding with AI assistance.

Why This Matters More in AI-Coded Apps

When a human writes code, they hold the architecture in their head. They know not to import supabase directly into a screen component. AI coding assistants don’t hold architecture in their head. They hold context windows. And context windows forget.

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

This file hasn’t changed across three different backend migrations. It doesn’t know what supabase.auth.signInWithPassword looks like. It knows auth.signIn(). That’s the whole point.

The Autotomy Expo Starter Pack ships with this architecture built in — service interfaces, composition root, barrel exports — all wired up so Cursor and Claude Code generate code that respects your boundaries from day one.