Geofence

Fences that outlive your app.

NativePHP Mobile
$ composer require all1web/nativephp-geofence
Geofence — Fences that outlive your app.

OS-level geofence registration that survives app death, reporting enter and exit transitions straight to a configured HTTPS endpoint.

  • Registered with the OS — fires with your app closed or the device rebooted
  • Enter/exit transitions POST to your own HTTPS endpoint
  • Inspect and revoke fences from PHP with list() and remove()

Documentation

Synced from GitHub 0 seconds ago

🛰️ nativephp-geofence

Fences that outlive your app.

OS-level geofences that survive app death and reboot, reported to your server from native code

PHP does not run in the background on a phone. The moment the OS kills your app — and it will — anything you built on a PHP loop stops existing. This plugin hands the job to the operating system instead: it registers real OS-level geofences that keep watching after your app is backgrounded, force-quit, or the device reboots, and reports enter/exit transitions straight to your HTTPS endpoint from native code.

Register from PHP, the app dies, the fence still fires a native HTTPS POST

Why this needs a plugin at all: geofencing is an OS service. The platform holds your fences, wakes a native receiver when one is crossed, and hands it a transition — all while your process is gone. There is no WebView trick, no queued job, and no PHP path to that. This plugin is that capability, delivered as four lines of Laravel.

Status — both platforms implemented. Android is contract-complete and unit-tested off-device; it still needs a physical-device pass before production — see What still needs validation. iOS ships in beta: implemented with CoreLocation region monitoring, validated end-to-end in the iOS Simulator — including delivery after the app is killed and after a reboot — and confirmed to compile clean for device architecture. See iOS for the exact validation matrix.


✨ What you get

  • 🛰️ Fences you define. Register any id / lat / lng / radius from plain PHP. Not an internal resurrection trick — a first-class API with real enter/exit semantics.
  • 💀 Survives app death and reboot — on both platforms. Android re-registers on BOOT_COMPLETED and reports from a native BroadcastReceiver; iOS relaunches the app for the crossing itself and the plugin's launch hook re-arms before the event is delivered. Either way: no PHP process alive anywhere. (Proven: on iOS the report lands 90 ms after the OS resurrects a force-killed app — see the validation record.)
  • 📱 One API, two platforms. The same Geofence:: calls and the same endpoint contract on Android and iOS — platform differences are documented, not discovered.
  • 🌐 Your server is the source of truth. A small authenticated JSON POST to an endpoint you own, with exponential-backoff retries. No vendor service in the middle, no data leaving for anyone but you.
  • 🔐 Credentials never sit in plaintext. Endpoint URL and bearer token live in EncryptedSharedPreferences (AES-256) on Android and the Keychain on iOS, and the PHP layer refuses any endpoint that isn't https://.
  • 🐘 Foreground events too. When the app happens to be alive, GeofenceEntered / GeofenceExited fire as ordinary Laravel events — a convenience on top of the POST, never a replacement for it.
  • 🎛️ Tunable responsiveness. Dwell delay, OS responsiveness hint, retry count, backoff base, and timeout are all config, forwarded to native code on every call.
  • 🧪 Unit-tested contract. A Pest suite pins the validator rules and the PHP↔bridge call contract, so your test suite never needs a device.

📊 Platform support at a glance

Android iOS
Register / remove / list / clear fences
Enter & exit reported to your endpoint
Delivery while the app is backgrounded
Delivery while the app is force-killed
Survives reboot BOOT_COMPLETED ✅ relaunch hook
Credentials encrypted at rest ✅ AES-256 prefs ✅ Keychain
Foreground PHP events — POST only
DWELL / responsiveness tuning — not in CoreLocation
Max fences ~100 20
Maturity implemented, device pass pending beta, simulator-validated

🎯 Perfect for

Field service and dispatch · fleet and delivery tracking · warehouse and job-site arrival logging · time-and-attendance by location · store-proximity offers · travel and commute journaling · safety check-ins · any app whose users should be noticed arriving without opening it.

🚀 On the roadmap

  • 🛡️ Hardened background delivery on Android — enqueue the endpoint POST through WorkManager so a long retry chain can outlive the receiver window.
  • 📍 DWELL transitions surfaced as a first-class event on Android (the loitering delay is already plumbed through config; CoreLocation has no DWELL, so this stays Android-only).
  • 📦 Batch registration — register or replace a whole fence set in one call.

📦 Install

After purchasing, connect Composer to the NativePHP plugin marketplace (your credentials are on your Purchased Plugins dashboard), then:

