Skip to content

Geofencing

Sergio Hernandez edited this page Jul 24, 2026 · 1 revision

Geofencing

The Geofencing service owns geographic zones and the visit events transporters generate against them. It evaluates positions fed by the Router and emits alerts through the Manager.

Gated by the geofencing account feature.

Related pages: Database · Router · Manager


Geometry model

A geofence always stores a polygon in Geom, whatever shape the user drew:

Shape Stored as
Polygon The polygon itself
Circle CircleCenter (Point) + CircleRadiusMeters as metadata, plus a 64-segment buffered polygon in Geom, computed in GeofenceWriter at write time

Because Geom is always a polygon, ST_Contains detection, the GiST index, reporting and map overlays are all shape-agnostic. Editors render the true circle from the metadata rather than the buffered approximation.

Validation

Create and update DTOs enforce polygon coordinates XOR circle (centre + radius, 10 m – 100 km), validated with NetTopologySuite's IsValidOp — a self-intersecting polygon is rejected with details.

Geometry inputs are validated, never silently degraded. All of these are 400s from GeofenceDtoValidator:

  • circle centres beyond ±85° latitude;
  • circle rings crossing the ±180° meridian (planar PostGIS plus normalized positions would misdetect);
  • polygon coordinates outside valid latitude/longitude ranges.

Visit-pair event model

One geofenceevents row represents one visit: an entry timestamp plus a nullable departure. The row carries AccountId directly (not only through its geofence) and a DwellAlertedAt stamp for the one-time dwell alert.

Concern Mechanism
Idempotency The open-visit partial index, plus an identical-entry-timestamp guard in the writer — a redelivered completed visit is skipped
Exit debounce 30 s
Dwell GeofenceDwellEvaluationService scans open visits every 60 s against each geofence's DwellThresholdMinutes

Visit history is permanent. A geofence with recorded visits cannot be deleted — the attempt returns ConflictExceptionCONFLICT. Deactivation is the supported way to retire a zone, and a deactivated geofence keeps its history readable.


Detection and alert emission

sequenceDiagram
    participant Router
    participant Geo as Geofencing
    participant DB as geofencing schema
    participant Mgr as Manager

    Router->>Geo: processPositions(batch)
    Geo->>DB: ST_Contains against active geofences
    DB-->>Geo: entries / exits
    Geo->>DB: upsert visit rows (commit)
    Geo->>Mgr: recordAlertEvent (geofence_client identity)
    Note over Geo,Mgr: inline, post-commit, best-effort
Loading

Alert emission goes through TrackHub.Geofencing.Infrastructure.ManagerApi (AlertEmitter, BackgroundJobRunRecorder), calling Manager's recordAlertEvent and createBackgroundJobRun under the service's own geofence_client identityCreateClient(Clients.Manager, asService: true), never the propagated caller token.

Event Raised by Severity Dedup key
GeofenceEntered GeofenceDetectionService, inline post-commit, per-geofence AlertOnEntry opt-in Info geofence-enter:{eventId:N}
GeofenceExited Same, per-geofence AlertOnExit opt-in Info geofence-exit:{eventId:N}
GeofenceDwellExceeded GeofenceDwellEvaluationService (60 s scan) Warning geofence-dwell:{eventId:N}

The dwell evaluator stamps DwellAlertedAt only after successful emission, which is retry-safe thanks to Manager's dedup.

Emission is best-effort. A failure logs and never fails position processing — a Manager outage must not stop geofence detection or lose positions.


Queries

Query Shape
geofencesByAccount GeofencesPageVm { items, totalCount } — take clamped 1..500, with type / active / name-ILike filters
geofenceEvents GeofenceEventsPageVm — required From/To (≤ 92 days), geofenceId and openOnly filters, take clamped 1..500, returns dwellSeconds and totalTime
transportersInGeofence(geofenceId, type) Optional arguments

geofencesByAccount carries no [Caching], and its enableCaching input is inert. The cache key is built from request properties only and cannot scope to the caller's account, so caching it would leak geofences across tenants. Do not re-add it. This is the case that established the platform-wide rule against [Caching] on caller-scoped queries.

Reporting's GeofenceReader drains pages at 500 per page with a 100 000-row defensive cap.


Portal

/geofenceManager is a single-purpose screen: an editor with polygon and circle tools on both map providers, a server-paged list (status filter defaulting to Active), debounced name search in the navbar, and compact type/status selects over the map.

  • Full-set surfaces (editor map, dashboard overlay) drain server pages via getAllGeofencesByAccount — 500 per page, 10 000-row defensive cap.
  • The OSM editor renders a single declarative layer per geofence; leaflet-editable drives editing through the react-leaflet layer refs.
  • The geofence map auto-fits to the account's geofence bounds; dashboard and geofence maps share the same base-map presentation.
  • Dashboard overlays render circles from the metadata, not the buffered polygon.

Geofence event history is not a portal query. It lives in the Reports screen's Geofence Events report — the portal ships no geofenceEvents document, because Reporting is that query's consumer.


Schema access

Geofencing owns the geofencing schema (geofences, geofenceevents) and maps app.accounts, app.account_features and app.audit_events read-only, excluded from its migrations. It also uses the vw_transporter_position and vw_user views for user-scoped access.

PostGIS is required. One CREATE EXTENSION postgis covers both this schema and trip — they share the TrackHub database.

Clone this wiki locally