npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@gachlab/capacitor-background-geolocation

v4.1.1

Published

Capacitor 8+ plugin for accurate background geolocation tracking, driving-event detection, sync queue and OEM diagnostics on iOS and Android.

Readme

@gachlab/capacitor-background-geolocation

npm version License Capacitor

Capacitor 8+ plugin for accurate background geolocation tracking on iOS and Android.

v3 exposes a composed facade: instead of ~46 flat methods and stringly-typed addListener calls, you get cohesive sub-APIs (bg.tracking, bg.locations, bg.geofences, bg.sync, bg.driver, …), a two-tier composed config, typed capability gating, and disposable event handles. The Capacitor bridge is unchanged under the hood (Promise + addListener) — the win is structure, not syntax.

Migrating from the flat v2 API? See MIGRATION.md.

Install

npm install @gachlab/capacitor-background-geolocation
npx cap sync

Requires @capacitor/core >= 8.0.0.

iOS setup

Add to ios/App/App/Info.plist:

<key>NSLocationWhenInUseUsageDescription</key>
<string>This app needs your location to track activity.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>This app needs your location even when the app is in the background.</string>
<key>NSMotionUsageDescription</key>
<string>This app uses motion data to detect when you are moving.</string>
<key>UIBackgroundModes</key>
<array>
  <string>location</string>
  <string>fetch</string>
</array>

Android setup

Add to android/app/src/main/AndroidManifest.xml (inside <manifest>):

<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.INTERNET" />

Runtime permission flow (API 23+) — request foreground first, then background:

import { BackgroundGeolocation } from '@gachlab/capacitor-background-geolocation';

const bg = BackgroundGeolocation(); // factory: zero-config singleton, or BackgroundGeolocation({ native }) to inject a fake

if ((await bg.permissions.request()) !== 'granted') {
  await bg.permissions.openSettings('app');
}
await bg.permissions.requestBackground();     // Android 10+ background location
await bg.permissions.requestNotifications();  // Android 13+ foreground-service icon

FOREGROUND_SERVICE_LOCATION is mandatory on Android 14 (API 34+); the plugin declares its foreground service with foregroundServiceType="location".

Quick start

import { BackgroundGeolocation } from '@gachlab/capacitor-background-geolocation';

const bg = BackgroundGeolocation(); // factory: zero-config singleton, or BackgroundGeolocation({ native }) to inject a fake

// 1. Configure the shared base ONCE — every feature inherits it.
await bg.configure({
  location:     { accuracy: 'high', distanceFilter: 25 },
  transport:    { baseUrl: 'https://api.example.com', headers: { Authorization: 'Bearer …' } },
  sync:         { path: '/sync', mode: 'batch', threshold: 100 },
  notification: { channel: 'tracking', title: 'Tracking', foreground: true },
  survival:     { stopOnTerminate: false, startOnBoot: true },
});

// 2. Listen for fixes (typed, disposable).
const sub = bg.locations.on((loc) => console.log('fix', loc.latitude, loc.longitude));

// 3. Start tracking. The session auto-stops if you use `await using`.
const session = await bg.tracking.start();
// …later
await session.stop();
sub.remove();

The facade

BackgroundGeolocation(deps?) is a factory: called with no args it returns a shared, zero-config instance (the registered Capacitor bridge); called with { native } it builds a fresh instance over an injected bridge — the seam for mocking in a test. It composes these sub-APIs by dependency injection (no new, no this). The raw flat proxy is still exported as NativeBackgroundGeolocation for escape-hatch use.