composer config repositories.nativephp-plugins composer https://plugins.nativephp.com
composer config http-basic.plugins.nativephp.com your-email@example.com your-license-key
composer require all1web/nativephp-geofence

NativePHP plugins are opt-in for security, so requiring the package is not enough — register the provider, then compile it into the native project:

php artisan native:plugin:register all1web/nativephp-geofence
php artisan native:install android --force   # or: native:install ios --force
php artisan native:run android

The rebuild matters: the geofence receivers are declared in the Android manifest at install time, so they exist only after this rebuild — not after a re-run of an old build.

php artisan vendor:publish --tag=geofence-config
# Local checkout (plugin development):
composer config repositories.geofence path ../nativephp-geofence
composer require "all1web/nativephp-geofence:*@dev"

🧑‍💻 Use it

use All1web\Geofence\Facades\Geofence;

// 1. Once after login — seed the endpoint the background receiver reports to.
//    The URL MUST be https://.
Geofence::configureEndpoint(
    url:   'https://api.example.test/geo/signals',
    token: $user->currentAccessToken()->plainTextToken,
);

// 2. Register a fence (from a foregrounded screen — the permission flow needs it).
Geofence::register(id: 'warehouse', lat: 40.7128, lng: -74.0060, radius: 200);

// 3. Manage them.
Geofence::list();            // ['fences' => [...], 'count' => n]
Geofence::remove('warehouse');
Geofence::clear();

That's the whole integration. Re-call configureEndpoint() whenever the token rotates.

From JavaScript

Every bridge function has a matching ES module export, for Livewire, Inertia, or plain JS front-ends. Alias the bundled wrapper once in vite.config.js:

// vite.config.js — resolve.alias:
// '@geofence': '/vendor/all1web/nativephp-geofence/resources/js/index.js'
import { configureEndpoint, register, remove, list, clear } from '@geofence';

await configureEndpoint('https://api.example.test/geo/signals', token);
await register('warehouse', 40.7128, -74.0060, 200);

const { fences, count } = await list();
await remove('warehouse');
await clear();

Foreground events (optional)

use All1web\Geofence\Events\GeofenceEntered;

#[\Native\Mobile\Attributes\OnNative(GeofenceEntered::class)]
public function whenEntered(GeofenceEntered $e): void
{
    // Fires only while the app is foregrounded. Never rely on this for
    // background transitions — those reach your HTTPS endpoint instead.
}

🌐 The endpoint contract

The native receiver POSTs exactly this, with an Authorization: Bearer <token> header:

{
  "type": "geo_enter",
  "payload": { "lat": 40.7128, "lng": -74.0060, "fence_id": "warehouse" }
}

A minimal receiver on any Laravel backend:

// routes/api.php
Route::post('/geo/signals', function (Request $request) {
    $data = $request->validate([
        'type'             => 'required|in:geo_enter,geo_exit',
        'payload.lat'      => 'required|numeric|between:-90,90',
        'payload.lng'      => 'required|numeric|between:-180,180',
        'payload.fence_id' => 'required|string',
    ]);

    LocationSignal::create([
        'user_id'  => $request->user()->id,   // resolved from the bearer token
        'type'     => $data['type'],
        'fence_id' => $data['payload']['fence_id'],
        'lat'      => $data['payload']['lat'],
        'lng'      => $data['payload']['lng'],
    ]);

    return response()->json(['ok' => true]);   // any 2xx stops the retry loop
})->middleware('auth:sanctum');

The token is whatever your backend expects — Sanctum, Passport, a signed value. The plugin never inspects it; it only sets the header.


⚙️ Configuration

config/geofence.php, forwarded to native code on every bridge call:

Key Default Meaning
initial_trigger_on_register true Fire ENTER immediately if already inside a fence at registration.
loitering_delay_ms 0 Dwell time before a DWELL transition (0 = off).
notification_responsiveness_ms 0 OS responsiveness hint; larger = more battery-friendly.
post_max_retries 5 Retries for the background endpoint POST.
post_backoff_base_ms 1000 Exponential backoff base (1×, 2×, 4×…).
post_timeout_ms 15000 Connect/read timeout per POST attempt.

🍏 iOS (beta)

Implemented — CoreLocation region monitoring, same PHP, same endpoint contract. The same Geofence:: calls register CLCircularRegions with the OS; crossings POST to your endpoint from native Swift with the same JSON body and bearer header as Android. Credentials live in the iOS Keychain (readable after first unlock, so a background relaunch behind a locked screen can still report), fences re-arm automatically at every launch, and the plugin's regions are namespaced so they coexist with any other location-aware plugin in your app.

