diff --git a/Services/ServiceTrackerDataStore.cs b/Services/ServiceTrackerDataStore.cs
new file mode 100644
index 0000000..0fb2a95
--- /dev/null
+++ b/Services/ServiceTrackerDataStore.cs
@@ -0,0 +1,135 @@
+using Disco.Plugins.ServiceTracker.Models;
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+namespace Disco.Plugins.ServiceTracker.Services
+{
+ ///
+ /// JSON file-based data store for ServiceTracker metadata.
+ /// Stores ticket extensions and configuration in the plugin data directory.
+ ///
+ public class ServiceTrackerDataStore
+ {
+ private readonly string _dataDirectory;
+ private readonly string _ticketsPath;
+ private readonly string _configPath;
+ private static readonly object _lock = new object();
+
+ public ServiceTrackerDataStore(string pluginDataDirectory)
+ {
+ _dataDirectory = pluginDataDirectory;
+ _ticketsPath = Path.Combine(_dataDirectory, "tickets.json");
+ _configPath = Path.Combine(_dataDirectory, "config.json");
+
+ if (!Directory.Exists(_dataDirectory))
+ Directory.CreateDirectory(_dataDirectory);
+ }
+
+ // --- Configuration ---
+
+ public ServiceTrackerConfig LoadConfig()
+ {
+ lock (_lock)
+ {
+ if (!File.Exists(_configPath))
+ {
+ var defaultConfig = ServiceTrackerConfig.CreateDefault();
+ SaveConfig(defaultConfig);
+ return defaultConfig;
+ }
+ var json = File.ReadAllText(_configPath);
+ return JsonConvert.DeserializeObject(json) ?? ServiceTrackerConfig.CreateDefault();
+ }
+ }
+
+ public void SaveConfig(ServiceTrackerConfig config)
+ {
+ lock (_lock)
+ {
+ var json = JsonConvert.SerializeObject(config, Formatting.Indented);
+ File.WriteAllText(_configPath, json);
+ }
+ }
+
+ // --- Tickets ---
+
+ public List LoadAllTickets()
+ {
+ lock (_lock)
+ {
+ if (!File.Exists(_ticketsPath))
+ return new List();
+
+ var json = File.ReadAllText(_ticketsPath);
+ return JsonConvert.DeserializeObject>(json) ?? new List();
+ }
+ }
+
+ public ServiceTicket GetTicket(int jobId)
+ {
+ return LoadAllTickets().FirstOrDefault(t => t.JobId == jobId);
+ }
+
+ public void SaveTicket(ServiceTicket ticket)
+ {
+ lock (_lock)
+ {
+ var tickets = LoadAllTicketsUnsafe();
+ var existing = tickets.FindIndex(t => t.JobId == ticket.JobId);
+ ticket.LastModifiedDate = DateTime.Now;
+
+ if (existing >= 0)
+ tickets[existing] = ticket;
+ else
+ tickets.Add(ticket);
+
+ SaveAllTicketsUnsafe(tickets);
+ }
+ }
+
+ public void DeleteTicket(int jobId)
+ {
+ lock (_lock)
+ {
+ var tickets = LoadAllTicketsUnsafe();
+ tickets.RemoveAll(t => t.JobId == jobId);
+ SaveAllTicketsUnsafe(tickets);
+ }
+ }
+
+ public void AddNote(int jobId, TicketNote note)
+ {
+ lock (_lock)
+ {
+ var tickets = LoadAllTicketsUnsafe();
+ var ticket = tickets.FirstOrDefault(t => t.JobId == jobId);
+ if (ticket != null)
+ {
+ if (ticket.Notes == null)
+ ticket.Notes = new List();
+ ticket.Notes.Add(note);
+ ticket.LastModifiedDate = DateTime.Now;
+ SaveAllTicketsUnsafe(tickets);
+ }
+ }
+ }
+
+ // Internal methods (caller must hold lock)
+ private List LoadAllTicketsUnsafe()
+ {
+ if (!File.Exists(_ticketsPath))
+ return new List();
+ var json = File.ReadAllText(_ticketsPath);
+ return JsonConvert.DeserializeObject>(json) ?? new List();
+ }
+
+ private void SaveAllTicketsUnsafe(List tickets)
+ {
+ var json = JsonConvert.SerializeObject(tickets, Formatting.Indented);
+ File.WriteAllText(_ticketsPath, json);
+ }
+ }
+}