mobile/native

Background BLE That Survives iOS and Android

Two of the app’s core features — unlocking nearby doors and passive attendance — depend on the radio doing work while nobody is looking at the screen. Both platforms treat that as something to be suspicious of, for good reasons. Living inside those rules is most of the job.

The OS is not on your side (by default)

iOS suspends apps aggressively and only keeps specific background activities alive if you declare them; Android gates long-running work behind foreground services with declared types. Neither platform wants an app scanning BLE indefinitely, because that’s exactly how you drain a battery and creep on people.

So the work splits in two: convince each OS that the background activity is legitimate and declared, then be a good citizen about how much you actually do.

iOS: declare the background modes

On iOS the app’s capabilities have to spell out what it does in the background. For the notification and proximity features that meant adding the relevant UIBackgroundModes — including remote-notification for background message handling — so the system permits the work instead of killing the app for attempting it.

// app.json (Expo) — declare what runs in the background
{
  "expo": {
    "ios": {
      "infoPlist": {
        "UIBackgroundModes": ["remote-notification", "bluetooth-central"]
      }
    }
  }
}

Android: a foreground service with a declared type

On Android, sustained BLE work rides in a foreground service, and modern Android requires that service to declare its type. Being explicit about it — rather than hiding scanning inside a generic service — is what keeps the OS from terminating it and keeps the app honest with Play policy.

<service
    android:name=".SmartLockService"
    android:foregroundServiceType="connectedDevice"
    android:exported="false" />

Walking mode: scan only when it matters

Declaring the capability buys you the right to scan; it doesn’t make scanning free. The lever that actually protects the battery is when you scan. A scoped “walking mode” ramps scanning up when there’s reason to believe the user is near managed hardware and stops it otherwise — and it’s driven from the native module so it survives the JS lifecycle and app backgrounding.

That start/stop-on-context discipline is the same idea behind the beacon attendance service: the radio should be busy exactly when presence is plausible and idle the rest of the time.

The upgrade tax

None of this stands still. Shipping these features rode alongside an Expo SDK upgrade (54 → 55) with custom native modules attached, which is its own adventure — native module changes and platform-version bumps land together, and you retest background behavior on real devices because the simulator lies about exactly the things that matter here.

← All postsNext: Passive Attendance With iBeacons — and Why the Backend Is the Hard Part