What's proven. The implementation is validated end-to-end in the iOS Simulator (iOS 26), remotely and repeatably, against a real HTTPS receiver with a trusted certificate:

Scenario Result
Register from PHP (configured/registered/list round-trip)
ENTER crossing → HTTPS POST, exact contract, first attempt
EXIT crossing → HTTPS POST
App force-killed → OS relaunches it → POST ✅ (delivered 90 ms after relaunch)
Device reboot, app never opened → crossing launches it → POST
Duplicate suppression (initial-trigger vs OS redelivery)
Device-architecture compile (arm64/Debug-iphoneos) ✅ 0 errors

Full method, raw log excerpts, and the limits of simulator testing are in the validation record. Beta marks a first release without field mileage — not missing work: what the simulator can't speak to is permission UX (the API exposes authorization/backgroundPermission so your app drives that flow explicitly), real-world latency, and per-device quirks.

Two honest platform differences to design around: iOS monitors at most 20 regions per app (Android: ~100), and the foreground GeofenceEntered/GeofenceExited PHP events are Android-only — on iOS the HTTPS POST is the delivery contract, full stop. Registration responses carry two extra keys on iOS: authorization (the permission ladder state) and backgroundPermission (true once the user grants "Always").


🏪 Android background-location policy — read before shipping to Play

Background geofencing depends on ACCESS_BACKGROUND_LOCATION, which Google Play treats as a sensitive permission requiring review. This is real work on your side, not a checkbox:

  • Request ACCESS_FINE_LOCATION first, in context. Request ACCESS_BACKGROUND_LOCATION later, in a separate prompt, behind a rationale screen — Android 11+ will not show both in one dialog.
  • Your listing needs a prominent in-app disclosure and a privacy policy explaining that location is collected in the background and sent to your server.
  • You must submit the Permissions Declaration Form justifying the core feature. "Geofencing that reports transitions to a server" is an accepted use case, but it must be user-facing and disclosed.
  • Expect OEM battery managers (Samsung, Xiaomi, Huawei) to throttle background receivers. Transition delivery is best-effort, not real-time — the OS batches geofence events to save power, and seconds-to-minutes of latency is normal and by design.

The plugin does the technical part correctly, and the paperwork part is pre-written for you: docs/PLAY-COMPLIANCE.md carries copy-paste Data safety form answers, the background-location declaration justification, the prominent-disclosure and rationale-screen dialog copy, a privacy-policy paragraph, and the review-video checklist. Filing them in your Play Console remains the app owner's responsibility.


🔒 Security

  • The endpoint must be https:// — the PHP layer rejects anything else, because the bearer token is sent from a background context with no interactive TLS prompt.
  • Endpoint URL and token live in EncryptedSharedPreferences (AES-256), seeded by configureEndpoint(). The plugin ships its own encrypted store rather than depending on core SecureStorage, which is not implemented on Android in the v4 RC.
  • Re-call configureEndpoint() on token rotation.

🔬 What still needs on-device validation

iOS is validated end-to-end in the iOS Simulator — bridge round-trip, ENTER/EXIT delivery with the exact contract, force-kill relaunch delivery, reboot delivery with the app never opened, and a clean device-architecture compile; the full evidence, method, and the honest limits of simulator testing are in docs/IOS-VALIDATION.md. It ships as beta on that basis — a first release without field mileage.

Android is compile-correct and mirrors the reference plugins closely, but has not yet run on physical hardware. Before production, validate on a real device: transition delivery, boot re-registration, the EncryptedSharedPreferences round-trip, and the retry loop. Full list in docs/DESIGN.md.


📋 Requirements

  • NativePHP Mobile ^3.0 || ^4.0
  • Android 10+ (minSdk 29 — background-location semantics), with Google Play services (the plugin pulls play-services-location)
  • iOS 18.0+ (CoreLocation region monitoring; no extra background modes, no App Groups, no extension targets)
  • An HTTPS endpoint you control

🔬 Digging deeper

Doc What's in it
Reference Full API, payload shapes, events, the endpoint retry contract, JS usage, per-platform facts, uninstall
iOS validation record The evidence behind the beta label — method, results, raw log excerpts, what remains
Play compliance kit Copy-paste Data safety answers, declaration + video checklist, disclosure dialog copy, privacy-policy paragraph
Design notes The architecture and the honest reasoning behind it
Changelog Version history

🛠️ Development

composer install
composer test

📜 License

Commercial. Distributed as a paid plugin via the NativePHP Plugin Marketplace; each purchase grants a license key used for Composer authentication. Licensed by ALL 1, a Wyoming corporation. Source access is included for your own development; redistribution of source is not — see LICENSE for the full EULA.