# How the Configuration System Is Stored and Managed in Music Assistant

> Learn how Music Assistant stores and manages its configuration system in a single JSON file. Discover encrypted, hierarchical, and batched access to settings.

- Repository: [Music Assistant/server](https://github.com/music-assistant/server)
- Tags: internals
- Published: 2026-06-16

---

**Music Assistant stores all persistent settings in a single JSON file ([`settings.json`](https://github.com/music-assistant/server/blob/main/settings.json)) managed by the `ConfigController` class in [`music_assistant/controllers/config.py`](https://github.com/music-assistant/server/blob/main/music_assistant/controllers/config.py), which provides encrypted, hierarchical, and batched access to configuration values.**

The `music-assistant/server` repository implements a centralized configuration architecture that balances simplicity with security. All settings persist to a single JSON file in the user's data directory, while the `ConfigController` class provides an async-compatible interface for reading, writing, and encrypting sensitive values. This design ensures that providers, players, and core controllers can store typed configuration data without direct filesystem access.

## Configuration Storage Architecture

### The settings.json File Structure

Music Assistant persists all configuration data to `$HOME/.musicassistant/settings.json`. This single file stores a nested dictionary representing the entire server state, including core settings, provider instances, player configurations, and DSP presets. The storage format uses hierarchical keys (e.g., `providers/<instance_id>/values/<key>`) that map to nested JSON objects, allowing the system to group related settings by domain while maintaining a flat file structure.

### Hierarchical Key Organization

The configuration system organizes data through slash-delimited paths that mirror the dictionary structure. For example, accessing `players/player123/enabled` traverses the nested objects to retrieve the specific value. This hierarchy enables clean separation between different subsystems:

- `core/` - Global server settings
- `providers/<instance_id>/` - Music provider configurations
- `players/<player_id>/` - Player-specific settings
- `dsp/` - DSP preset definitions

## The ConfigController Implementation

### Core Access Methods

Located in [`music_assistant/controllers/config.py`](https://github.com/music-assistant/server/blob/main/music_assistant/controllers/config.py), the `ConfigController` class exposes four primary methods for configuration manipulation:

- **`get(path, default=None)`** - Retrieves values using dot or slash notation
- **`set(path, value)`** - Writes values and triggers delayed persistence
- **`remove(path)`** - Deletes configuration entries and cleans up empty parent objects
- **`set_default(path, value)`** - Sets values only if the key does not exist

These methods automatically handle intermediate object creation, ensuring that setting `providers/spotify/enabled` creates the necessary nested dictionaries without manual initialization.

### Encryption and Security

The system implements transparent encryption for sensitive configuration values. During first startup, Music Assistant generates a server-wide UUID stored as `CONF_SERVER_ID`. The first 32 bytes of this UUID derive a Fernet symmetric encryption key. Any configuration entry marked with an "encrypt" suffix in the `ConfigEntry` model (defined in [`music_assistant_models/config_entries.py`](https://github.com/music-assistant/server/blob/main/music_assistant_models/config_entries.py)) automatically encrypts before storage and decrypts upon retrieval. This mechanism protects API keys and passwords while keeping the configuration file portable.

### Change Coalescing and Persistence

To minimize disk I/O, the configuration system implements write coalescing with a `DEFAULT_SAVE_DELAY` of 5 seconds. When `set()` is called, an `asyncio.TimerHandle` schedules the actual write operation. Subsequent changes within the window reset the timer, batching multiple updates into a single file operation. The `save(immediate=True)` method bypasses this delay for critical operations like onboarding completion, forcing an immediate write to [`settings.json`](https://github.com/music-assistant/server/blob/main/settings.json).

## Typed Configuration Entries

### ConfigEntry Model

Each configurable item throughout Music Assistant implements the `ConfigEntry` model from [`music_assistant_models/config_entries.py`](https://github.com/music-assistant/server/blob/main/music_assistant_models/config_entries.py). These entries define:

- **Data type** - String, integer, boolean, or secure string
- **Default values** - Fallback when user preferences are unset
- **Validation rules** - Constraints checked before persistence
- **UI hints** - Labels and descriptions for the frontend

The model also includes flags indicating whether values require encryption or represent sensitive credentials.

### Provider and Player Configuration

Providers and players contribute their own configuration schemas without modifying core code. For example, [`music_assistant/providers/spotify/config.py`](https://github.com/music-assistant/server/blob/main/music_assistant/providers/spotify/config.py) defines provider-specific entries for client IDs and secrets, while player implementations expose settings like volume normalization or output formats. The `ConfigController` aggregates these definitions on demand through `get_provider_config_entries()`, `get_player_config_entries()`, and `get_core_config_entries()`, building complete configuration UIs dynamically.

## API Exposure and Management

The `ConfigController` exposes its functionality through the JSON-RPC API using the `@api_command` decorator. Frontend clients interact with endpoints like `config/providers` and `config/players/get` to read and modify settings remotely. When removing providers or players, the controller automatically cleans up orphaned configuration sections, preventing stale data accumulation in [`settings.json`](https://github.com/music-assistant/server/blob/main/settings.json).

## Working with the Configuration System

The following examples demonstrate how to interact with the configuration system through the `MusicAssistant` instance:

```python

# Retrieve a value using hierarchical path notation

from music_assistant import MusicAssistant

mass: MusicAssistant = ...   # already running instance

default_volume = mass.config.get("core/volume/default")
print("Default volume:", default_volume)

```

```python

# Set a value and force immediate persistence

mass.config.set("core/volume/default", 45)
mass.config.save(immediate=True)  # Bypasses the 5-second delay

```

```python

# Retrieve a complete provider configuration

provider_cfg = await mass.config.get_provider_config("spotify--a1b2c3")
print(provider_cfg)

```

```python

# Update DSP configuration with automatic encryption handling

from music_assistant.helpers.dsp import DSPConfig

dsp = await mass.config.get_player_dsp_config("player123")
dsp.enabled = True
dsp.preset = "bass_boost"
await mass.config.save_dsp_config("player123", dsp)

```

```python

# Remove a provider and automatically clean up associated config

await mass.config.remove_provider_config("myprovider--xyz")

```

## Summary

- **Single-file storage**: All configuration persists to `$HOME/.musicassistant/settings.json` as a nested JSON dictionary.
- **Hierarchical access**: The `ConfigController` uses dot/slash notation (e.g., `providers/spotify/enabled`) to navigate nested configuration objects.
- **Transparent encryption**: Sensitive values automatically encrypt using a Fernet key derived from `CONF_SERVER_ID`, protecting credentials while stored on disk.
- **Write coalescing**: Changes batch with a 5-second delay by default, reducing disk I/O through `asyncio.TimerHandle` scheduling.
- **Typed schemas**: `ConfigEntry` models define validation, defaults, and UI metadata for all configurable parameters across providers and players.
- **Automatic cleanup**: Removing providers or players triggers deletion of orphaned configuration sections, maintaining file hygiene.

## Frequently Asked Questions

### Where does Music Assistant store its configuration file?

Music Assistant stores all persistent configuration in a file named [`settings.json`](https://github.com/music-assistant/server/blob/main/settings.json) located in the user's data directory at `$HOME/.musicassistant`. This single JSON file contains the entire server state, including provider credentials, player settings, and DSP presets.

### How does Music Assistant protect sensitive configuration data like API keys?

The configuration system automatically encrypts values marked as sensitive in their `ConfigEntry` definition. During first startup, the server generates a UUID (`CONF_SERVER_ID`) and derives a Fernet symmetric key from the first 32 bytes of this identifier. The `ConfigController` transparently encrypts data before writing to [`settings.json`](https://github.com/music-assistant/server/blob/main/settings.json) and decrypts it upon retrieval, ensuring passwords and API keys remain secure at rest.

### What happens if multiple configuration changes occur in quick succession?

The implementation uses write coalescing to optimize disk I/O. When `set()` is called, the controller schedules a save operation using `asyncio.TimerHandle` with a `DEFAULT_SAVE_DELAY` of 5 seconds. Subsequent changes within this window reset the timer, batching updates into a single write operation. You can force immediate persistence by calling `save(immediate=True)` when necessary.

### Can providers add their own configuration options without modifying core code?

Yes, the architecture supports extensible configuration through the `ConfigEntry` model. Providers define their configuration schemas in module-specific files (e.g., [`music_assistant/providers/spotify/config.py`](https://github.com/music-assistant/server/blob/main/music_assistant/providers/spotify/config.py)) and return these entries through standardized methods. The `ConfigController` aggregates these definitions dynamically via `get_provider_config_entries()`, allowing the frontend to render configuration forms without core code changes.