# Manual Setup — Bundle Drop Docs

> Manually configure Bundle Drop for bare React Native or Expo when guided setup cannot safely update a customized project.

Canonical: https://bundledrop.app/docs/manual-setup

## When to Use Manual Setup

Use this guide when you prefer to integrate without AI-assisted setup, when `bundle-drop login` or `bundle-drop init` cannot safely update a customized project, or when a guided setup plan returns low confidence. This is a complete supported setup path for bare React Native and Expo projects. For standard projects, the reviewed CLI setup in [Installation](/docs/installation) remains the quickest option.

Manual setup changes how the native app chooses its JavaScript bundle at cold start. Preserve your existing Debug, package-list, lifecycle, and embedded-bundle behavior. After changing native or Expo configuration, create a new native binary before testing OTA updates.

## Before You Edit Native Files

Install the package and create `bundle.drop.config.js` before running CLI login. Copy the project values shown in the Bundle Drop dashboard and keep the runtime version aligned with the native binary you are building:

**bundle.drop.config.js**

```js
module.exports = {
  projectType: "bare",
  serverUrl: "https://api.bundledrop.app",
  defaultChannel: "develop",
  runtimeVersion: {
    ios: "1.0.0",
    android: "1.0.0",
  },
  org: { slug: "your-org-slug" },
  project: {
    name: "Your App Name",
    slug: "your-app-slug",
    apiKey: "your-project-api-key",
  },
};
```

Use `projectType: "expo"` for an Expo app. The project identity and `project.apiKey` are public app configuration, not a Personal Access Token or private signing key. Commit `bundle.drop.config.js`; keep CLI authentication files and Personal Access Tokens out of source control.

With the config already present, authenticate from the app directory. `login` recognizes the existing project config and synchronizes it without running AI-assisted setup:

**Terminal**

```bash
npx bundle-drop login
```

React Native autolinking owns the Android package and iOS pod. Do not manually add `BundleDropPackage`, an Android Gradle project, or a manifest entry. Bare iOS projects still need pods installed:

**npm — Terminal**

```bash
npm install @gfean/react-native-bundle-drop
cd ios && pod install && cd ..
```

**Yarn — Terminal**

```bash
yarn add @gfean/react-native-bundle-drop
cd ios && pod install && cd ..
```

**Expo — Terminal**

```bash
npx expo install @gfean/react-native-bundle-drop
```

## Bare React Native Metro Setup

Wrap your existing Metro config with `withBundleDrop`. It prepares the runtime configuration required by native builds and resolves `bundle-drop-config`. Keep your existing resolver, transformer, and other Metro customizations inside the config passed to the wrapper.

**metro.config.js**

```js
const { getDefaultConfig, mergeConfig } = require("@react-native/metro-config");
const { withBundleDrop } = require("@gfean/react-native-bundle-drop/metro");

// Keep your app's existing Metro customizations here.
const config = mergeConfig(getDefaultConfig(__dirname), {});

module.exports = withBundleDrop(config, { projectRoot: __dirname });
```

## Android `MainApplication`

Choose the tab that matches the host already present in your app. Current Kotlin `ReactHost` apps must keep that shape. Do not add or recreate `ReactNativeHost` or `DefaultReactNativeHost` solely for Bundle Drop.

The resolver belongs in the native Release bundle path because native selection happens before JavaScript initializes. Returning the existing fallback in Debug keeps Metro in control.

**Current Kotlin ReactHost — MainApplication.kt**

```kt
import com.bundledrop.BundleDropModule

private fun getJSBundleFile(): String? {
  if (BuildConfig.DEBUG) return null
  return BundleDropModule.resolveJSBundleFile(applicationContext, null)
}

override val reactHost: ReactHost by lazy {
  getDefaultReactHost(
    context = applicationContext,
    packageList = PackageList(this).packages.apply {
      // Preserve existing manually added packages.
    },
    jsBundleFilePath = getJSBundleFile(),
    // Preserve every other existing host option.
  )
}
```

