diff --git a/src/TeamsISO.Engine/Persistence/ConfigStore.cs b/src/TeamsISO.Engine/Persistence/ConfigStore.cs
new file mode 100644
index 0000000..13fef05
--- /dev/null
+++ b/src/TeamsISO.Engine/Persistence/ConfigStore.cs
@@ -0,0 +1,59 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.Extensions.Logging;
+using TeamsISO.Engine.Domain;
+
+namespace TeamsISO.Engine.Persistence;
+
+///
+/// Loads and saves as JSON.
+/// Writes are atomic: serialize to a temp file in the same directory, then File.Move with overwrite.
+///
+public sealed class ConfigStore
+{
+ private static readonly JsonSerializerOptions Options = new()
+ {
+ WriteIndented = true,
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ Converters = { new JsonStringEnumConverter() }
+ };
+
+ private readonly string _path;
+ private readonly ILogger _logger;
+
+ public ConfigStore(string path, ILogger logger)
+ {
+ _path = path;
+ _logger = logger;
+ }
+
+ public EngineConfig Load()
+ {
+ if (!File.Exists(_path))
+ {
+ _logger.LogInformation("Config file not found at {Path}; using defaults.", _path);
+ return EngineConfig.Default;
+ }
+
+ try
+ {
+ var json = File.ReadAllText(_path);
+ var loaded = JsonSerializer.Deserialize(json, Options);
+ return loaded ?? EngineConfig.Default;
+ }
+ catch (JsonException ex)
+ {
+ _logger.LogWarning(ex, "Config file at {Path} is not valid JSON; using defaults.", _path);
+ return EngineConfig.Default;
+ }
+ }
+
+ public void Save(EngineConfig config)
+ {
+ Directory.CreateDirectory(Path.GetDirectoryName(_path)!);
+ var temp = _path + ".tmp";
+ var json = JsonSerializer.Serialize(config, Options);
+ File.WriteAllText(temp, json);
+ File.Move(temp, _path, overwrite: true);
+ }
+}
diff --git a/src/tests/TeamsISO.Engine.Tests/Persistence/ConfigStoreTests.cs b/src/tests/TeamsISO.Engine.Tests/Persistence/ConfigStoreTests.cs
new file mode 100644
index 0000000..fa156e2
--- /dev/null
+++ b/src/tests/TeamsISO.Engine.Tests/Persistence/ConfigStoreTests.cs
@@ -0,0 +1,77 @@
+using Microsoft.Extensions.Logging.Abstractions;
+using TeamsISO.Engine.Domain;
+using TeamsISO.Engine.Persistence;
+
+namespace TeamsISO.Engine.Tests.Persistence;
+
+public class ConfigStoreTests : IDisposable
+{
+ private readonly string _dir;
+
+ public ConfigStoreTests()
+ {
+ _dir = Path.Combine(Path.GetTempPath(), $"teamsiso-tests-{Guid.NewGuid():N}");
+ Directory.CreateDirectory(_dir);
+ }
+
+ public void Dispose() => Directory.Delete(_dir, recursive: true);
+
+ private ConfigStore NewStore() =>
+ new(Path.Combine(_dir, "config.json"), NullLogger.Instance);
+
+ [Fact]
+ public void Load_WhenFileMissing_ReturnsDefault()
+ {
+ var store = NewStore();
+ var config = store.Load();
+ config.Should().Be(EngineConfig.Default);
+ }
+
+ [Fact]
+ public void SaveThenLoad_RoundTripsExactly()
+ {
+ var store = NewStore();
+ var input = new EngineConfig(
+ new FrameProcessingSettings(TargetFramerate.Fps59_94, TargetResolution.R720p,
+ AspectMode.Letterbox, AudioMode.Isolated),
+ new[]
+ {
+ new IsoAssignment(Guid.NewGuid(), IsEnabled: true, CustomOutputName: "TEAMSISO_A"),
+ new IsoAssignment(Guid.NewGuid(), IsEnabled: false, CustomOutputName: null),
+ });
+
+ store.Save(input);
+ var loaded = store.Load();
+
+ loaded.Should().BeEquivalentTo(input);
+ }
+
+ [Fact]
+ public void Load_WhenFileCorrupt_ReturnsDefault()
+ {
+ var path = Path.Combine(_dir, "config.json");
+ File.WriteAllText(path, "not valid json {{{");
+
+ var store = NewStore();
+ var config = store.Load();
+ config.Should().Be(EngineConfig.Default);
+ }
+
+ [Fact]
+ public void Save_WritesAtomically_NoPartialFileVisible()
+ {
+ var store = NewStore();
+ var path = Path.Combine(_dir, "config.json");
+
+ store.Save(EngineConfig.Default);
+ var firstWrite = File.ReadAllText(path);
+
+ store.Save(new EngineConfig(
+ FrameProcessingSettings.Default with { Framerate = TargetFramerate.Fps60 },
+ Array.Empty()));
+
+ File.Exists(path).Should().BeTrue();
+ File.ReadAllText(path).Should().NotBe(firstWrite);
+ Directory.GetFiles(_dir).Should().HaveCount(1);
+ }
+}