> ## Documentation Index
> Fetch the complete documentation index at: https://docs.userpilot.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Multi-Instance

> Run multiple independent Userpilot instances in the same iOS app

Run multiple independent Userpilot instances in the same process — for example when your app integrates Userpilot directly and also embeds a vendor SDK that bundles Userpilot for its own users.

Each instance keeps its own analytics, autocapture routing, storage, and experience UI without interfering with the others.

<Tip>
  **Note**

  If you only have one Userpilot instance in your app, nothing changes. Keep using the `Userpilot` instance returned by `Userpilot(config:)`. The SDK still resolves unattributed autocapture and screen-tracking events through the instance that claimed the default role (`isDefault`, which defaults to `true`).
</Tip>

***

## Quick Reference

| **Scenario**                     | **Use**                                                                                         |
| -------------------------------- | ----------------------------------------------------------------------------------------------- |
| Single instance (host app only)  | `Userpilot(config:)` — `isDefault` defaults to `true`, so the host claims default automatically |
| Vendor SDK that embeds Userpilot | `Userpilot(config:)` inside the vendor facade with `.defaultInstance(false)`                    |
| Re-initialize idempotently       | `Userpilot(config:)` again — returns the existing instance for the same token                   |

`Userpilot.Config.isDefault` defaults to **`true`**. The host application does not need to call `.defaultInstance(true)` — a plain `Userpilot.Config(token:)` already claims the default role. Embedded vendor SDKs **must** call `.defaultInstance(false)` so they do not compete for that role.

The two initialization entry points that used to exist (`Userpilot(config:)` and `Userpilot.create(config:)`) are collapsed into a single `Userpilot(config:)`. An instance claims the default role on registration when the role is unclaimed. Subsequent calls with the same token return the existing instance. A second instance with a **different** token that also leaves `isDefault` at its default cannot displace an existing claimant — the SDK logs a warning and un-anchored events keep routing to whoever already holds the role.

***

## Event Routing

Every event — autocapture interaction, autocapture screen, or a manual `screen(_:)` / `track(_:)` — routes to one Userpilot instance using a single three-tier rule:

1. **Explicit `userpilot:` argument**, when the API exposes one (for example `View.userpilotScreen(_:userpilot:)`).
2. **Anchored owner**, when something in the UI hierarchy claims the originating UI (`Config.attach(viewControllerClasses:)`, `Config.attach(windows:)`, `Config.attach(bundles:)`).
3. **Default** — the instance that claimed `isDefault` (defaults to `true`).

If none of those resolve to a live instance, the event is a silent no-op.

There is no fan-out. In the single-instance case the only registered instance claims the default automatically (`isDefault` defaults to `true`), so nothing in your integration changes.

***

## Single-Instance Integration

```swift theme={null}
let config = Userpilot.Config(token: "YOUR_TOKEN")
    .logging(enabled: true)
    .enableScreenAutoCapture()
    .enableInteractionAutoCapture()

let userpilot = Userpilot(config: config)   // isDefault defaults to true → claims default
userpilot.identify(userId: "user-123")
userpilot.track(eventName: "Added to Cart")
```

No `.defaultInstance(true)` is required — `isDefault` already defaults to `true`, so the host app's plain `Config` claims the default role on init.

***

## Multi-Instance Integration

### Vendor SDK

If you ship an SDK that uses Userpilot internally, initialize from inside your SDK facade. The factory is **idempotent** — re-calls with the same token return the existing instance.

```swift theme={null}
public final class AcmeSDK {
    public static let shared = AcmeSDK()

    private let acmeUserpilot: Userpilot

    private init() {
        let config = Userpilot.Config(token: "ACME")
            .defaultInstance(false)
            .enableScreenAutoCapture()
            .enableInteractionAutoCapture()
            .attach(bundles: [Bundle(for: AcmeSDK.self)])

        acmeUserpilot = Userpilot(config: config)
    }

    public func identifyAcmeUser(_ id: String) {
        acmeUserpilot.identify(userId: id)
    }
}
```

Key points:

