Expo SDK
@appspacer/expo — Asset OTA updates and crash reporting for Expo managed workflow apps.
Update model: Because Expo managed workflow does not allow custom native modules or JS bundle hot-swapping, AppSpacer for Expo delivers asset OTA updates — images, JSON configs, string files, and any other data your app fetches at runtime. Your app reads from the OTA cache first, falling back to bundled assets when nothing is cached yet.
Requirements
| Minimum | |
|---|---|
| Expo SDK | >= 50 |
| React Native | >= 0.73 |
expo-file-system | >= 17 |
expo-crypto | >= 13 |
expo-network | >= 6 |
Installation
npx expo install @appspacer/expo expo-file-system expo-crypto expo-network expo-applicationInitialization
Call AppSpacer.init() once in your root component or app/_layout.tsx before anything else:
import { AppSpacer } from '@appspacer/expo';
export default function RootLayout() {
useEffect(() => {
AppSpacer.init({ apiKey: 'your_api_key' });
}, []);
return <Slot />;
}Configuration Options
| Option | Type | Default | Description |
|---|---|---|---|
apiKey | string | Required | Your AppSpacer API key |
appVersion | string | Application.nativeApplicationVersion | Override the app version |
updateStrategy | 'onInit' | 'background' | 'manual' | 'onInit' | When to check for updates |
debugLogging | boolean | false | Print verbose logs |
crashReportingEnabled | boolean | true | Capture JS crashes automatically |
maxBreadcrumbs | number | 50 | Max breadcrumbs stored per session |
Serving OTA Assets
After init, read cached assets anywhere in your app. Always provide a fallback to the bundled asset:
import { AppSpacer } from '@appspacer/expo';
import { Image } from 'expo-image';
export function HeroBanner() {
const [src, setSrc] = useState(require('./assets/hero.png'));
useEffect(() => {
AppSpacer.readAssetBytes('images/hero.png').then(bytes => {
if (bytes) {
const base64 = Buffer.from(bytes).toString('base64');
setSrc({ uri: `data:image/png;base64,${base64}` });
}
});
}, []);
return <Image source={src} style={{ width: '100%', height: 200 }} />;
}Reading JSON config
const flags = await AppSpacer.readAssetJson('config/feature-flags.json');
const showNewCheckout = flags?.new_checkout ?? false;Manual Update Check
const result = await AppSpacer.checkForUpdates();
if (result.status === OtaUpdateStatus.updated) {
console.log(`Updated ${result.updatedCount} asset(s). Bundle: ${result.newBundleId}`);
}API Reference
| Method | Returns | Description |
|---|---|---|
AppSpacer.init({ apiKey }) | Promise<void> | Initialize — call once at app startup |
AppSpacer.checkForUpdates() | Promise<OtaUpdateResult> | Check and download new assets |
AppSpacer.readAssetBytes(key) | Promise<Uint8Array | null> | Read cached asset as raw bytes |
AppSpacer.readAssetString(key) | Promise<string | null> | Read cached asset as UTF-8 text |
AppSpacer.readAssetJson(key) | Promise<Record | null> | Read and parse cached JSON asset |
AppSpacer.clearAssetCache() | Promise<void> | Delete all cached assets |
AppSpacer.cacheSize() | Promise<number> | Total cached bytes |
AppSpacer.activeBundleInfo() | Promise<Record | null> | Metadata about the active bundle |
AppSpacer.reportError(ex) | Promise<void> | Report a handled error |
AppSpacer.addBreadcrumb(msg) | void | Add a breadcrumb for crash context |
AppSpacer.sessionId | string | Current session ID |
Backend Integration
GET /api/sdk/expo/manifest
Headers: x-api-key: <your_api_key>
Query: app_version, current_bundle_id, install_id
POST /api/sdk/expo/report-status
Body: { release_id, device_id, app_version, status, error_message }Send
install_id(a stable per-device ID) on every manifest call — it is required for staged-rollout targeting and monthly active-user metering. Reportinstalled/failedback toreport-statusso egress is billed once per device and failed installs can trigger auto-rollback.
Returns 204 No Content when up to date, or a manifest JSON when an update is available.
Upload your asset bundles from the AppSpacer dashboard or CLI and set platform: expo on the release.