**Existing Kotlin host — MainApplication.kt**

```kt
import com.bundledrop.BundleDropModule

// Add this only inside the ReactNativeHost or DefaultReactNativeHost
// object that already exists in the app.
override fun getJSBundleFile(): String? {
  val fallback = super.getJSBundleFile()
  if (BuildConfig.DEBUG) return fallback
  return BundleDropModule.resolveJSBundleFile(applicationContext, fallback)
}
```

**Existing Java host — MainApplication.java**

```java
import com.bundledrop.BundleDropModule;

// Add this only inside the ReactNativeHost or DefaultReactNativeHost
// object that already exists in the app.
@Override
protected String getJSBundleFile() {
    String fallback = super.getJSBundleFile();
    if (BuildConfig.DEBUG) {
        return fallback;
    }
    return BundleDropModule.resolveJSBundleFile(
        getApplicationContext(),
        fallback
    );
}
```

Keep the current template's `loadReactNative(this)` call and every existing package or host option. If your app already has custom bundle selection, evaluate it once and pass that result as `fallback`. When migrating from CodePush, replace the CodePush resolver; do not retain CodePush as Bundle Drop's fallback.

## iOS `AppDelegate`

Add the locator to the object that already owns `bundleURL()`. In older Swift templates that is usually `AppDelegate`; in current factory-based templates it is commonly the nested `ReactNativeDelegate`. If `sourceURL(for:)` already delegates to `bundleURL()`, keep it unchanged.

**Swift — AppDelegate.swift**

```swift
import BundleDrop

override func bundleURL() -> URL? {
#if DEBUG
  return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
#else
  return BundleDropLocator.bundleURL()
    ?? Bundle.main.url(forResource: "main", withExtension: "jsbundle")
#endif
}
```

**Objective-C / Objective-C++ — AppDelegate.m / AppDelegate.mm**

```objc
#import <BundleDrop/BundleDropLocator.h>

- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
{
#if DEBUG
  return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
#else
  NSURL *bundleDropURL = [BundleDropLocator bundleURL];
  if (bundleDropURL != nil) {
    return bundleDropURL;
  }
  return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
#endif
}
```

Preserve a custom Metro bundle root and the app's existing embedded Release fallback. If the Objective-C delegate already has `sourceURLForBridge:` call `[self bundleURL]`, put the Debug/Release selection in `-bundleURL` instead of duplicating it.

## Expo Manual Setup

Expo projects use the Bundle Drop config plugin and Metro wrapper. Do not manually patch generated Android `MainApplication` or iOS `AppDelegate` files. Expo autolinking and the config plugin own those generated seams, and managed/CNG projects can keep `ios/` and `android/` absent until prebuild, `expo run:*`, or EAS Build.

Register the plugin exactly once in the authoritative Expo app configuration. Preserve every unrelated plugin and config value. For `app.json`, add the package to the existing `expo.plugins` array instead of replacing the file.

**app.json — app.json**

```json
{
  "expo": {
    "plugins": [
      "@gfean/react-native-bundle-drop"
    ]
  }
}
```

**Dynamic config — Merge inside app.config.***

```ts
const bundleDropPlugin = "@gfean/react-native-bundle-drop";
const existingPlugins = config.plugins ?? [];
const hasBundleDropPlugin = existingPlugins.some((plugin) =>
  (typeof plugin === "string" ? plugin : plugin[0]) === bundleDropPlugin
);

// Merge this into the object your existing config function already returns.
return {
  ...config,
  plugins: hasBundleDropPlugin
    ? existingPlugins
    : [...existingPlugins, bundleDropPlugin],
};
```

The dynamic-config tab is an insertion pattern, not a replacement `app.config.*` file. Apply the duplicate check inside the existing config function and keep its environment reads, computed values, plugins, and returned fields.

