When Optional SDKs Act Like Core Infrastructure
A common launch failure in vibe-coded mobile apps is that analytics, attribution, or crash reporting gets initialized alongside truly critical services. If one of those optional SDKs misbehaves on a specific device or under a specific network condition, the whole app can die before the first screen.
This happens because AI-generated setup code tends to treat every dependency as equally important. Without an explicit tier system, there is no architectural distinction between must-have infrastructure and nice-to-have observers. Everything becomes must-have by accident.
Three Tiers of Dependencies
Every dependency in your app falls into one of three buckets. Drawing these lines explicitly is the difference between an app that survives growth and one that dies by a thousand production crashes:
- Hard dependencies: If these fail, your app cannot render. Config parsing. Storage creation. Core service construction. These are your foundation.
- Soft dependencies: These can fail gracefully. Auth. Network-dependent features. Permission-dependent flows. The app shell still renders; the user sees a degraded experience, not a crash.
- Optional dependencies: These can fail silently. Analytics. Crash reporting. A/B testing. They never block the app. They are observers, not participants.
The mistake most vibe-coded apps make is treating everything as hard. Every SDK gets initialized at startup. Every failure is a startup crash. But your analytics SDK does not deserve the same status as your storage layer.
The Hard Dependency Boundary
The hard boundary validates config and constructs services. If it fails, the app shows an error screen with a retry button — not a blank white void that users will one-star on the App Store.
// src/context/hard-dependency-boundary.tsx
// This is the gatekeeper. If it fails, nothing else renders.
import * as SplashScreen from 'expo-splash-screen'
void SplashScreen.preventAutoHideAsync()
export function HardDependencyBoundary({
children,
}: {
children: (services: Services) => ReactNode
}) {
const { services, ready, error, retry } =
useHardDependencies()
useEffect(() => {
if (ready || error) void SplashScreen.hideAsync()
}, [ready, error])
if (error) {
return (
<ErrorScreen
title='Startup failed'
message={error.message}
onRetry={retry}
/>
)
}
if (!ready || !services) return null
return <>{children(services)}</>
}
Notice the render prop pattern. The boundary passes initialized services to its children. The children don’t construct services; they receive them. Construction happens exactly once, and any failure is caught at the boundary — not leaked into a production crash report.
The Optional Boundary
Compare that to optional dependencies. These should never crash your app. If Mixpanel goes down, your app doesn’t even hiccup:
// src/context/optional-dependency-boundary.tsx
// If analytics fails, we log a warning and keep going.
export function OptionalDependencyBoundary({
children,
}: PropsWithChildren) {
const { analytics, crashReporting } = useServices()
useEffect(() => {
try {
analytics.track('app_started')
} catch (error) {
if (__DEV__) console.warn('Analytics failed:', error)
}
try {
crashReporting.setContext('app', { started: true })
} catch (error) {
if (__DEV__) console.warn('Crash reporting failed:', error)
}
}, [analytics, crashReporting])
return <>{children}</>
}
The difference in posture is striking. Hard boundary: if it fails, show an error screen. Optional boundary: if it fails, log a warning and move on. Both are explicit. Neither pretends to be the other.
Most production crashes in vibe-coded apps come from one mistake: treating soft or optional dependencies as hard ones. The Autotomy Expo Starter Pack draws these lines from day one so you never learn this lesson at 2 AM.