| Sub-API | Methods | | --- | --- | | bg.config | configure(patch) (also bg.configure) · current()BaseConfig · on(cb) — reactive, reload-safe base config | | bg.tracking | start(override?)TrackingSession · stop() · status() | | bg.locations | on(cb) · current(opts?) · all() · pending({ consume? }) · stationary() · delete(id) · clear() | | bg.geofences | add(list) · remove(ids?) · list() · on('enter'\|'exit'\|'dwell'\|'error', cb) | | bg.sync | flush() · clear() · pending() · on('start'\|'progress'\|'success'\|'error'\|'prioritySuccess'\|'priorityFailed', cb) | | bg.driver | lastTripScore() (gated) · on(type, cb) | | bg.logs | page(opts?) · stream(opts?) | | bg.permissions | check() · request() · requestBackground() · requestActivity() · requestNotifications() · openSettings('app'\|'location') | | bg.diagnostics | report() · version() · killReason() · oem.* (gated: Android) | | bg.recordings | start() · clear() · locations() · count() | | bg.platform | startTask() · endTask(key) · switchMode('background'\|'foreground') (iOS) | | top-level | bg.on(event, cb) · bg.sos(payload?) · bg.capabilities() · bg.supports(cap) · bg.require(cap) · bg.removeAllListeners() |

Configuration (two-tier cascade)

Shared config (transport, notification identity, base sampling) lives on the plugin and is set once via bg.configure(). Features inherit it and override only their delta:

base (bg.configure)  ⊕  session (tracking.start)  ⊕  per-call (current, add)  =  effective

Merge rule: deep-merge for maps (headers, queryParams), replace for scalars, null = explicit unset. Config groups:

  • locationaccuracy ('high'|'medium'|'low'|'passive'), distanceFilter, provider ('distanceFilter'|'activity'|'raw'), maxAcceptedAccuracy, includeBattery, mockPolicy, activityType, interval, fastestInterval, activityInterval, activityConfidenceThreshold.
  • stationary (Android) — radius, timeout, pollInterval, pollFast, exitMode.
  • transportbaseUrl, headers, method, mode ('batch'|'single'), queryParams, bodyTemplate. Feature endpoints append their path to baseUrl.
  • syncpath, mode, threshold, auto, priority.{ events, path, retries, retryDelaysMs }.
  • notificationenabled, channel, icon.{ small, large }, color, title, text, foreground, showTime, showDistance, sync.{ title, text, completedText, failedText }.
  • survivalstopOnTerminate, startOnBoot, restartOnKill, heartbeatInterval, watchdog.{ enabled, intervalMs }, iosBackgroundFallback, saveBatteryOnBackground, pauseLocationUpdates, showsBackgroundLocationIndicator, wakeLockMode.
  • persistencemaxLocations.
  • driving — the driver-intelligence config (see below).
  • native — a typed escape hatch (Record<string, unknown>) for raw wire flags not yet surfaced in the clean types.

A session override composes over the base for one tracking run:

const session = await bg.tracking.start({
  location: { accuracy: 'passive' }, // cheaper accuracy just for this run
});

Events

Global / lifecycle events are on bg.on(...); domain events live on their sub-API. Every .on() returns a disposable Subscription (typed by its payload).

bg.on('authorization', ({ status }) => console.log(status)); // notAuthorized | authorized | authorizedForeground
bg.on('error', (e) => console.warn(e.code, e.message));

const g = bg.geofences.on('enter', (e) => console.log('entered', e.id));   // e.action === 'enter'
const s = bg.sync.on('error', (e) => console.warn(e.httpStatus, e.message));

Global events: start, stop, foreground, background, error, authorization, heartbeat, providerChange, serviceRestarted, iosFallbackActivated, abortRequested, httpAuthorization.

Capability gating

Native features that don't exist on every platform are gated. Feature-detect instead of branching on platform; a gated call off-platform throws a typed CapabilityError.

if (await bg.supports('driverIntelligence')) {
  const score = await bg.driver.lastTripScore();
}

// or assert-or-throw:
await bg.require('driverIntelligence');
const score = await bg.driver.lastTripScore();

const caps = await bg.capabilities(); // static register: platform, backgroundTracking,
                                       // geofencing, maxGeofences, sensorFusion, oemSettings, …

Cancelable one-shots

bg.locations.current() accepts an AbortSignal. It cancels the caller's wait (the native GPS keeps its own lifecycle):

const ac = new AbortController();
const loc = await bg.locations.current({ accuracy: 'passive', timeout: 10_000, signal: ac.signal });

Disposable handles (using)

