Saat Fitur Baru Menjangkau Sistem Lama

Salah satu mode kegagalan yang sering muncul di aplikasi React Native hasil vibe coding adalah ketika fitur baru mengimpor service yang sudah ada secara langsung, menyentuh state yang bukan miliknya, lalu merusak flow lain yang tidak terkait. Auth sering jadi korban karena ia cenderung berubah menjadi dependency bersama semua orang.

Dalam aplikasi yang dirakit cepat dengan Cursor dan Claude Code, setiap fitur dihasilkan dalam konteks lokal. Kodenya compile. Bahkan mungkin lulus happy path. Tetapi tanpa service boundaries, file-file yang tampak independen gagal bersama.

Inilah masalah fundamental dari vibe coding saat scale. AI coding assistants adalah arsitek yang buta konteks. Mereka menghasilkan apa yang Anda minta, bukan apa yang dibutuhkan codebase Anda. Tanpa service boundaries yang eksplisit, setiap feature baru adalah potensi breaking change yang tinggal menunggu waktu.

Aturan Interface

Aturannya sederhana: setiap service di aplikasi Anda mengekspos sebuah interface. Interface itu hidup di codebase Anda. Implementasinya - entah Supabase, Firebase, RevenueCat, atau mock - berada di balik interface tersebut. Sisa aplikasi Anda tidak tahu yang mana yang dipakai.

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

Inilah seam-nya. Semua yang ada di sisi aplikasi berbicara ke AuthService. Semua yang ada di sisi SDK mengimplementasikannya. Saat Anda ingin mengganti Supabase dengan Firebase, Anda menulis satu implementation class baru. Login screen tidak berubah. Route guards tidak berubah. Hooks tidak berubah. Claude Code tidak perlu tahu bahwa ada pergantian.

Composition Root

Satu file membangun seluruh dependency graph. Di semua tempat lain, Anda hanya mengonsumsinya.

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

Ingin mengganti Supabase ke Firebase? Ubah satu baris. Berpindah dari Segment ke PostHog? Satu baris. Inilah bentuk arsitektur React Native yang scalable saat Anda melakukan vibe coding dengan bantuan AI.

Kenapa Ini Lebih Penting pada Aplikasi Hasil AI

Saat manusia menulis kode, mereka menyimpan arsitektur di kepala. Mereka tahu untuk tidak mengimpor supabase langsung ke screen component. AI coding assistants tidak menyimpan arsitektur di kepala. Mereka menyimpan context window. Dan context window lupa.

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

File ini tidak berubah sepanjang tiga migrasi backend yang berbeda. Ia tidak tahu seperti apa supabase.auth.signInWithPassword. Ia tahu auth.signIn(). Itulah keseluruhan maksudnya.

Autotomy Expo Starter Pack sudah membawa arsitektur ini secara bawaan - service interfaces, composition root, barrel exports - semuanya sudah dirangkai agar Cursor dan Claude Code menghasilkan kode yang menghormati boundaries Anda sejak hari pertama.