当新 feature 把手伸进旧系统
Vibe-coded React Native app 里一个反复出现的 failure mode 是:新 feature 直接 import 现有 service,伸手去改不属于自己的 state,然后把无关的 flow 一起打坏。auth 很容易成为受害者,因为它往往会变成所有人的 shared dependency。
在用 Cursor 和 Claude Code 快速拼出来的 app 里,每个 feature 都是在局部上下文里生成的。代码能 compile,甚至可能通过 happy path。但如果没有 service boundary,看起来彼此独立的 file 最后会一起失效。
这就是 Vibe coding 一旦进入规模化阶段的根本问题。AI coding assistant 是不会感知上下文边界的 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 就够了。login screen 不用改。route guard 不用改。hooks 不用改。Claude Code 甚至不需要知道底层已经发生过一次 swap。
Composition Root
整个 dependency graph 只允许一个 file 负责构建。其他地方只消费,不负责组装。
// 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 assistance 下做 Vibe coding 时,依然能 scale 的 React Native architecture。
为什么这在 AI-coded app 里更重要
当人类自己写代码时,architecture 是装在脑子里的。你知道不该把 supabase 直接 import 到一个 screen component 里。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 已经跨过三次 backend migration 了,但它本身一行都没变。它不知道 supabase.auth.signInWithPassword 长什么样,它只知道 auth.signIn()。这就是重点。
Autotomy Expo Starter Pack 默认把这套 architecture 都配好了。service interface、composition root、barrel export 全都接好线,所以 Cursor 和 Claude Code 从第一天开始生成的代码就会尊重你的边界。