當新 feature 把手伸進舊系統

Vibe-coded React Native app 裡一個反覆出現的 failure mode 是:新 feature 直接 import 現有 service,伸手去改不屬於自己的 state,然後把無關的 flow 一起打壞。auth 很容易成為受害者,因為它往往會變成所有人的 shared dependency。

在用 Cursor 和 Claude Code 快速拼起來的 app 裡,每個 feature 都是在局部 context 裡生成的。程式碼能 compile,甚至可能通過 happy path。但只要沒有 service boundary,看起來彼此獨立的 files 最後就會一起失效。

這就是 Vibe coding 一旦進入規模化階段時的根本問題。AI coding assistant 是看不見上下文邊界的 architect。它生成的是你要求它寫的東西,不是你的 codebase 真正需要的東西。沒有明確的 service boundary,每一個新 feature 都是在等著引爆的 breaking change。

Interface 規則

規則很簡單:app 裡的每一個 service 都必須暴露一個 interface。 interface 放在你的 codebase 裡。實作層不管是 Supabase、Firebase、RevenueCat,還是一個 mock,都藏在這個 interface 後面。app 的其他部分根本不需要知道它到底是哪一個。

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

這就是 seam。app 這一側的所有東西都只跟 AuthService 對話。SDK 這一側的所有實作,都只是去滿足它。當你想把 Supabase 換成 Firebase,你只需要寫一個新的 implementation class。login screen 不用改。route guard 不用改。hooks 不用改。Claude Code 甚至不需要知道這次替換有發生。

Composition Root

只用一個檔案建構整張 dependency graph。其他地方一律只負責使用。

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

想把 Supabase 換成 Firebase?改一行。 想把 Segment 換成 PostHog?也是一行。 這就是在 AI 輔助下做 Vibe coding 時,真正能 scale 的 React Native 架構。

為什麼這在 AI 生成 app 裡更重要

當人類工程師寫程式碼時,他們會把架構放在腦子裡。他們知道不該把 supabase 直接 import 到 screen component。AI coding assistant 不會把架構放在腦子裡。它們只有 context window。而 context window 會忘記。

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

這個檔案在三次不同的 backend migration 裡都沒改過。它不需要知道 supabase.auth.signInWithPassword 長什麼樣子。它只知道 auth.signIn()這就是重點。

Autotomy Expo Starter Pack 已經把這套架構內建好了:service interfaces、composition root、barrel exports,全都接線完成,讓 Cursor 和 Claude Code 從第一天起生成的程式碼,就會尊重你的邊界。