Skip to main content

Geofencing

A geofence is a named boundary on the Earth's surface. Devices report where they are, and detection rules ask whether a reported position is inside a fence. Both halves move over time — where the device was and what the fence was — so an honest answer depends on comparing the right version of each.

Status

Available: geofence authoring in the console (draw, edit, delete) or over GraphQL, POLYGON_2D boundaries with holes, spherical containment in detection rules, and a frozen archive of every fence set with a browsable history. Planned: additional geometry kinds — the schema reserves POLYGON_2_5D and VOXEL_3D, both rejected on write today.

Drawing a fence​

In the console, geofences live under Areas → Geofences. Click the map to place a corner, click the first corner to close the shape, and save. On an existing fence, you can:

  • drag a corner to move it,
  • Alt-click a corner to remove it, or
  • click the small handle on an edge to add a corner there.

A fence carries a token — the identifier rules use to name it — plus an optional name and description. The token is fixed once created: the console shows it read-only when editing, and the API refuses an update that would change it. You can change the name at any time, and changing it breaks nothing.

The token is fixed because a rule names a fence by its token, inside rule text the platform cannot rewrite for you. A rename would leave every geo.inFence("old-token") naming nothing. If you need a different token:

  1. Create the new fence.
  2. Repoint the rules that name the old one.
  3. Delete the old fence.

That order makes you deal with the rules rather than discover them later.

The map behind your fence​

The editor draws on the tenant's basemap. A new instance ships with one already configured, so tiles appear without any setup.

A tenant admin changes the tile source once, for everyone, under Settings → Map. The fields in this editor are a personal override on top of that, remembered in your browser only, for trying a provider before committing it tenant-wide. See Basemaps for how the tiers resolve, why the tile URL and its attribution move together, which provider ships by default, and how to protect a provider API key.

If no tier has a tile source — an operator set the instance default to {} and the tenant set nothing — the editor falls back to a bundled world map. It shows public-domain continents and country borders, compiled into the console, and nothing at street zoom. Drawing still works, and the coordinates you place are exact either way, because it uses the same projection a tiled map uses.

Boundary rules​

Boundaries are stored as GeoJSON, with positions in [longitude, latitude] order and rings closed explicitly (the first position repeated as the last). The console handles this for you; an integration authoring over GraphQL supplies the document directly.

Three limits apply, and each bounds a different cost:

LimitDefaultWhat it bounds
Positions in one fence512What compiling that fence, and holding it compiled, costs
Fences per tenant100How large the announcement of a fence-set change is
Positions across your whole fence set51,200How much of the shared detection engine's geometry your fences occupy

The position count includes each ring's closing position. The whole-set total counts distinct shapes: two fences drawn identically cost one shape, not two.

These are defaults, not fixed platform limits. Each is part of your plan, and an operator can raise or lower any of them for your tenant. The defaults are consistent with each other — 100 fences of 512 positions is exactly 51,200 — so a tenant that has never had them changed can reach all three.

The limits are checked when a fence is saved, because a rule's authoring cost gate cannot see a number that lives on a fence rather than in a rule. The fence count does not affect how long a single containment test takes: a rule names one fence, and the engine reaches that fence directly.

Saving when you are over a limit​

Changing a fence is only refused when it makes things worse

If your tenant is already over a limit, you keep every fence you have. Saving is refused only when the change would make the number larger than it already is.

Your tenant can be over a limit because a plan changed, or because the fences predate the limit. In that state:

  • Editing a fence's name or description always works.
  • Deleting a fence always works.
  • Making a fence smaller almost always works. The exception: because the whole-set total counts distinct shapes, making one of several identically-drawn fences different separates it from the others and can raise your total even though that fence got smaller. The refusal says so when it happens.
  • Deleting lowers the stored total, so an over-limit tenant that deletes a fence cannot recreate it. To move a fence to a new token, create the new one first, then delete the old.

Rings must bound an area​

A boundary must bound an area. A ring whose edges cross — a bow-tie — has no well-defined interior, so a containment question about it has no honest answer. Such rings are refused when you save, by the same check the detection engine applies when it compiles a fence set's geometry.

This matters more than it sounds. Before the check ran at authoring time, such a fence saved cleanly, sat in the registry looking healthy, and failed only later, when a rule finally named it.

The console warns before the server refuses

While you draw, the console flags a self-crossing shape immediately. Its check is a flat approximation of the server's spherical one, so the two can disagree on very large fences or ones spanning the antimeridian. That is why the console only warns; the server's answer decides.

Spherical containment​

Containment is computed on a sphere, not on a flat map, and that is not a refinement. Treating longitude and latitude as x and y gives wrong answers in two places real fences sit:

  • Across the antimeridian. A fence spanning 179°E to 179°W is 2° wide. Read as flat coordinates, it becomes a 358°-wide band covering most of the world — answering "inside" for a device in the Atlantic and "outside" for one standing in the fence.
  • At high latitudes. The shortest path between two points at the same latitude bows toward the pole, so a "rectangle" drawn as four corners is not bounded by lines of constant latitude. On a box covering 10°W–10°E and 80°–81°N, a point at 80.05°N is outside the real fence at the centre and inside at the edges. Flat maths says both are inside.

Where the edge counts​

