# How the TeslaMate Geo-Fence Feature Creates Custom Locations: A Complete Technical Guide

> Learn how the TeslaMate Geo-fence feature creates custom locations by validating parameters, persisting to PostgreSQL, and enabling spatial queries. A technical deep dive.

- Repository: [TeslaMate/teslamate](https://github.com/teslamate-org/teslamate)
- Tags: deep-dive
- Published: 2026-06-16

---

**The TeslaMate Geo-fence feature creates custom locations by validating circular boundary parameters through Ecto changesets, persisting them to PostgreSQL inside atomic transactions, and registering them for spatial queries using the `within_geofence?` macro.**

The TeslaMate Geo-fence feature allows users to define named circular zones around specific coordinates to track vehicle presence, monitor charging sessions, and calculate costs. Understanding how this feature creates custom locations requires examining the Elixir LiveView interface, Ecto schema validation, and PostgreSQL spatial query implementation in the teslamate-org/teslamate repository.

## Geo-Fence Schema and Data Structure

Every custom location begins as a `%GeoFence{}` struct defined in [`lib/teslamate/locations/geo_fence.ex`](https://github.com/teslamate-org/teslamate/blob/main/lib/teslamate/locations/geo_fence.ex). This Ecto schema maps to the `geo_fences` table and stores the spatial parameters required to define a circular boundary zone.

The schema captures:
- **name**: User-defined identifier for the location
- **latitude** and **longitude**: Center point coordinates in decimal degrees
- **radius**: Circular boundary distance in meters (defaults to 20)
- **billing fields**: Optional cost calculation parameters for charging sessions

```elixir

# lib/teslamate/locations/geo_fence.ex

%GeoFence{
  radius: 20,
  latitude: lat,
  longitude: lng
}

```

## The Custom Location Creation Pipeline

Creating a custom location follows a three-stage pipeline involving the LiveView interface, changeset validation, and transactional persistence.

### Step 1: LiveView Form Initialization

When users initiate geo-fence creation, the `TeslaMateWeb.GeoFenceLive.Form` module pre-populates a `%GeoFence{}` struct with either manually entered coordinates or the vehicle's last known position. The form assigns a default radius of 20 meters and maintains the struct in the socket assigns for state management.

```elixir

# lib/teslamate_web/live/geofence_live/form.ex

%GeoFence{
  radius: 20,
  latitude: lat,
  longitude: lng
}

```

### Step 2: Changeset Validation

Upon form submission, the system validates input through `Locations.change_geofence/2` in [`lib/teslamate/locations.ex`](https://github.com/teslamate-org/teslamate/blob/main/lib/teslamate/locations.ex). This function constructs an `Ecto.Changeset` that enforces required fields, validates radius constraints, and sanitizes the name field before permitting database insertion.

```elixir

# Validation pipeline inside the LiveView

defp validate(params, %{assigns: assigns}) do
  changeset = Locations.change_geofence(assigns.geofence, params)

  with {:ok, geofence} <- Ecto.Changeset.apply_action(changeset, :update) do
    {:ok, geofence, changeset}
  end
end

```

### Step 3: Transactional Persistence

The `Locations.create_geofence/1` function wraps database operations in `Repo.transaction` to ensure atomicity. It inserts the geo-fence record using `GeoFence.changeset/2`, then immediately registers the fence with `apply_geofence/1` to activate it for system-wide spatial queries.

```elixir

# lib/teslamate/locations.ex

def create_geofence(attrs) do
  Repo.transaction(fn ->
    with {:ok, geofence} <- %GeoFence{} |> GeoFence.changeset(attrs) |> Repo.insert(),
         :ok <- apply_geofence(geofence) do
      geofence
    else
      {:error, reason} -> Repo.rollback(reason)
    end
  end)
end

```

## Spatial Querying with Custom Locations

Once persisted, custom locations function as spatial predicates through the `within_geofence?` macro in [`lib/teslamate/custom_expressions.ex`](https://github.com/teslamate-org/teslamate/blob/main/lib/teslamate/custom_expressions.ex). This macro generates PostgreSQL earth distance calculations using `earth_box` and `earth_distance` functions to determine if vehicle coordinates fall within a fence's radius.

```elixir

# lib/teslamate/custom_expressions.ex

defmacro within_geofence?(position, geofence, :right) do
  quote do
    fragment(
      """
      earth_box(ll_to_earth(?::numeric, ?::numeric), ?) @> ll_to_earth(?::numeric, ?::numeric) AND
      earth_distance(ll_to_earth(?::numeric, ?::numeric), ll_to_earth(?::numeric, ?::numeric)) < ?
      """,
      ^unquote(geofence).latitude,
      ^unquote(geofence).longitude,
      ^unquote(geofence).radius,
      unquote(position).latitude,
      unquote(position).longitude,
      ^unquote(geofence).latitude,
      ^unquote(geofence).longitude,
      unquote(position).latitude,
      unquote(position).longitude,
      ^unquote(geofence).radius
    )
  end
end

```

## Complete LiveView Save Handler Implementation

The following implementation from [`lib/teslamate_web/live/geofence_live/form.ex`](https://github.com/teslamate-org/teslamate/blob/main/lib/teslamate_web/live/geofence_live/form.ex) demonstrates the complete event handling flow when users save a new custom location:

```elixir
def handle_event("save", %{"geo_fence" => params}, socket) do
  with {:ok, geofence, changeset} <- validate(params, socket),
       {:ok, socket} <- show_modal_or_save(geofence, changeset, socket) do
    {:noreply, socket}
  else
    {:error, %Ecto.Changeset{} = changeset} ->
      {:noreply, assign(socket, changeset: changeset, show_errors: true)}
  end
end

defp validate(params, %{assigns: assigns}) do
  changeset = Locations.change_geofence(assigns.geofence, params)

  with {:ok, geofence} <- Ecto.Changeset.apply_action(changeset, :update) do
    {:ok, geofence, changeset}
  end
end

defp save(%{assigns: %{action: :new, geofence: _}} = socket) do
  Locations.create_geofence(socket.assigns.changeset.params)
end

```

## Summary

- The **Geo-fence feature** creates custom locations using a circular boundary model defined by latitude, longitude, and radius parameters stored in the `geo_fences` table.
- **LiveView forms** in [`lib/teslamate_web/live/geofence_live/form.ex`](https://github.com/teslamate-org/teslamate/blob/main/lib/teslamate_web/live/geofence_live/form.ex) handle UI state and coordinate pre-population, defaulting to a 20-meter radius for new locations.
- **Ecto changesets** via `Locations.change_geofence/2` validate all inputs before database insertion, ensuring data integrity for spatial calculations.
- **Transactional safety** in `Locations.create_geofence/1` guarantees that geo-fences are only activated after successful database persistence and registration.
- The **`within_geofence?` macro** enables efficient spatial queries using PostgreSQL's earth distance functions to match vehicle positions against custom locations without loading entire tables into memory.

## Frequently Asked Questions

### What database does TeslaMate use for storing geo-fence custom locations?

TeslaMate uses **PostgreSQL** with spatial extensions to store geo-fence data. The `within_geofence?` macro leverages PostgreSQL-specific functions including `earth_box`, `earth_distance`, and `ll_to_earth` to perform efficient spherical distance calculations on the database server, minimizing application memory usage when checking vehicle positions against multiple custom locations.

### Can geo-fences overlap, and how does TeslaMate handle multiple custom locations at the same coordinates?

Yes, overlapping geo-fences are permitted. The system treats each custom location as an independent record in the `geo_fences` table. When querying vehicle positions, the application checks against all active fences using the `within_geofence?` predicate, allowing a single coordinate to match multiple custom locations simultaneously for different tracking purposes such as home charging and workplace monitoring.

### What is the default radius when creating a new geo-fence in TeslaMate?

The default radius is **20 meters**. This value is hardcoded in the `TeslaMateWeb.GeoFenceLive.Form` module when initializing a new `%GeoFence{}` struct. Users can modify this value through the web interface before saving the custom location to adjust the detection boundary for their specific use case, such as expanding coverage for large parking structures or reducing it for precise driveway detection.

### How does TeslaMate validate geo-fence data before saving?

Validation occurs through `Ecto.Changeset` operations in `Locations.change_geofence/2` defined in [`lib/teslamate/locations.ex`](https://github.com/teslamate-org/teslamate/blob/main/lib/teslamate/locations.ex). The changeset validates required fields (name, latitude, longitude, radius), enforces radius limits, and sanitizes the name field. Invalid submissions return error tuples that the LiveView handles by re-rendering the form with validation errors displayed to the user, preventing malformed data from reaching the database.