Handles carry Symbol.dispose / Symbol.asyncDispose, so using / await using auto-clean at scope end. .remove() / .stop() still work if you prefer explicit.

{
  using sub = bg.locations.on((loc) => render(loc));
  await using session = await bg.tracking.start();
  // …work…
} // sub removed and session stopped automatically here

Logs

page() reads one newest-first batch; stream() hides the fromId paging entirely:

for await (const entry of bg.logs.stream({ minLevel: 'warn' })) {
  if (entry.timestamp < cutoff) break; // stops paging — no further native calls
  report(entry);
}

Driver intelligence

Enable the GPS-derived driver-insight pipeline via configure({ driving: { enabled: true } }). The native core then emits driving events (subscribe via bg.driver.on(...)):

  • tripStart, tripEnd, moving, stopped — sustained-speed state machine.
  • speeding — when speed crosses driving.speedLimit (km/h).
  • hardBrake, rapidAcceleration, sharpTurn — sensor-free heuristics from speed/bearing.
  • possibleCrash — sudden velocity drop or accelerometer impact (source: 'gps' | 'sensor'). Always confirm with the user before notifying anyone. driving.crashConfirmWindowMs defers the event until the vehicle stays stopped, cancelling it if speed recovers.
  • phoneUsage — bearing-jitter heuristic (driving.sensorFusion: false) or accel/gyro jitter.
  • idleStart, idleEnd — stationary for ≥ driving.idleThresholdMs during an active trip.
await bg.configure({
  driving: {
    enabled: true,
    speedLimit: 90,             // km/h, 0 to disable
    minTripSpeed: 3.0,          // m/s
    minTripDurationMs: 30_000,
    sensorFusion: false,        // true → accel/gyro pipeline
    idleThresholdMs: 300_000,   // stillness before idleStart (default 5 min)
    idleEndThresholdMs: 30_000, // movement before idleEnd (default 30 s)
    scoring: { speeding: 30, hardBraking: 25, rapidAcceleration: 20, sharpTurn: 15, phoneUsage: 10 }, // sum 100
  },
});

bg.driver.on('tripEnd', async (e) => {
  const score = e.score ?? (await bg.driver.lastTripScore());
  if (score) console.log('overall', score.overall, '/100');
});
bg.driver.on('speeding', (e) => console.warn('over limit', e.speedKmh, 'vs', e.limitKmh));

Trip scoring

When driving.scoring is set, each completed trip accumulates a penalty-based score. tripEnd carries score?: TripScore; bg.driver.lastTripScore() retrieves it on demand.

Priority sync

Safety-critical events (possibleCrash, sos by default) are delivered immediately via a dedicated POST that bypasses the queue, retrying on failure and queueing offline. Configure it under sync.priority:

await bg.configure({
  transport: { baseUrl: 'https://api.example.com' },
  sync: {
    path: '/sync',
    priority: {
      events: ['possibleCrash', 'sos', 'hardBrake'],
      path: '/priority',            // appended to transport.baseUrl
      retries: 3,
      retryDelaysMs: [10_000, 30_000, 60_000],
    },
  },
});

bg.sync.on('prioritySuccess', (e) => console.log(`${e.eventType} on attempt ${e.attemptNumber}`));
bg.sync.on('priorityFailed', (e) => console.warn(`${e.eventType} failed after ${e.attempts} (HTTP ${e.httpStatus})`));

The channel deduplicates by event timestamp so retries never double-post.

Web limitations

The web fallback uses navigator.geolocation and only works while the page is alive (bg.capabilities() reports backgroundTracking: false). Sync queue, sessions, OEM screens, and other native-only features are no-ops or throw. Driver-intelligence calls throw CapabilityError — gate with bg.supports('driverIntelligence').

Compatibility

  • Capacitor >=8.0.0
  • iOS >=14.0
  • Android API >=23 (Android 6.0)
  • Web: foreground only via navigator.geolocation

Migration

Coming from the flat v2 API (or the Cordova-era plugin)? The method-by-method mapping is in MIGRATION.md.

License

Apache-2.0. See LICENSE and NOTICE.md for attribution to upstream contributors.