# Building an Apple App Clip with React Native & Expo

When building a mobile application, one of the biggest conversion bottlenecks is download friction.

If someone comes to our website [svastha.co](https://svastha.co) through Safari, a social post, or an iMessage link, we'd like to provide a seamless experience for them to download the app and start the experience.

That led us to **Apple App Clips**: instant, bite-sized mini-apps that launch card previews on iOS without downloading the full binary.

While the end-user experience is magical, building an App Clip inside a modern **React Native (Expo)** codebase turned out to be an engineering obstacle course. Here is a breakdown of how the architecture works, the roadblocks we hit, how we solved provisioning profile mismatches in Apple Developer Portal, and the key lessons learned.

![](https://cdn.hashnode.com/uploads/covers/5fa451743e634314b51795a2/6880c3c9-a392-4ad7-b6dd-a0b39324eff8.png align="center")

### The Architecture: How App Clips Work with React Native

An App Clip is not a web view, nor is it a separate App Store listing. It is a secondary native target (`your.bundle.id.Clip`) embedded inside your main application's `.ipa` bundle.

Using an Expo config plugin like `react-native-app-clip`, the build toolchain automates:

*   Creating the Xcode target (`YourAppClip`) alongside the main app.
    
*   Generating dedicated `Info.plist` and `.entitlements` files for the clip.
    
*   Setting up CocoaPods autolinking for the App Clip target.
    

In JavaScript / TypeScript, your root component checks whether it is running within the clip target:

```tsx
import { isClip, displayOverlay } from "react-native-app-clip";
import AppClipScreen from "./src/clip/AppClipScreen";

export default function App() {
  if (isClip()) {
    return <AppClipScreen />;
  }
  return <FullAppNavigator />;
}
```

The user instantly sees the requested content with zero sign-up friction. If they want deeper features (such as saved preferences, account sync, or notifications), tapping a button invokes Apple’s native `SKOverlay`:

```tsx
displayOverlay(); // Native iOS App Store sheet slides up from the bottom!
```

## Lesson 1: The App Clip Size Trap (Error ITMS-90865)

## **The Problem**

During submission to App Store Connect, Apple rejected the binary with:

> **ITMS-90865: Thinned App Clip size is too large** — The main bundle of the App Clip is 46 MB, which exceeds the maximum allowable size. After app thinning, the main bundle of any App Clip variant must be less than 10 MB.

### Why Did This Happen?

*   **Apple's Historic Limits**: For iOS 15 and iOS 16, Apple limits the uncompressed main binary of an App Clip to **10 MB – 15 MB**.
    
*   **The Weight of React Native**: Under modern React Native with the New Architecture and Hermes, the underlying prebuilt frameworks (`React.framework`, `hermesvm.framework`, and runtime dependencies) easily exceed 40 MB uncompressed.
    
*   When your build tool links React Native and Hermes into the App Clip target, the uncompressed payload reaches **40–50 MB**, instantly triggering Apple's rejection.
    

### The Fix

Apple expanded App Clip limits starting in **iOS 17.0**: for digital invocations (Safari Smart App Banners and universal links), the maximum allowable App Clip size was increased to **100 MB**.

In `app.json`, configure your App Clip plugin with `deploymentTarget: "17.0"`:

```json
[
  "react-native-app-clip",
  {
    "name": "Your App Clip Name",
    "targetSuffix": "Clip",
    "bundleIdSuffix": "Clip",
    "groupIdentifier": "group.your.bundle.id",
    "deploymentTarget": "17.0"
  }
]
```

By raising the App Clip target to `17.0` (while leaving your main app's deployment target at iOS 15.1+), the 46 MB bundle easily qualifies under the 100 MB allowance. *(Users on iOS 16 or earlier simply open your web experience or main App Store page).*

## Lesson 2: EAS CLI & Apple Capability Sync Failures

### The Problem

When running an automated EAS build, the build crashed during the credential setup phase:

```text
Failed to patch capabilities: [
  { capabilityType: 'ONDEMANDINSTALL_EXTENSIONS', option: 'ON' },
  { capabilityType: 'APP_GROUPS', option: 'OFF' }
]
✖ Failed to sync capabilities
Apple API error: The request entity is not a valid request document object - Unexpected or invalid value at 'data.relationships.bundleIdCapabilities.data.[0].attributes'
```

### Why Did This Happen?

When EAS Build prepares an iOS build, it calls Apple’s App Store Connect API to auto-configure capabilities on every registered Bundle Identifier.

App Clips have unique constraints:

1.  They require a `parentBundleId` relationship in the Apple API that automated CLI patchers often omit.
    
2.  Apple’s API schema for App Clip capabilities like `ONDEMANDINSTALL_EXTENSIONS` differs from standard apps.
    

### The Fix

Bypass automated capability syncing by passing `EXPO_NO_CAPABILITY_SYNC=1`:

```bash
EXPO_NO_CAPABILITY_SYNC=1 npx eas-cli build --platform ios ...
```

Add this flag directly to your `package.json` release scripts:

```json
"release:ios": "npm run release:preflight:ios && EXPO_NO_CAPABILITY_SYNC=1 npx eas-cli build --platform ios --profile production --auto-submit --non-interactive"
```

You can then configure capabilities manually in the Apple Developer Portal once, and builds will proceed without API schema conflicts.

## Lesson 3: Certificate Mismatch & Provisioning Profile Regeneration

When adding an App Clip or modifying an existing App ID's capabilities, Apple flags:

> *"Adding or removing any capabilities will invalidate any provisioning profiles that include this App ID and they must be regenerated for future use."*

If your distribution certificates or provisioning profiles fall out of sync, your build will fail in Fastlane with:

```text
Provisioning profile doesn't include signing certificate "Apple Distribution: ...".
Provisioning profile doesn't include the com.apple.developer.associated-domains entitlement.
```

### How to Regenerate Profiles and Sync Credentials Cleanly

#### Step 1: Align Your Distribution Certificate in Apple Developer Portal

1.  Go to **Apple Developer Portal > Certificates, Identifiers & Profiles > Profiles**.
    
2.  If your account switched from an Individual to an Organization/Company account, ensure all active Provisioning Profiles are linked to the current **Company Distribution Certificate**.
    
3.  Open your App ID (`your.bundle.id`) and App Clip ID (`your.bundle.id.Clip`) and verify that required capabilities (e.g. **Associated Domains** and **App Groups**) are checked and saved.
    

#### Step 2: Regenerate via EAS Credentials Manager

Rather than guessing which local cert matches:

1.  Run:
    
    ```bash
    EXPO_NO_CAPABILITY_SYNC=1 npx eas-cli credentials
    ```
    
2.  Select **iOS** > **production** (or your active build profile).
    
3.  Select **Provisioning Profile** > **Delete Provisioning Profile**.
    
4.  Select **Generate a new Apple Provisioning Profile** > choose **Yes**.
    
5.  Repeat for the App Clip target if prompted.
    
6.  EAS will pull the updated App ID configuration from Apple Developer, bind it to your active Distribution Certificate, and cache the new `.mobileprovision` file.
    

## Lesson 4: Web-to-Clip Handshake (`apple-app-site-association`)

An App Clip will not trigger from the web unless the domain handshake is complete:

### 1\. Smart App Banner Meta Tag

In your website’s HTML `<head>`:

```html
<meta
  name="apple-itunes-app"
  content="app-id=YOUR_APP_APPLE_ID, app-clip-bundle-id=your.bundle.id.Clip, app-clip-display=card"
/>
```

### 2\. Apple App Site Association (AASA)

Hosted with `Content-Type: application/json` at `https://yourdomain.com/.well-known/apple-app-site-association`:

```json
{
  "appclips": {
    "apps": ["<TEAM_ID>.your.bundle.id.Clip"]
  },
  "applinks": {
    "apps": [],
    "details": [
      {
        "appID": "<TEAM_ID>.your.bundle.id",
        "paths": ["*", "/shared/*"]
      }
    ]
  }
}
```

### 3\. iOS Associated Domains Entitlement

In `app.json`:

```json
"associatedDomains": [
  "applinks:yourdomain.com",
  "appclips:yourdomain.com"
]
```

* * *

## Summary Checklist

| Topic | Solution |
| --- | --- |
| **Size Exceeded (ITMS-90865)** | Set `deploymentTarget: "17.0"` in plugin options to use Apple's 100 MB digital invocation limit for React Native / Hermes bundles. |
| **Capability Sync API Failure** | Add `EXPO_NO_CAPABILITY_SYNC=1` to your build script so EAS skips fragile capability patching. |
| **Invalidated Profiles** | Delete outdated provisioning profiles in `eas-cli credentials` and let EAS regenerate fresh profiles against your current Distribution Certificate. |
| **Apple Review Guidelines** | Ensure your App Clip delivers instant utility without a mandatory login barrier (Apple will reject clips that block users behind an auth wall). |