* **`isDefault` defaults to `true`.** Every `Config` attempts to claim the default role on registration unless you opt out. Vendor SDKs **must** call `.defaultInstance(false)` so they do not compete for that single slot. When every instance opts out, there is no default and un-anchored events are dropped.
* Default resolution is **claim-based, not registration-order-based** — the instance that successfully holds the `isDefault` claim becomes the SDK's default fallback. Init order between host and vendor does not matter when the vendor opts out; only one claimant is allowed at a time, and a second conflicting claim is rejected.
* **`attach(bundles:)`** teaches the autocapture router which UI events belong to your tenant.
* Keep a **strong reference** to your instance (for example a stored property on your facade). The registry holds instances weakly.

### Host app + embedded vendor

Once the vendor SDK opts out and anchors its UI, the host app does nothing special for default resolution:

```swift theme={null}
let config = Userpilot.Config(token: "HOST")
    .enableScreenAutoCapture()
    .enableInteractionAutoCapture()
    // no .defaultInstance(true) needed — isDefault defaults to true

let hostUserpilot = Userpilot(config: config)
AcmeSDK.shared.identifyAcmeUser("acme-99")
```

Both instances are now live and independent. Init order does not affect which tenant is default as long as the vendor opts out with `.defaultInstance(false)`.

***

## The Default Instance

Only **one** instance can hold the default role at a time. An instance with `isDefault` left at its default (`true`) claims that role on registration **when the role is unclaimed**. If the role is already held, the new claim is **rejected** (a warning is logged) and the **existing claimant keeps the role**.

This is **claim-based**, not registration-order-based: registering first does not make an instance the default unless it also opts in, and there is no first-registered fallback when every instance opts out.

**Correct setup:** the host leaves `isDefault` at its default; every embedded vendor calls `.defaultInstance(false)`. The host then holds the default role **regardless of init order**.

**Misconfiguration:** if two instances both leave `isDefault = true`, whichever registers **while the role is still unclaimed** becomes the default; any later conflicting claim is rejected. Do not rely on init order — always opt vendor instances out.

If **no** instance claims the default role (every instance called `.defaultInstance(false)`), un-anchored events are dropped rather than attributed to an arbitrary tenant. Keep the default of `true` on the host app so it owns un-anchored events.

Surviving instances are **not** auto-promoted when the default is torn down. Call `Userpilot(config:)` again with a config that leaves `isDefault` at its default to reclaim the role.

For manual API calls, keep and pass the explicit `Userpilot` instance you created.

***

## Autocapture Routing

Autocapture events are attributed to the instance that owns the originating UI. The SDK walks the responder chain from the touched view up to the nearest `UIViewController` and checks each registered instance's claimed scope, in this priority order:

1. **`attach(viewControllerClasses:)`** — view controller class match (subclass-aware).
2. **`attach(windows:)`** — the view controller's containing window matches.
3. **`attach(bundles:)`** — `Bundle(for: VC.class)` identifier matches.
4. **Default fallback** — routes to the instance that claimed `isDefault`, or is dropped when no default exists.

| **Vendor situation**                                         | **Recommended attach**                                                                                              |
| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- |
| Vendor UI lives in its own framework                         | `.attach(bundles: [Bundle(for: AcmeSDK.self)])`                                                                     |
| Vendor presents on a custom `UIWindow`                       | `.attach(windows: [acmeWindow])`                                                                                    |
| Vendor UI is mixed into the host bundle (rare)               | `.attach(viewControllerClasses: [AcmeViewController.self])` per view controller                                     |
| Pure SwiftUI vendor view inside a generic hosting controller | `.attach(windows:)` on the vendor overlay window, or `.attach(viewControllerClasses:)` on a wrapper view controller |

`.attach(...)` calls are additive — call them as many times as needed and they stack into a single `Config`.

### SwiftUI ownership

`UIHostingController` lives in `com.apple.SwiftUI`, so its bundle never matches a vendor claim. The SDK walks `viewController.parent` outward from hosting and container view controllers until it finds an ownable view controller. This usually works when SwiftUI views are nested inside a UIKit container in your bundle.

For pure SwiftUI vendor UI with no UIKit boundary, prefer `.attach(windows:)` on a vendor-owned overlay window, or pass `userpilot:` explicitly on SwiftUI screen-tracking modifiers.

***

## SwiftUI Screen Events

The `View.userpilotScreen(_:userpilot:)` modifier accepts an optional explicit `userpilot:` argument:

```swift theme={null}
struct AcmeSettings: View {
    let acmeUserpilot: Userpilot

    var body: some View {
        VStack { /* … */ }
            .userpilotScreen("Acme.Settings", userpilot: acmeUserpilot)
    }
}
```

