新しい feature が古い system に手を伸ばすとき
Vibe-coded な React Native app で繰り返し起きる failure mode のひとつは、新しい feature が既存 service を直接 import し、自分の ownership ではない state に触れて、無関係な flow を壊してしまうことです。auth がよく巻き込まれるのは、誰にとっても shared dependency になりがちだからです。
Cursor と Claude Code で短期間に組み上げた app では、各 feature はその場の local context で生成されます。code は compile する。happy path も通るかもしれない。でも service boundary がないと、独立して見える file 同士が一緒に壊れます。
これが scale した Vibe coding の根本問題です。AI coding assistant は context-blind な architect です。生成するのは、あなたが頼んだもの。codebase が必要としているものではありません。明示的な service boundary がない限り、新しい feature は常に breaking change の候補になります。
Interface Rule
ルールは単純です。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 を 1 つ書けばいい。login screen は変わらない。route guard も変わらない。hook も変わらない。Claude Code は swap が起きたことを知る必要すらない。
Composition Root
依存グラフを構築する file は 1 つだけにする。それ以外では 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(),
}
}
Supabase を Firebase に替えたい? 1 行変えるだけ。 Segment を PostHog に替えたい? やはり 1 行。 これが、AI assistance と一緒に Vibe coding しながらも scale できる React Native architecture の姿です。
なぜ AI-coded app ではこれがもっと重要なのか
人間がコードを書くときは、architecture を頭の中に持っています。supabase を screen component に直接 import してはいけないことも分かっている。でも AI coding assistant が持っているのは architecture ではなく 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>
)
}
この file は 3 回の backend migration を経ても変わっていません。supabase.auth.signInWithPassword がどういう API かを知らない。知っているのは auth.signIn() だけです。それが本質です。
Autotomy Expo Starter Pack には、この architecture が最初から組み込まれています。service interface、composition root、barrel export まで全部配線済みなので、Cursor と Claude Code が初日から境界を尊重したコードを生成できます。