The Polyfence geofence layer for React Native. Define zones once; same zones run on your mobile app, your IoT device, and your server (Polyfence platform). This package is the React Native bridge over polyfence-core (engine source: github.com/polyfence/polyfence-core). Privacy-first by default — positions never leave the device; only zone events.
The screenshots above are from the example app in this repo —
a working iOS + Android React Native app that fetches zones from the
Polyfence SaaS, tracks location, and renders enter / exit / dwell events.
Sign up at polyfence.io for a free API key, then
follow example/README.md to run it locally.
You're building a React Native app that needs geofencing — delivery, logistics, fitness, healthcare, asset tracking, agritech, fleet, or consumer. You want the math on-device, the zones defined once, and the same definitions reusable on your IoT firmware or server when you grow into those surfaces.
This package is the React Native bridge. The same engine runs on iOS/Android via polyfence-core, on embedded MCUs via polyfence-embedded, and server-side via the Polyfence API.
- Polygon geofencing — Not just circles. Define zones with arbitrary polygon boundaries (complex city zones, campus outlines, delivery areas).
- Unlimited zones — No artificial limits. Monitor hundreds of zones simultaneously with zone clustering for performance.
- Privacy-first — All geofencing runs on-device. Zero location data ever leaves the device by default. No cloud dependency.
- SmartGPS — Intelligent GPS scheduling based on proximity, movement, activity, and battery state. 40-50% less battery drain than naive polling.
- Activity recognition — Automatically detect user activity (walking, driving, stationary) and optimize GPS intervals.
- Background tracking — True background operation with foreground service (Android) and location background mode (iOS).
- TypeScript-first — Full type definitions. Built for type safety.
- Dwell detection — Detect when users stay in zones for a configured duration.
Three storage choices — same plugin API in all cases:
| Approach | Backend | API Key | Best For |
|---|---|---|---|
| Hardcode zones in your app | None | Not needed | Static zones, full control, privacy-first apps |
| Fetch from your own API | Your backend | Not needed | Existing infrastructure, custom zone logic |
| Use the Polyfence dashboard | polyfence.io | Required | Visual zone editor, analytics dashboard |
Same plugin API in all cases. The Polyfence platform layer (SDK + dashboard + API) is the geofence layer underneath your product, whether you store zones in code or in the dashboard.
| Requirement | Version |
|---|---|
| React Native | 0.73+ |
| Node.js | 18.0+ |
| Android | API 24+ (Android 7.0), tested up to API 35 (Android 15) |
| iOS | 14.0+ |
| Feature | Android | iOS |
|---|---|---|
| Circle geofences | Yes | Yes |
| Polygon geofences | Yes | Yes |
| Dwell detection | Yes | Yes |
| Zone clustering | Yes | Yes |
| Scheduled tracking | Yes | Yes |
| Activity recognition | Yes | Yes |
| Background tracking | Yes (foreground service) | Yes ("Always" permission) |
| Battery optimization bypass | Yes | N/A |
| GPS accuracy profiles | Yes | Partial (iOS manages GPS) |
npm install polyfence-react-native
# or
yarn add polyfence-react-nativeCurrent version: 2.0.3
Native dependency: Polyfence uses polyfence-core for native geofencing engines. It's included automatically — Maven for Android, CocoaPods for iOS. On iOS, run cd ios && pod install after adding the dependency.
cd ios && pod install<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_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.WAKE_LOCK" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />Ensure your android/app/build.gradle has the correct minimum SDK version:
android {
defaultConfig {
minSdkVersion 24 // Required for Polyfence
}
}Foreground Service Notification: Polyfence requires a foreground service notification on Android. The plugin automatically creates the notification channel — no additional setup required. The notification uses low priority and is silent.
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app needs location access to detect when you enter or exit defined zones.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Background location access is required for continuous zone monitoring.</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>Background location access is required for continuous zone monitoring.</string>
<key>UIBackgroundModes</key>
<array>
<string>location</string>
</array>iOS Background Mode in Xcode:
- Open
ios/[YourApp].xcworkspacein Xcode - Select the [YourApp] target → Signing & Capabilities
- Click + Capability → add Background Modes
- Check Location updates
iOS Permission Flow:
iOS requires "Always" location permission for background geofencing:
- First Request: When you call
requestPermissions({ always: true }), iOS shows a "While in use" permission dialog - Manual Step Required: The user must manually enable "Always" permission in Settings → Privacy & Security → Location Services → Your App → "Always"
- Check Permission Status:
const isEnabled = await Polyfence.instance.isLocationServiceEnabled();
if (!isEnabled) {
// Guide user to enable location services
}
const granted = await Polyfence.instance.requestPermissions({ always: true });
if (granted) {
// User granted "While in use" — they still need to enable "Always" in Settings
// You may want to show a dialog guiding them to Settings
}import { Polyfence } from 'polyfence-react-native';
await Polyfence.instance.initialize();Wire
onErrorbefore the calls below. Several SDK methods (including some used in steps 3 and 5) can emit errors as a side effect of being called. IfonErrorisn't subscribed at the time, those errors are dropped silently — no replay, no warning in the return value. Skip ahead to Step 6, wireonErroronce, then come back here. The remaining steps assume you've done that.
Want your sessions on your dashboard? Telemetry is anonymous by default — anonymous sessions feed only aggregate stats, not your account's analytics. To attribute them to your Polyfence account, pass your API key (the same one you use for the zone API) to
initialize:await Polyfence.instance.initialize(undefined, { apiKey: YOUR_POLYFENCE_API_KEY });It's sent as
x-api-keywith each telemetry upload. React Native has no compile-time env, so supply the key however your app manages config (e.g.react-native-config, an env file, or a constant). To turn telemetry off entirely:initialize(undefined, { disableTelemetry: true }).
iOS: requestPermissions({ always: true }) triggers the system permission dialog.
Android: requestPermissions() does not show a dialog — it only reads the current permission state and returns a boolean. To trigger the OS dialog on Android, use a library like react-native-permissions first, then call requestPermissions() to verify the result.
import { Platform } from 'react-native';
// Android only — trigger the OS permission dialog.
// import { request, PERMISSIONS } from 'react-native-permissions';
// if (Platform.OS === 'android') {
// await request(PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION);
// await request(PERMISSIONS.ANDROID.ACCESS_BACKGROUND_LOCATION);
// }
// Both platforms — verify the result. On iOS this ALSO shows the
// system dialog on first call.
const hasPermission = await Polyfence.instance.requestPermissions({ always: true });
if (!hasPermission) {
// Handle permission denied — e.g. guide the user to Settings.
return;
}// Circle zone
await Polyfence.instance.addZone({
id: 'office',
name: 'Office',
type: 'circle',
center: { latitude: 37.422, longitude: -122.084 },
radius: 150,
});
// Polygon zone
await Polyfence.instance.addZone({
id: 'campus',
name: 'Campus',
type: 'polygon',
polygon: [
{ latitude: 37.422, longitude: -122.084 },
{ latitude: 37.423, longitude: -122.085 },
{ latitude: 37.424, longitude: -122.083 },
],
});Zones are automatically persisted across app restarts. No hard limit on zone count (tested with 100+ zones on both platforms). Large polygons (1000+ points) are supported; the plugin uses Douglas-Peucker simplification to optimize complex polygons.
const subscription = Polyfence.instance.onGeofenceEvent((event) => {
switch (event.type) {
case 'enter':
console.log(`Entered: ${event.zoneId}`);
break;
case 'exit':
console.log(`Exited: ${event.zoneId}`);
break;
case 'dwell':
console.log(`Stayed in ${event.zoneId} for ${event.dwellDurationMs}ms`);
break;
// Recovery events fire ONLY when the tracking service was killed
// and restarted (Doze kill / OOM / force-stop / phone reboot). On
// the first GPS fix after restart, the SDK reconciles persisted
// zone state against the actual location and emits recoveryEnter /
// recoveryExit for any mismatch. Treat recovery events like
// enter/exit unless you specifically want to distinguish "user just
// crossed the boundary" from "user was already inside/outside when
// the tracking process resumed after being killed."
case 'recoveryEnter':
console.log(`Confirmed inside (post-restart): ${event.zoneId}`);
break;
case 'recoveryExit':
console.log(`Confirmed outside (post-restart): ${event.zoneId}`);
break;
// Signal-lost / restored fire only when degraded-GPS handling is
// enabled (gpsStalenessTimeoutMs > 0), and are distinct from recovery
// events: recoveryEnter/Exit fire when the tracking SERVICE was killed
// and restarted, whereas signalLost fires while the service is RUNNING
// but GPS has gone stale. If GPS goes fully stale while you're inside a
// zone, the SDK reports signalLost — membership is now uncertain, NOT
// exited — then signalRestored once a valid fix confirms you're still
// inside (or a normal exit if you actually left). NOTE: on signalLost,
// event.location is the LAST-KNOWN (stale / approximate) position, not
// a current fix — don't render it as the user's live location.
case 'signalLost':
console.log(`GPS lost, still assumed inside: ${event.zoneId}`);
break;
case 'signalRestored':
console.log(`GPS restored, confirmed inside: ${event.zoneId}`);
break;
}
});
// Don't forget to unsubscribe in cleanup
subscription.remove();Events fire whether your app is foregrounded, backgrounded, or the screen is locked. Here's what users see on each platform when zones fire in the background:
await Polyfence.instance.startTracking();Subscribe to
onErrorbefore calling any other SDK method.onErroris the SDK's central error channel — GPS failures, permission revocations, service issues, battery warnings, zone validation errors, and any other runtime error all route through this one subscription. Methods such asbatteryOptimizationStatus(),addZone(), andrequestPermissions()can emit errors as a side effect of being called; if no listener is attached at the time, that error is silently dropped — no retry, no replay, no warning in the return value.
const errorSubscription = Polyfence.instance.onError((error) => {
switch (error.type) {
case 'gpsPermissionDenied':
// Guide user to settings
break;
case 'gpsServiceDisabled':
// Prompt to enable GPS
break;
default:
console.log(`Error: ${error.message}`);
}
});| Method | Returns | Description |
|---|---|---|
initialize(config?) |
Promise<void> |
Initialize the geofencing engine |
startTracking() |
Promise<void> |
Start background GPS tracking |
stopTracking() |
Promise<void> |
Stop GPS tracking |
dispose() |
Promise<void> |
Clean up resources and stop tracking. Safe to re-initialize() afterwards (e.g. logout → login) — re-acquire via Polyfence.instance. |
removeAllListeners() |
void |
Remove all event listeners without disposing the engine |
| Method | Returns | Description |
|---|---|---|
addZone(zone) |
Promise<void> |
Add a circle or polygon zone |
removeZone(zoneId) |
Promise<void> |
Remove a zone by ID |
clearAllZones() |
Promise<void> |
Remove all zones |
getZoneStates() |
Promise<ZoneState[]> |
Get current INSIDE/OUTSIDE state for all zones |
getZoneStates()only reports reliable inside/outside state afterstartTracking(). Inside/outside is computed by the running location service, so before tracking starts there is nothing evaluated to report — on AndroidgetZoneStates()returns[]even afteraddZone(). Always follow this order:initialize()→addZone()→startTracking()→getZoneStates(). (To track membership while running, use theonZoneEnter/onZoneExitevents.)
addZone(zone)treatszone.idas the primary key — duplicate IDs silently overwrite. CallingaddZonewith anidalready being monitored replaces the previous zone entry; no error is thrown. Re-adding also resets the persisted INSIDE/OUTSIDE state for that zone (and on iOS, its confidence state). If the device is currently inside the zone, the next reconciliation may fire a freshenter/recoveryEnterevent. In-place metadata edits without a re-enter are a known limitation. If your workflow requires unique IDs, dedupe before calling.
| Method | Returns | Description |
|---|---|---|
getConfiguration() |
Promise<PolyfenceConfiguration> |
Get current configuration |
updateConfiguration(config) |
Promise<void> |
Update configuration |
resetConfiguration() |
Promise<void> |
Reset to defaults |
setAccuracyProfile(profile) |
Promise<void> |
Set GPS accuracy profile |
| Method | Returns | Description |
|---|---|---|
requestPermissions(options?) |
Promise<boolean> |
Request location permissions. Android: reads current state only, does not show a dialog — see Step 2 for the two-step Android flow. |
isLocationServiceEnabled() |
Promise<boolean> |
Check if location services are enabled |
batteryOptimizationStatus() |
Promise<BatteryOptimizationStatus> |
Check battery optimization status (Android) |
requestBatteryOptimizationExemption() |
Promise<void> |
Launch the Android system exemption dialog (fire-and-forget — re-poll batteryOptimizationStatus() to observe the user's response) |
| Method | Returns | Description |
|---|---|---|
debugInfo() |
Promise<PolyfenceDebugInfo> |
Get operational diagnostics in 5 groups: systemStatus, performance, battery, zones, recentErrors. For current configuration use getConfiguration(); for current zone membership use getZoneStates() |
getSessionTelemetry() |
Promise<SessionTelemetry> |
Get session metrics (GPS updates, zone events, battery impact) |
errorHistory(options?) |
Promise<PolyfenceError[]> |
Get recent errors |
| Method | Callback | Description |
|---|---|---|
onLocationUpdate(callback) |
(location: PolyfenceLocation) => void |
Raw GPS location updates |
onGeofenceEvent(callback) |
(event: GeofenceEvent) => void |
Zone enter / exit / dwell / recoveryEnter / recoveryExit / signalLost / signalRestored events |
onError(callback) |
(error: PolyfenceError) => void |
Central error channel — subscribe before any other SDK call. GPS / permission / service / battery / zone-validation errors all route here; errors fired without a listener are dropped silently |
onPerformance(callback) |
(payload: PerformanceEventPayload) => void |
Live GPS performance snapshots (type: 'runtime_status') emitted periodically by polyfence-core while tracking. See Performance Events |
onHealthScore(callback) |
(event: HealthScoreEvent) => void |
Periodic health score (0-100) with top issue |
onZoneEnter(callback) |
(event: GeofenceEvent) => void |
Zone enter events only |
onZoneExit(callback) |
(event: GeofenceEvent) => void |
Zone exit events only |
All event methods return a Subscription object with a remove() method to unsubscribe.
Polyfence.instance.onGeofenceEvent((event) => {
console.log({
zoneId: event.zoneId,
zoneName: event.zoneName,
type: event.type, // 'enter' | 'exit' | 'dwell' | 'recoveryEnter' | 'recoveryExit' | 'signalLost' | 'signalRestored'
location: event.location, // includes location.activity ('still' | 'walking' | 'running' | 'cycling' | 'driving' | 'unknown')
timestamp: event.timestamp,
detectionTimeMs: event.detectionTimeMs, // ms the engine took to detect the transition
distanceToBoundaryM: event.distanceToBoundaryM, // metres from event location to zone boundary
dwellDurationMs: event.dwellDurationMs, // only set on DWELL events
});
});Polyfence.instance.onLocationUpdate((location) => {
console.log({
latitude: location.latitude,
longitude: location.longitude,
accuracy: location.accuracy, // metres, undefined until the first GPS fix
speed: location.speed, // m/s
activity: location.activity, // 'still' | 'walking' | 'running' | 'cycling' | 'driving' | 'unknown'
timestamp: location.timestamp,
});
});Polyfence.instance.onError((error) => {
console.log({
type: error.type, // 'gpsPermissionDenied' | 'gpsServiceDisabled' | ...
message: error.message,
context: error.context,
correlationId: error.correlationId,
timestamp: error.timestamp,
});
});onPerformance surfaces live GPS performance snapshots emitted periodically by polyfence-core LocationTracker while tracking is running (typically every ~30 seconds and on strategy / accuracy-profile change). The channel filters to type: 'runtime_status' — health-score events travel on their own onHealthScore subscription, and internal SDK state broadcasts are not surfaced.
const subscription = Polyfence.instance.onPerformance((payload) => {
// Engine runtime status — emitted periodically (and on change) by
// polyfence-core LocationTracker. Shape: { type: 'runtime_status',
// data: { strategy, intervalMs, accuracyProfile, nearestZoneDistanceM,
// isStationary, batteryMode, gpsAccuracy, currentGpsAccuracy,
// secondsSinceLastGpsFix, gpsAvailabilityDrops5Min, timestamp } }
const data = payload.data as Record<string, unknown>;
console.log({
strategy: data.strategy, // 'CONTINUOUS' | 'INTELLIGENT' | ...
intervalMs: data.intervalMs,
gpsAccuracy: data.gpsAccuracy, // metres (current fix)
currentGpsAccuracy: data.currentGpsAccuracy, // metres (last health-tracked fix; null until first fix)
nearestZoneDistanceM: data.nearestZoneDistanceM,
});
});
// Remove the subscription during cleanup (e.g. useEffect return).
subscription.remove();// Maximum accuracy (highest battery usage)
await Polyfence.instance.setAccuracyProfile('maxAccuracy');
// Balanced accuracy/battery (DEFAULT - recommended)
await Polyfence.instance.setAccuracyProfile('balanced');
// Battery-optimized for background monitoring
await Polyfence.instance.setAccuracyProfile('batteryOptimal');
// Intelligent auto-adjustment
await Polyfence.instance.setAccuracyProfile('adaptive');| Profile | Update Interval | Battery Impact | Use Case |
|---|---|---|---|
| maxAccuracy | 5 seconds | High | Delivery, navigation, fleet tracking |
| balanced | 10 seconds | Medium | Most location-aware apps (DEFAULT) |
| batteryOptimal | 30 seconds | Low | Background monitoring, casual use |
| adaptive | Dynamic | Variable | Apps with varying accuracy needs |
await Polyfence.instance.updateConfiguration({
dwellSettings: {
enabled: true,
dwellThresholdMs: 5 * 60 * 1000, // 5 minutes
},
});
Polyfence.instance.onGeofenceEvent((event) => {
if (event.type === 'dwell') {
console.log(`User confirmed in ${event.zoneId}`);
}
});For apps with 100+ zones, clustering improves performance:
await Polyfence.instance.updateConfiguration({
clusterSettings: {
enabled: true,
activeRadiusMeters: 5000, // Check zones within 5km
},
});Track only during specific time windows:
await Polyfence.instance.updateConfiguration({
scheduleSettings: {
enabled: true,
timeWindows: [
{
startTime: { hour: 9, minute: 0 },
endTime: { hour: 17, minute: 0 },
daysOfWeek: [1, 2, 3, 4, 5], // Monday-Friday
},
],
},
});Automatically detect activity and optimize GPS:
await Polyfence.instance.updateConfiguration({
activitySettings: {
enabled: true,
// Per-activity update intervals in milliseconds. Defaults are
// sensible; override only the ones you want to tune.
stillIntervalMs: 120000, // 2 min when stationary
walkingIntervalMs: 15000, // 15s when walking
runningIntervalMs: 10000,
cyclingIntervalMs: 8000,
drivingIntervalMs: 5000,
},
});Additional Permissions Required (Android):
<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" />Polyfence requires native modules and cannot run on Expo Go. Use expo-dev-client for a custom development build:
npx create-expo-app MyApp
cd MyApp
npx expo install expo-dev-client
npx expo prebuild --clean
npm install polyfence-react-native
npx expo run:ios # or run:androidSee Expo Custom Development Client docs for more details.
Zero PII about your end users. The only personal information Polyfence holds is the developer's account info (email, billing) — same as any paid SaaS.
Different defaults for different data classes:
- Positions — opt-in. Never persisted on Polyfence servers by default. If you turn retention on, positions are stored in your tenant — never names, phones, emails, or health data.
- Anonymous telemetry — opt-out, one line disables (see below). Never coordinates, never identifiers, never PII — only aggregates (platform, plugin version, accuracy averages, error counts).
- Zone events — always on. They're the value we deliver, not surveillance.
This is the deliberate posture, not an inconsistency. See PRIVACY.md for the full breakdown.
await Polyfence.instance.initialize(undefined, { disableTelemetry: true });- On-device geofencing: All zone detection runs locally using native GPS APIs
- Local persistence: Zones stored in SharedPreferences (Android) / UserDefaults (iOS)
- No tracking: No user behavior tracking, no cross-app tracking
- GDPR/CCPA-friendly: Anonymous aggregates only by default, one-line disable for telemetry, opt-in for position retention
Additive release — no public API removed. New surfaces:
GeofenceEventType.signalLost/.signalRestored— reported when GPS goes stale while a device is inside a zone. Opt-in viaPolyfenceConfiguration.gpsStalenessTimeoutMs(0= off, the default).
Behavioural change on Android — initialize(config). If you pass any tracking-config key to initialize, the LocationTracker foreground Service now starts as part of initialize rather than being deferred until startTracking(). On Android 8+, context.startService from a backgrounded context throws IllegalStateException — so initialize will now REJECT where it previously silently dropped the config. This is a bug fix (the old behaviour hid misconfigured init calls), but callers that init from a background service must catch the rejection and retry once the app foregrounds. Apps that call initialize on cold-start or from the foreground see no change. iOS is unaffected.
For the full change list, see CHANGELOG.md.
Always remove event subscriptions in cleanup to prevent memory leaks:
useEffect(() => {
const subscription = Polyfence.instance.onGeofenceEvent((event) => {
// handle event
});
return () => {
subscription.remove();
};
}, []);Zones are automatically persisted across app restarts — no manual persistence needed. When loading zones from an external source, consider delta-based sync to avoid re-registering all zones.
Some Android manufacturers aggressively kill background services. If tracking stops, the user likely needs to whitelist your app. See dontkillmyapp.com for device-specific instructions.
If you see "NativeModule not found" error:
- Rebuild the app so autolinking picks up the module (RN 0.60+ autolinks —
react-native linkis obsolete); on iOS runcd ios && pod installthen rebuild - For Expo: Use
expo-dev-client(not Expo Go) - Clear build caches:
rm -rf node_modules && npm install && cd ios && rm -rf Pods && pod install
cd ios
rm -rf Pods Podfile.lock
pod repo update
pod installEnsure minimum SDK is 24+:
android {
defaultConfig {
minSdkVersion 24
}
}The following Flutter APIs are intentionally deferred from this package:
enableIntelligentOptimization(),enableProximityOptimization(),enableMovementOptimization()— ML-powered optimization APIs. These will be added when the intelligence layer is integrated (planned for a future release).zonesgetter — UsegetZoneStates()to query current zone state.currentConfigurationgetter — UsegetConfiguration()instead.statusStream— UseonPerformance()event listener for runtime status updates.
These gaps will be addressed in subsequent releases.
Android — Filter logcat:
adb logcat | grep -E "LocationTracker|GeofenceEngine|Polyfence"iOS — Filter Xcode console:
LocationTracker GeofenceEngine Polyfence
Programmatic debugging — Use the debug API:
const debug = await Polyfence.instance.debugInfo();
// systemStatus — permissions, GPS, last fix
console.log('Last fix:', debug.systemStatus.lastLocationUpdate); // ms since epoch, 0 if no fix yet
console.log('GPS enabled:', debug.systemStatus.isGpsEnabled);
console.log('Permission granted:', debug.systemStatus.isLocationPermissionGranted);
// zones — counts (use getZoneStates() for inside/outside membership)
console.log('Active zones:', debug.zones.activeZones);
// performance / battery
console.log('Detections:', debug.performance.totalZoneDetections);
console.log('Battery:', debug.battery.batteryLevel, debug.battery.isCharging ? '⚡' : '');
// recent errors (already normalized to PolyfenceError shape)
debug.recentErrors.forEach((err) => console.log(err.type, err.message));Note:
debugInfo()is for operational diagnostics. For the current configuration (accuracy profile, update strategy, intervals) callgetConfiguration(). For the current zone membership (which zones the user is inside right now) callgetZoneStates()or subscribe toonZoneEnter/onZoneExit.
When opening a GitHub issue, include:
- Output of
Polyfence.instance.debugInfo() - Device manufacturer and OS version (e.g., Samsung Galaxy S24, Android 14)
- Whether battery optimization is disabled
- Logcat/Xcode console output
- Minimal code sample to reproduce
Contributions are welcome. See CONTRIBUTING.md for development setup, code style, and PR guidelines.
- Plugin Issues: GitHub Issues
- Questions & Discussions: Open an issue with the
questionlabel - Security Issues: See SECURITY.md
- Commercial Support: polyfence.io
MIT — see LICENSE
Copyright (c) 2026 Polyfence