If `userpilot:` is passed, the event publishes on that instance. Otherwise it publishes on the default instance. If neither resolves, the event is a no-op.

***

## Forwarding Events to the Host

By default, autocapture events publish only on the instance that owns the originating UI. A vendor-owned screen or tap is reported to the **vendor** tenant only.

If the host app wants autocapture events that originate inside embedded vendor SDKs, opt in on the **host (default) instance**:

```swift theme={null}
let hostConfig = Userpilot.Config(token: "HOST")
    .enableScreenAutoCapture()
    .enableInteractionAutoCapture()
    .allowReceiveEventsFromExternalSource()

let hostUserpilot = Userpilot(config: hostConfig)
```

When enabled:

* Autocapture events that resolve to a **non-default** instance are published to that instance **and** forwarded to the default instance.
* The flag is read **only on the resolved default instance**. Setting it on a vendor instance has no effect.
* Forwarded events are delivered unchanged through the host's publisher — associated with the host's user/session and reported to the host backend. There is no extra tagging.
* The host's own events are never duplicated to itself, and forwarding never re-routes, so there is no fan-out loop.

Routing still decides the owning tenant; forwarding only adds a copy to the default when the default opted in.

***

## Idempotent Initialization

`Userpilot(config:)` is a get-or-create factory. Calling it twice for the same token returns the same instance. The `isDefault` flag on a repeat call is ignored when the instance already exists — the original default claim (or lack thereof) is preserved:

```swift theme={null}
let a = Userpilot(config: .init(token: "HOST"))   // claims default (isDefault defaults true)
let b = Userpilot(config: .init(token: "HOST"))
// a and b share the same dependency container. The supplied config on `b` is discarded.
```

This protects against accidental double initialization from `SceneDelegate` / `AppDelegate` overlap.

***

## Experience Presentation

Each `Userpilot` instance owns its own dedicated overlay `UIWindow`. When an experience triggers, it presents on that window, not on the host app's key window:

* Two instances can present experiences simultaneously on different window levels.
* Touches outside any presented experience pass through so the underlying app stays interactive.
* Window levels are deterministic and stable across launches.

***

## Push Notifications

`PushNotificationAutoConfig` registers one monitor per Userpilot instance:

* **Token registration** fans out to every monitor so each instance can forward the token to its backend.
* **Notification responses** use the payload `app_token` to route directly to the matching `Userpilot` instance. Older payloads without `app_token` fall back to trying registered monitors until one claims the response.

***

## Limitations

* **Static autocapture flags.** UIKit method swizzles install once per process for the union of all instances' enabled features. Autocapture uses each instance's config to filter events at delivery time.
* **The default fallback is not thread-locked to a tenant.** It always points at the default claimant (`isDefault`). To act on a specific tenant, keep and pass the explicit `Userpilot` instance you created. When no instance holds the default role, there is no first-registered fallback.
* **`stopAutoCapture()` / `resumeAutoCapture()`** are per-instance methods.
* **`Userpilot.enableAutomaticPushConfig()`** is process-wide — calling it from any instance enables it for all.
* **SwiftUI ownership** can be ambiguous with no UIKit container in your bundle. Prefer `.attach(windows:)` or pass `userpilot:` explicitly.
* **Two instances with the same token** never happens through the public factory — `Userpilot(config:)` is idempotent.

***

## Complete Example

```swift theme={null}
// AppDelegate.swift (host app)
let hostConfig = Userpilot.Config(token: "HOST")
    .logging(enabled: true)
    .enableScreenAutoCapture()
    .enableInteractionAutoCapture()
let hostUserpilot = Userpilot(config: hostConfig)   // isDefault defaults to true → claims default

// AcmeSDK.swift (vendor SDK)
let vendorConfig = Userpilot.Config(token: "ACME")
    .defaultInstance(false)
    .enableScreenAutoCapture()
    .enableInteractionAutoCapture()
    .attach(bundles: [Bundle(for: AcmeSDK.self)])
let acmeUserpilot = Userpilot(config: vendorConfig)

hostUserpilot.identify(userId: "host-42")
acmeUserpilot.identify(userId: "acme-99")
acmeUserpilot.track(eventName: "vendor_action")
```

<Frame>
  [**For any questions or concerns please reach out to support@userpilot.com**](mailto:support@userpilot.com)
</Frame>
