# Apps

SplitHub runs in mobile and desktop apps the same way it runs on a website: one install counts screens and events, and runs experiments with the same verdicts. An app and a website can share a project, so a visitor who signs up on the web and buys in the app is one funnel.

## Pick your SDK

| App | SDK | Install |
|---|---|---|
| React Native, Expo | `@splithub/react-native` | `npx splithub init` (detects Expo and React Native) |
| iOS, macOS (Swift) | Swift package `SplitHub` | Xcode → Add Package Dependencies |
| Android (Kotlin) | `io.splithub:splithub` (+ `splithub-compose`) | one Gradle line |

Guides: [React Native and Expo](/docs/react-native), [iOS (Swift)](/docs/ios), [Android (Kotlin)](/docs/android).

The native iOS and Android SDKs are new (version 0.1). They pass the same assignment tests as the web SDK; expect their API to grow.

## What stays the same

- **Assignment** is the same local hash on every platform: no network call on render, and the same visitor id gets the same variant on web, iOS and Android.
- **Events** are `track(name, props?)`. Any name works; mark one as a goal and experiments count it.
- **Verdicts** come from the same sequential test, with the same split check, minimum sample and stars. See [Statistics](/docs/statistics).
- **The CLI and the MCP server** read app experiments and app analytics with the same commands and tools.

## Screens are page views

A screen is a `page` row: the url is the route (`/product/42` with Expo Router) or `/<screen name>` (`/Settings`), the title is the screen name. So every page report, funnel step (`page:/Checkout`) and pageview goal (`{ type: 'pageview', path: '/Checkout' }`) works on screens with no change.

React Native tracks screens for you with `<ExpoRouterScreens />` or `<NavigationScreens />`. Native apps call `SplitHub.screen("Settings")`, or use `.splithubScreen("Settings")` in SwiftUI and `TrackScreen("Settings")` in Compose.

## Why app experiments live in the cloud

A website deploys in minutes. An app waits for store review, then for people to update, and old versions stay in use for months. So for apps:

- The variants ship inside the build: the code for each version you want to try.
- Everything else lives in the dashboard: start, pause, finish, weights, `value` payloads, targeting. Installed apps pick it up on their next launch, with no release.
- New `value` experiments (copy, prices, flags as JSON) can start from the dashboard, as long as the app reads that experiment.

On the very first launch the SDK waits up to 400 ms for the experiments and shows the control meanwhile. After that it keeps a copy on the device, decides at once, and refreshes in the background. A change never flips a variant in the middle of a session.

That is why native apps always use SplitHub Cloud, and why `init` suggests the cloud for React Native too. A React Native app can still keep `splithub.config.ts`; then `npx splithub push` keeps the cloud and the file in agreement, and pushed experiments reach apps whose build has their variants. Redirect experiments don't apply in an app: use `value` or `component`.

## Target app versions

Every event carries the app's platform and version, so a targeting flow can use them:

| Field | Values | Operators |
|---|---|---|
| `platform` | `ios`, `android`, `macos`, `windows`, `linux`, `web` | `is`, `is_not`, `in`, `not_in` |
| `app_version` | `2.4.1` | `gte` (at least), `lt` (below), and the text operators |

```ts
flow: {
  start: 'new-app',
  nodes: {
    'new-app': { type: 'condition', conditions: [{ field: 'app_version', op: 'gte', value: '2.4' }], then: 'split', else: 'out' },
    split: { type: 'split', outputs: [{ weight: 50, to: 'a' }, { weight: 50, to: 'b' }] },
    a: { type: 'variant', variant: 'control' },
    b: { type: 'variant', variant: 'new-checkout' },
    out: { type: 'skip' },
  },
},
```

Versions compare part by part, so `2.10.0` is at least `2.9`. Gate on the first version that ships the new variant's code: older builds see the control and are not counted. More in [Targeting flows](/docs/targeting).

## Offline and sessions

- Events go out in small batches, and right away when the app moves to the background.
- Without a connection they wait on the device (up to 1,000) and go out later with their original time. Today's numbers can still grow a little when phones come back online.
- A new session starts when the app returns after 30 minutes in the background.
- Development builds send nothing, so your own taps never count. Check tracking with a release build.

## Reports for apps

Make an **app property** for each app: `npx splithub analytics connect` creates one with `kind: 'app'` in an app project, or pick App when you create a property in the dashboard. It uses screens instead of pages and hides the web-only panels.

- **Platform and app version** are dimensions and filters everywhere: `analytics <id> --by app_version`, `results <id> --by platform`, `--filter platform=ios`.
- **Retention** is the report most apps live by: users grouped by the day or week they started, and how many came back after. `npx splithub analytics retention <id>`; see [Reports](/docs/analytics-reports#retention).

## Allowed domains for apps

An app has no web origin, so its app id stands in for one. Add it to an experiment's or property's allowed list next to your sites:

```bash
npx splithub domains set property sh-x1h78w example.com com.acme.shop
```

App ids match exactly. A list that names only websites refuses apps.
