# How CodexBar's Settings Persistence System Works with Configuration Files

> Discover how CodexBar's settings persistence uses UserDefaults and JSON files. Learn about its dual-layer architecture for storing preferences and configurations.

- Repository: [Peter Steinberger/CodexBar](https://github.com/steipete/CodexBar)
- Tags: internals
- Published: 2026-07-08

---

**CodexBar persists user preferences through a dual-layer architecture that stores simple scalar values in UserDefaults while maintaining richer provider configurations in a JSON file within the app-group container.**

The open-source macOS menu bar application [steipete/CodexBar](https://github.com/steipete/CodexBar) implements a robust persistence layer centered around the `SettingsStore` class. This system ensures that user preferences survive app restarts, handles legacy setting migrations automatically, and maintains thread-safe access to configuration data across the main app and its extensions.

## The Dual-Storage Architecture

CodexBar divides persistent data into two distinct storage mechanisms based on complexity and usage patterns. This separation allows frequently accessed simple flags to remain in memory-mapped preferences while complex provider definitions reside in version-controlled JSON.

### UserDefaults for Scalar Preferences

Simple configuration values—including refresh intervals, launch-at-login flags, and UI metric choices—are stored in **UserDefaults**. According to the source code in [`SettingsStore.swift`](https://github.com/steipete/CodexBar/blob/main/SettingsStore.swift), the initializer reads every key using `userDefaults.object(forKey:)` or `string(forKey:)` and populates a `SettingsDefaultsState` struct. The complete loading routine lives in `SettingsStore.loadDefaultsState(userDefaults:)` between lines 73 and 124.

The system supports both standard defaults and an optional shared app-group container accessed via `AppGroupSupport.sharedDefaults()`. This allows the main application and any extensions to share preference data seamlessly.

### JSON Configuration for Provider Definitions

Richer configuration data—specifically per-provider definitions including endpoint URLs, token overrides, and cookie sources—is managed by **CodexBarConfigStore**. This class maintains a JSON file located at `~/Library/Group Containers/<team-id>/Library/Application Support/CodexBar/config.json` within the app-group container.

During initialization, `SettingsStore` creates a `CodexBarConfigStore` instance and calls `load()` to retrieve a `CodexBarConfig` object. When provider settings change, the store schedules an asynchronous `configPersistTask` that writes updates back to disk via `configStore.save(_:)`, ensuring disk I/O occurs off the main thread.

## Initialization and Migration Flow

The startup sequence in [`SettingsStore.swift`](https://github.com/steipete/CodexBar/blob/main/SettingsStore.swift) handles complex migration scenarios to ensure seamless updates between app versions.

### App-Group Migration

If the app is running outside of test mode, `SettingsStore` fires `scheduleAppGroupMigration` asynchronously (lines 65-70). This routine copies legacy data from the standard UserDefaults database into the shared app-group defaults, ensuring users retain their settings when upgrading to versions that support app extensions.

### Legacy Defaults Bridging

The method `shouldBridgeSharedDefaults(for:)` (lines 15-20) determines whether a given UserDefaults instance should fall back to shared defaults. This bridging allows the same UI code to read and write values even when unit tests replace the defaults with a sandboxed instance.

### Configuration Loading and Key Migration

After migration, the system performs several housekeeping tasks:

1. **Legacy key migration** – Older keys like `openAIWebAccess` are automatically migrated to newer equivalents such as `openAIWebAccessEnabled` immediately after defaults load (around line 78).
2. **Configuration loading** – `CodexBarConfigMigrator.loadOrMigrate(...)` reads the JSON file or creates a fresh default config, returning a `CodexBarConfig` that becomes `self.config` (lines 100-105).
3. **State population** – The `SettingsDefaultsState` struct is filled with values from UserDefaults, including derived flags like `mergedMenuLastSelectedWasOverviewStorage`.

## Runtime Persistence Mechanisms

CodexBar employs different persistence strategies depending on which storage layer is being modified.

### Synchronous UserDefaults Updates

When UI elements toggle simple preferences, updates are written immediately to UserDefaults. For example, setting `defaultsState.launchAtLogin` triggers a direct call to `userDefaults.set(_:forKey:)` during initialization (line 19). This synchronous approach ensures that preferences are instantly available to system services like launch-at-login helpers.

### Asynchronous Config File Writes

Provider-specific changes require more care to prevent disk thrashing. The method `setProviderEnabled(_:enabled:)` mutates `self.config` and then calls `persistConfig()`, which schedules a `Task` to invoke `configStore.save(self.config)`. This indirection guarantees that multiple rapid updates coalesce into a single disk write and that filesystem operations never block the UI thread.

You can optionally await the persistence task if immediate durability is required:

```swift
// Toggle a provider (queues async write)
settings.setProviderEnabled(provider: .zai,
                            metadata: ProviderDescriptorRegistry.metadata[.zai]!,
                            enabled: false)

// Wait for the write to complete
await settings.configPersistTask

```

## Test Mode Special Handling

When running unit tests, `SettingsStore` detects `isRunningTests` and modifies its behavior to ensure test isolation:

- **Temporary defaults** – Forces creation of a temporary `UserDefaults` suite instead of the real user database.
- **Auto-seeding** – Populates missing default keys automatically so tests assume a fully seeded environment.
- **Keychain gating** – Disables keychain access unless explicitly enabled via `KeychainAccessGate.isDisabled`.

This logic is embedded directly within the `SettingsStore` class, making the persistence layer a single source of truth for both production and test environments.

## Summary

- **Dual-layer storage** separates simple UserDefaults scalars from complex JSON provider configurations.
- **Automatic migration** handles legacy key updates and app-group container transitions during startup.
- **Asynchronous persistence** prevents UI blocking when writing provider configuration changes to disk.
- **Test isolation** ensures unit tests run against temporary storage without affecting user data.
- **Centralized coordination** through [`SettingsStore.swift`](https://github.com/steipete/CodexBar/blob/main/SettingsStore.swift) provides a single API for all persistence operations.

## Frequently Asked Questions

### Where does CodexBar store its configuration files?

CodexBar stores simple preferences in `UserDefaults.standard` (or a shared app-group instance) and complex provider configurations in a JSON file located at `~/Library/Group Containers/<team-id>/Library/Application Support/CodexBar/config.json`. This path resides within the app-group container, allowing data sharing between the main app and extensions.

### How does CodexBar handle settings from older app versions?

The `SettingsStore` class automatically migrates legacy settings during initialization. It schedules an `AppGroupMigration` to move data from standard defaults to shared defaults, and it migrates renamed keys (such as `openAIWebAccess` to `openAIWebAccessEnabled`) immediately after loading. These migrations run asynchronously to prevent startup delays.

### What happens when I toggle a provider in the CodexBar UI?

When you toggle a provider, the `setProviderEnabled(_:enabled:)` method immediately updates the in-memory `CodexBarConfig` object and schedules an asynchronous `configPersistTask`. This task writes the updated configuration to the JSON file via `configStore.save(_:)`, ensuring the disk write happens off the main thread while the UI remains responsive.

### Can I force CodexBar to save settings immediately?

For UserDefaults-backed settings, changes are written synchronously and are immediate. For provider configuration changes, you can await the `configPersistTask` property on `SettingsStore` after making changes. This optional await ensures the JSON file write completes before your code continues execution.