Когда новые фичи лезут в старые системы
Один из повторяющихся failure mode в vibe-coded React Native-приложениях такой: новая фича напрямую импортирует существующий service, залезает в state, которым не владеет, и ломает несвязанный flow. Auth страдает особенно часто, потому что быстро превращается в общую зависимость для всех.
В приложениях, которые быстро собрали с помощью Cursor и Claude Code, каждая фича генерируется в локальном контексте. Код компилируется. Иногда даже проходит happy path. Но без service boundaries независимые файлы ломаются вместе.
В этом и состоит фундаментальная проблема vibe coding в масштабе. AI coding assistants - архитекторы без зрения на контекст. Они генерируют то, что вы просите, а не то, что нужно кодовой базе. Без явных service boundaries каждая новая фича - это потенциальное breaking change, которое только и ждёт, чтобы случиться.
Правило интерфейса
Правило такое: каждый сервис в приложении публикует интерфейс. Интерфейс живёт в вашей кодовой базе. Реализация - будь то Supabase, Firebase, RevenueCat или mock - находится за этим интерфейсом. Остальная часть приложения не знает, какая именно реализация используется.
// 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. Всё на стороне приложения общается с AuthService. Всё на стороне SDK реализует его. Когда вы захотите сменить Supabase на Firebase, вы просто напишете один новый implementation class. Экран логина не меняется. Route guards не меняются. 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? Одна строка. Вот так выглядит масштабируемая архитектура React Native, когда вы занимаетесь vibe coding с AI-помощником.
Почему это особенно важно в AI-coded приложениях
Когда код пишет человек, архитектура у него в голове. Он знает, что нельзя импортировать supabase напрямую в screen component. AI coding assistants не держат архитектуру в голове. У них есть 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 генерировали код, уважающий ваши границы с первого дня.