Preserve Expo's Metro defaults and wrap the final config:

**metro.config.js**

```js
const { getDefaultConfig } = require("expo/metro-config");
const { withBundleDropExpo } = require("@gfean/react-native-bundle-drop/metro");

module.exports = withBundleDropExpo(getDefaultConfig(__dirname), {
  projectRoot: __dirname,
});
```

Finally, add `projectType: "expo"` to the object already exported by `bundle.drop.config.js`. Do not replace the file: keep its generated `serverUrl`, organization, project, and per-platform runtime versions unchanged.

If the project uses a custom Metro config, pass that completed config to `withBundleDropExpo` rather than replacing it with the short example.

### Remove Active `expo-updates`

If active `expo-updates` is present, stop: both systems cannot own native startup in the same binary. When the guided planner is available, prefer its reviewed migration preview:

**Terminal**

```bash
npx bundle-drop init --project-type expo --migrate-expo-updates
```

If guided setup is unavailable or returned low confidence, migrate manually:

1. Remove the direct `expo-updates` dependency with the package manager already used by the project.
2. In the authoritative Expo config, remove every `expo-updates` plugin entry, whether it is a string or a tuple such as `["expo-updates", { ... }]`. Preserve every unrelated plugin, and keep the Bundle Drop plugin exactly once.
3. Remove only `updates.url` and `updates.enabled` from the Expo config. Preserve other `updates` fields; remove the `updates` object only if it becomes empty.
4. Keep the Bundle Drop Metro wrapper and `projectType: "expo"` configuration described above.
5. Generate and install a new native binary. An existing binary still contains the old startup owner.

**npm — Terminal**

```bash
npm uninstall expo-updates --legacy-peer-deps
```

**Yarn — Terminal**

```bash
yarn remove expo-updates
```

**pnpm — Terminal**

```bash
pnpm remove expo-updates
```

**Bun — Terminal**

```bash
bun remove expo-updates
```

Expo Go does not include the Bundle Drop native adapter. Debug and development-client builds intentionally stay on Metro. Use a non-Debug build to test installed OTA startup:

**Local — Terminal**

```bash
npx expo run:ios --configuration Release
npx expo run:android --variant release
```

**EAS Build — Terminal**

```bash
eas build --platform ios
eas build --platform android
```

## Initialize and Verify

Native setup only chooses the startup bundle. You must also call `BundleDrop.init` once before app registration, or in the Expo Router root layout. Use the complete examples in [Installation](/docs/installation#initialize-bundle-drop-in-javascript) and [Expo](/docs/expo#initialize-with-expo-router).

Synchronize the runtime-delivery trust bootstrap after the manual edits, then run diagnostics for the project type and platform you changed:

**Bare React Native — Terminal**

```bash
npx bundle-drop sync
npx bundle-drop doctor --project-type bare --platform android
npx bundle-drop doctor --project-type bare --platform ios
```

**Expo — Terminal**

```bash
npx bundle-drop sync
npx bundle-drop doctor --project-type expo
```

`sync` writes `.bundle-drop/runtime-delivery.lock.json`, repairs its `.gitignore` exception, and recreates the file if it was accidentally deleted. This lockfile contains public trust configuration, not private credentials. Commit it together with `bundle.drop.config.js` and `.gitignore`. Projects using the former `runtime-delivery.generated.json` filename remain readable; running `sync` migrates a valid legacy file to the lockfile after validation.

**Terminal**

```bash
git add bundle.drop.config.js .bundle-drop/runtime-delivery.lock.json .gitignore
```

The doctor may warn until Android autolinking output or `Podfile.lock` exists. After integration, build and install a new native binary. In manual compatibility mode, bump the affected platform's runtime when its native boundary changes. For opt-in fingerprint or hybrid compatibility, use the [native fingerprinting setup](/docs/native-fingerprinting) and rebuild with the matching derived identity; see [Runtime Version](/docs/runtime-version) to compare modes.
