# How TeslaMate Handles Global vs Per-Car Configuration in the Settings Module

> Explore how TeslaMate's Settings module manages global vs per-car configuration using distinct Ecto schemas and a fallback pattern for efficient management.

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

---

**TeslaMate uses two distinct Ecto schemas—`GlobalSettings` for application-wide defaults and `CarSettings` for vehicle-specific overrides—to separate global configuration from per-car preferences using a fallback resolution pattern.**

TeslaMate, the open-source Tesla data logger built with Elixir and Phoenix, manages application configuration through a sophisticated dual-layer settings architecture. Understanding how the **Settings module handles global vs per-car configuration** is essential for customizing individual vehicle behavior while maintaining consistent application defaults across your fleet. The implementation stores these values in separate database tables and resolves them through a hierarchical lookup mechanism.

## Understanding the Dual-Layer Architecture

The TeslaMate codebase maintains configuration in two isolated scopes that operate independently yet complement each other during runtime resolution.

### Global Settings (GlobalSettings)

The `TeslaMate.Settings.GlobalSettings` module, defined in [`lib/teslamate/settings/global_settings.ex`](https://github.com/teslamate-org/teslamate/blob/main/lib/teslamate/settings/global_settings.ex), manages application-wide defaults that apply universally unless overridden. This schema maps to the `settings` table and stores values such as `:unit_of_length`, `:unit_of_temperature`, `:unit_of_pressure`, `:preferred_range`, `:base_url`, `:grafana_url`, `:language`, and `:theme_mode`.

The system treats this table as a singleton—only the first row returned by `Repo.one(GlobalSettings)` is considered the active global configuration. The schema's `changeset/2` function casts permitted attributes, validates required fields, trims URL values, and runs `validate_url/2` to ensure endpoint validity. An `@supported_languages` map restricts language codes to predefined options using `validate_inclusion/3`.

### Per-Car Settings (CarSettings)

Vehicle-specific configurations reside in `TeslaMate.Settings.CarSettings`, located in [`lib/teslamate/settings/car_settings.ex`](https://github.com/teslamate-org/teslamate/blob/main/lib/teslamate/settings/car_settings.ex). This schema connects to the `car_settings` table and contains fields like `:suspend_min`, `:suspend_after_idle_min`, `:req_not_unlocked`, `:free_supercharging`, `:use_streaming_api`, `:enabled`, and `:lfp_battery`.

Each car maintains a one-to-one relationship with its settings record through `has_one :car, Car, foreign_key: :settings_id`. The `changeset/2` function in this module casts all fields defined in `@all_fields` and enforces their presence, providing strict validation for car-specific operational parameters.

## Database Schema and Migrations

The physical database layer reflects this logical separation through distinct migration files. The [`create_settings.exs`](https://github.com/teslamate-org/teslamate/blob/main/create_settings.exs) migration establishes the global `settings` table, while [`car_settings.exs`](https://github.com/teslamate-org/teslamate/blob/main/car_settings.exs) creates the per-car `car_settings` table. These migrations align exactly with the Ecto schema definitions, ensuring type consistency and constraint enforcement at the database level.

## Configuration Resolution and Fallback Logic

TeslaMate resolves configuration values through a three-step hierarchy that prioritizes specificity over generality.

When the application needs a configuration value, it first loads the singleton global record using `Repo.one(GlobalSettings)`. For vehicle-specific operations, the code fetches the car record and preloads its associated settings via `Repo.preload(:settings)`. The resolution logic then checks the per-car value first; if the field is `nil` or missing, the system falls back to the corresponding global default.

```elixir

# Fetch the global configuration singleton

global = Repo.one(TeslaMate.Settings.GlobalSettings)

# Load a specific car with its settings association

car = Repo.get!(TeslaMate.Log.Car, car_id) |> Repo.preload(:settings)

# Resolve a setting with fallback logic

suspend_min =
  case car.settings.suspend_min do
    nil -> global.suspend_min   # fallback to global default

    val -> val                  # use per-car override

  end

```

Updates to global configuration flow through `GlobalSettings.changeset/2` and persist via `Repo.update/1`, while per-car modifications use `CarSettings.changeset/2`. Both schemas maintain independent validation pipelines, ensuring data integrity within their respective scopes.

## Working with Settings in Practice

### Updating Global Settings

To modify application-wide defaults, fetch the singleton record and apply changes through the changeset pipeline:

```elixir
attrs = %{
  unit_of_length: :km,
  unit_of_temperature: :C,
  unit_of_pressure: :bar,
  preferred_range: :ideal,
  language: "en",
  theme_mode: :system,
  base_url: "https://example.com",
  grafana_url: "https://grafana.example.com"
}

global = Repo.one!(TeslaMate.Settings.GlobalSettings)
changeset = TeslaMate.Settings.GlobalSettings.changeset(global, attrs)
{:ok, _} = Repo.update(changeset)

```

### Updating Per-Car Settings

Vehicle-specific overrides require preloading the settings association before modification:

```elixir
car = Repo.get!(TeslaMate.Log.Car, car_id) |> Repo.preload(:settings)

attrs = %{
  suspend_min: 15,
  suspend_after_idle_min: 10,
  req_not_unlocked: true,
  free_supercharging: false,
  use_streaming_api: true,
  enabled: true,
  lfp_battery: false
}

changeset = TeslaMate.Settings.CarSettings.changeset(car.settings, attrs)
{:ok, _} = Repo.update(changeset)

```

### Reading Settings with Fallback

Encapsulate the resolution logic in a helper function to maintain consistent fallback behavior throughout the application:

```elixir
defp get_setting(car, key) do
  case Map.get(car.settings, key) do
    nil -> Map.get(global(), key)   # global/0 returns the singleton

    value -> value
  end
end

```

## Summary

- **Dual Schema Design**: TeslaMate separates concerns using `GlobalSettings` for application defaults and `CarSettings` for vehicle-specific values.
- **Database Separation**: Global settings reside in the `settings` table (singleton row), while per-car settings live in `car_settings` with a one-to-one relationship to the cars table.
- **Fallback Resolution**: The system checks per-car values first and falls back to global settings when specific values are `nil` or undefined.
- **Validation Independence**: Each schema maintains its own `changeset/2` validation logic, ensuring data integrity without cross-dependencies.
- **Implementation Files**: Core logic exists in [`lib/teslamate/settings/global_settings.ex`](https://github.com/teslamate-org/teslamate/blob/main/lib/teslamate/settings/global_settings.ex) and [`lib/teslamate/settings/car_settings.ex`](https://github.com/teslamate-org/teslamate/blob/main/lib/teslamate/settings/car_settings.ex), supported by corresponding migration files.

## Frequently Asked Questions

### What happens if a per-car setting value is null?

When a field in the `CarSettings` record is `nil`, TeslaMate automatically falls back to the corresponding value in the `GlobalSettings` singleton. This ensures that cars without explicit overrides still inherit sensible application-wide defaults defined by the administrator.

### How does TeslaMate validate settings updates?

Both schemas implement `changeset/2` functions that cast permitted fields and enforce validation rules. The `GlobalSettings` changeset validates URL formats and supported language codes, while `CarSettings` requires all defined fields in `@all_fields` to be present, preventing partial updates that could leave vehicles in undefined states.

### Can multiple cars share the same per-car settings record?

No. The architecture enforces a strict one-to-one relationship between a car and its settings record through the `has_one :car, Car, foreign_key: :settings_id` association. Each vehicle maintains its own isolated row in the `car_settings` table, ensuring that modifications to one vehicle's configuration never affect another.

### Where are the database migrations for these settings located?

The migration files reside in the standard Phoenix migrations directory. The global settings table is created by [`create_settings.exs`](https://github.com/teslamate-org/teslamate/blob/main/create_settings.exs), while the per-car settings table is established by [`car_settings.exs`](https://github.com/teslamate-org/teslamate/blob/main/car_settings.exs). These migrations define the column types and constraints that align with the `GlobalSettings` and `CarSettings` schema definitions.