Three edge cases are each decided one way and applied uniformly:

  • The boundary is inside. A position exactly on a fence's edge is contained. Left to the underlying geometry library, the boundary would be split between adjacent regions, so two fences sharing an edge would each claim part of it and neither would claim the rest. An explicit on-edge test avoids that, with a tolerance of about 6 mm on the ground.
  • A hole's edge is inside too, by the same rule. A position strictly inside a hole is outside the fence; a position on the hole's ring is inside the fence. Two adjacent fences, or a fence and its own hole, never disagree about a point they share.
  • Ring direction does not matter. Clockwise and counter-clockwise spellings of the same ring give the same answer. The winding is normalised rather than trusted, so an integration authoring GeoJSON over the API does not have to get it right. The one shape this cannot rescue is a "fence" so large it wraps most of the globe, where "the smaller region" is ambiguous. The position limit and the requirement to bound an area put that well outside anything a real fence looks like.

Fence-set versions and history​

A change to the fence set — a fence created, a boundary edited, a fence deleted — freezes the whole fence set into a new version. Each version stores the geometry of every fence as it stood at that moment, so the shapes survive later edits and deletions.

Renaming a fence, or editing its description or metadata, does not create a version. A name changes how no event is judged, so a new version would freeze exactly the shapes the previous one already holds. That is why your version count can stay the same after a save that plainly changed something.

Every location event is stamped with the fence-set version in force when it arrived. That stamp makes replaying a rule over past events meaningful: last week's events are judged against last week's fences. Without it, a preview would answer from today's shapes and be quietly fictional — confident, plausible, and about a world that never existed.

The History tab on a geofence shows the boundary as it was at any version, with the current shape drawn behind it for comparison. It gives one of three answers:

What you seeWhat it means
The boundaryThe shape stored under this token at that version.
"Not in the set at version N"The fence did not exist then — created later, or deleted and its token reused. Not the same as existing with no shape.
"Shape this viewer cannot draw"It was in the set and was enforced; only the console cannot render it.

Deleting a fence is permanent and frees its token for reuse. So an entry in an old version tells you the shape stored under a token at that time — not necessarily that it belonged to the fence you are looking at now.

Using a fence in a rule​

Detection rules reach a fence by token:

geo.inFence("yard-perimeter")

The predicate answers for the position on the event being evaluated, against the fence set that event was stamped with.

A rule naming a fence that cannot be compiled, because its ring does not bound an area, still compiles and publishes. The fence stays in the set carrying its error. At evaluation, every sample against it is skipped and counted as an evaluation error rather than answered arbitrarily — the same outcome as an unknown fence token, described below.

A fence test and a measurement test cannot share a condition​

A condition that calls geo.inFence(...) and reads a measurement is refused when you publish it:

geo.inFence("yard") && m["temp"] > 80 ← refused

This is not a style rule: no event could ever satisfy it. A location event reports a position and carries no measurements; a measurement event carries readings and reports no position. A condition needing both is fed nothing, forever, and would sit in your rule list reporting healthy while never firing. Publish is the only point where both halves are visible together; at evaluation, each is just a sample that did not qualify.

Express it as two rules instead — one on the fence, one on the measurement. Or, if the value you are testing changes rarely, put it on the device as an attribute, which a location event does carry.

Unknown or missing fences​

geo.inFence("typo") compiles and publishes: nothing checks at publish time that the token names a real fence. At evaluation the call cannot answer, and the platform will not invent an answer. The sample is skipped and counted as an evaluation error, never answered "outside". Answering "outside" would be worse than useless: for a rule holding a condition over time, it would look like the device leaving the fence.

Four situations produce this, and only the first is a mistake:

SituationWhat you see
The token is misspelled, or the fence was deletedEvaluation errors on every location event, from the moment the rule goes live
Previewing a rule over events from before the fence was drawnErrors for the whole stretch before it existed — the fence genuinely was not in the set then
A rule authored against a fence that exists in a later version than the events being replayedThe same, and for the same reason
A tenant's very first fence rule, in the seconds after it is publishedTransient; the engine loads the fence set as the rule arrives

Evaluation errors appear in two places only:

  • per rule, on the authoring preview, as its evaluation-error count;
  • in aggregate, on the detection engine's devicechain_eventprocessing_detect_fanout_eval_errors_total metric, which the DetectFanoutEvalErrors alert watches when the Helm chart's metrics alerts are enabled.

The rule-health view does not show them, and a rule producing errors on every sample still reads ACTIVE there. Nothing points at the geofence as the cause. If a fence rule is producing errors and nothing else, check the token first.

Fence sets in memory​

The live engine holds the four most recent fence-set versions per tenant: the current one plus three superseded. That is sized for events still in flight, which are seconds to minutes old. Reaching the bound takes four changes to the fence set while an event is between the ingest path and the engine. An event stamped with an evicted version reports the same counted evaluation error as an unknown fence.

Nothing is lost from history when that happens. Every version's snapshot is stored durably, and the preview and replay paths read from there rather than from the live cache. Only live evaluation is bounded, and it recovers on its own: the next events carry the current version.

The engine also re-reads each tenant's current fence set from stored history every few minutes. Normally that changes nothing, because a fence edit is announced to the engine as it happens. It matters in one uncommon case: saving a fence never fails because the announcement could not be sent, so if the announcement is lost, the engine would otherwise keep evaluating against the older version until it next restarted. The periodic re-read closes that gap, so a fence edit takes effect within a few minutes at the outside, even if its announcement never arrives — no restart, and no need to re-save the fence.

For how detection rules are authored and evaluated, see event processing. For the location data itself, see connecting a device.

Permissions​

Reading geofences and their history requires device:read, and deleting one requires device:write — the same authorities that govern the rest of the device registry.

Creating or changing a fence additionally requires location:read. Drawing a fence is not only a write: it asks a question about where devices are. Someone who could create fences with device:write alone could place a small one, watch whether any rule reacts, move it, and read a fleet's positions out of the answers — without ever holding the authority to see a coordinate. Deleting stays on device:write, because removing a fence asks nothing.