-
Notifications
You must be signed in to change notification settings - Fork 1
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
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.
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.
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
ConflictException→CONFLICT. Deactivation is the supported way to retire a zone, and a deactivated geofence keeps its history readable.
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
Alert emission goes through TrackHub.Geofencing.Infrastructure.ManagerApi (AlertEmitter, BackgroundJobRunRecorder), calling Manager's recordAlertEvent and createBackgroundJobRun under the service's own geofence_client identity — CreateClient(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.
| 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 |
geofencesByAccountcarries no[Caching], and itsenableCachinginput 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.
/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
geofenceEventsdocument, because Reporting is that query's consumer.
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.
Platform
- Architecture
- Technology
- Inter-Service Communication
- Database
- Security and Identity
- User Permissions Overview
Services
- Manager
- Telemetry
- Router
- Adding a Provider
- Geofencing
- Trip Management
- Reporting
- Common Library
- Frontend
Engineering
User docs
- User Guide (ships in the app)