diff --git a/src/ScadaLink.SiteRuntime/Repositories/SiteExternalSystemRepository.cs b/src/ScadaLink.SiteRuntime/Repositories/SiteExternalSystemRepository.cs
new file mode 100644
index 0000000..95621f2
--- /dev/null
+++ b/src/ScadaLink.SiteRuntime/Repositories/SiteExternalSystemRepository.cs
@@ -0,0 +1,254 @@
+using System.Text.Json;
+using Microsoft.Data.Sqlite;
+using ScadaLink.Commons.Entities.ExternalSystems;
+using ScadaLink.Commons.Interfaces.Repositories;
+using ScadaLink.SiteRuntime.Persistence;
+
+namespace ScadaLink.SiteRuntime.Repositories;
+
+///
+/// Site-side read-only implementation of
+/// backed by the local SQLite database via .
+/// Write operations throw because site-local
+/// artifacts are managed exclusively through deployment from Central.
+///
+public class SiteExternalSystemRepository : IExternalSystemRepository
+{
+ private readonly SiteStorageService _storage;
+
+ public SiteExternalSystemRepository(SiteStorageService storage)
+ {
+ _storage = storage ?? throw new ArgumentNullException(nameof(storage));
+ }
+
+ // ── ExternalSystemDefinition (read) ──
+
+ public async Task> GetAllExternalSystemsAsync(
+ CancellationToken cancellationToken = default)
+ {
+ await using var connection = CreateConnection();
+ await connection.OpenAsync(cancellationToken);
+
+ await using var command = connection.CreateCommand();
+ command.CommandText = @"
+ SELECT name, endpoint_url, auth_type, auth_configuration
+ FROM external_systems";
+
+ var results = new List();
+ await using var reader = await command.ExecuteReaderAsync(cancellationToken);
+ while (await reader.ReadAsync(cancellationToken))
+ {
+ results.Add(MapExternalSystem(reader));
+ }
+
+ return results;
+ }
+
+ public async Task GetExternalSystemByIdAsync(
+ int id, CancellationToken cancellationToken = default)
+ {
+ // The SQLite table is keyed by name, not by integer ID.
+ // Scan all rows and match on the synthetic ID derived from the name.
+ var all = await GetAllExternalSystemsAsync(cancellationToken);
+ return all.FirstOrDefault(e => e.Id == id);
+ }
+
+ // ── ExternalSystemMethod (read) ──
+
+ public async Task> GetMethodsByExternalSystemIdAsync(
+ int externalSystemId, CancellationToken cancellationToken = default)
+ {
+ // Find the parent system to get its name, then parse its method_definitions JSON.
+ var system = await GetExternalSystemByIdAsync(externalSystemId, cancellationToken);
+ if (system is null)
+ return Array.Empty();
+
+ await using var connection = CreateConnection();
+ await connection.OpenAsync(cancellationToken);
+
+ await using var command = connection.CreateCommand();
+ command.CommandText = @"
+ SELECT method_definitions
+ FROM external_systems
+ WHERE name = @name";
+ command.Parameters.AddWithValue("@name", system.Name);
+
+ var json = (string?)await command.ExecuteScalarAsync(cancellationToken);
+ if (string.IsNullOrWhiteSpace(json))
+ return Array.Empty();
+
+ return ParseMethodDefinitions(json, externalSystemId);
+ }
+
+ public async Task GetExternalSystemMethodByIdAsync(
+ int id, CancellationToken cancellationToken = default)
+ {
+ // Scan all systems and their methods to find the matching synthetic ID.
+ var systems = await GetAllExternalSystemsAsync(cancellationToken);
+ foreach (var system in systems)
+ {
+ var methods = await GetMethodsByExternalSystemIdAsync(system.Id, cancellationToken);
+ var match = methods.FirstOrDefault(m => m.Id == id);
+ if (match is not null)
+ return match;
+ }
+
+ return null;
+ }
+
+ // ── DatabaseConnectionDefinition (read) ──
+
+ public async Task> GetAllDatabaseConnectionsAsync(
+ CancellationToken cancellationToken = default)
+ {
+ await using var connection = CreateConnection();
+ await connection.OpenAsync(cancellationToken);
+
+ await using var command = connection.CreateCommand();
+ command.CommandText = @"
+ SELECT name, connection_string, max_retries, retry_delay_ms
+ FROM database_connections";
+
+ var results = new List();
+ await using var reader = await command.ExecuteReaderAsync(cancellationToken);
+ while (await reader.ReadAsync(cancellationToken))
+ {
+ var def = new DatabaseConnectionDefinition(
+ name: reader.GetString(0),
+ connectionString: reader.GetString(1))
+ {
+ Id = GenerateSyntheticId(reader.GetString(0)),
+ MaxRetries = reader.GetInt32(2),
+ RetryDelay = TimeSpan.FromMilliseconds(reader.GetInt64(3))
+ };
+ results.Add(def);
+ }
+
+ return results;
+ }
+
+ public async Task GetDatabaseConnectionByIdAsync(
+ int id, CancellationToken cancellationToken = default)
+ {
+ var all = await GetAllDatabaseConnectionsAsync(cancellationToken);
+ return all.FirstOrDefault(d => d.Id == id);
+ }
+
+ // ── Write operations (not supported on site) ──
+
+ public Task AddExternalSystemAsync(ExternalSystemDefinition definition, CancellationToken cancellationToken = default)
+ => throw new NotSupportedException("Managed via artifact deployment from Central");
+
+ public Task UpdateExternalSystemAsync(ExternalSystemDefinition definition, CancellationToken cancellationToken = default)
+ => throw new NotSupportedException("Managed via artifact deployment from Central");
+
+ public Task DeleteExternalSystemAsync(int id, CancellationToken cancellationToken = default)
+ => throw new NotSupportedException("Managed via artifact deployment from Central");
+
+ public Task AddExternalSystemMethodAsync(ExternalSystemMethod method, CancellationToken cancellationToken = default)
+ => throw new NotSupportedException("Managed via artifact deployment from Central");
+
+ public Task UpdateExternalSystemMethodAsync(ExternalSystemMethod method, CancellationToken cancellationToken = default)
+ => throw new NotSupportedException("Managed via artifact deployment from Central");
+
+ public Task DeleteExternalSystemMethodAsync(int id, CancellationToken cancellationToken = default)
+ => throw new NotSupportedException("Managed via artifact deployment from Central");
+
+ public Task AddDatabaseConnectionAsync(DatabaseConnectionDefinition definition, CancellationToken cancellationToken = default)
+ => throw new NotSupportedException("Managed via artifact deployment from Central");
+
+ public Task UpdateDatabaseConnectionAsync(DatabaseConnectionDefinition definition, CancellationToken cancellationToken = default)
+ => throw new NotSupportedException("Managed via artifact deployment from Central");
+
+ public Task DeleteDatabaseConnectionAsync(int id, CancellationToken cancellationToken = default)
+ => throw new NotSupportedException("Managed via artifact deployment from Central");
+
+ public Task SaveChangesAsync(CancellationToken cancellationToken = default)
+ => throw new NotSupportedException("Managed via artifact deployment from Central");
+
+ // ── Private helpers ──
+
+ ///
+ /// Creates a new SQLite connection using the same connection string as .
+ /// We access the connection string via reflection-free approach: the storage service
+ /// exposes it through a known field. Since it doesn't, we derive it from the injected service
+ /// by using a shared connection string provider pattern. For now, we accept the connection
+ /// string via a secondary constructor path or expose it from storage.
+ ///
+ /// Implementation note: We use the SiteStorageService's internal connection string.
+ /// This field is accessed via a package-internal helper since SiteStorageService
+ /// doesn't expose it directly. As a pragmatic solution, we pass the connection string
+ /// separately at DI registration time.
+ ///
+ private SqliteConnection CreateConnection()
+ {
+ // Access the connection string from SiteStorageService via its internal field.
+ // This uses reflection as a pragmatic choice — the alternative is modifying
+ // SiteStorageService to expose the connection string, which is out of scope.
+ var field = typeof(SiteStorageService).GetField("_connectionString",
+ System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
+ var connectionString = (string)field!.GetValue(_storage)!;
+ return new SqliteConnection(connectionString);
+ }
+
+ private static ExternalSystemDefinition MapExternalSystem(SqliteDataReader reader)
+ {
+ var name = reader.GetString(0);
+ return new ExternalSystemDefinition(
+ name: name,
+ endpointUrl: reader.GetString(1),
+ authType: reader.GetString(2))
+ {
+ Id = GenerateSyntheticId(name),
+ AuthConfiguration = reader.IsDBNull(3) ? null : reader.GetString(3)
+ };
+ }
+
+ private static IReadOnlyList ParseMethodDefinitions(
+ string json, int externalSystemId)
+ {
+ try
+ {
+ var methods = JsonSerializer.Deserialize>(json,
+ new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
+
+ if (methods is null)
+ return Array.Empty();
+
+ return methods.Select(m => new ExternalSystemMethod(
+ name: m.Name ?? string.Empty,
+ httpMethod: m.HttpMethod ?? "GET",
+ path: m.Path ?? string.Empty)
+ {
+ Id = GenerateSyntheticId($"{externalSystemId}:{m.Name}"),
+ ExternalSystemDefinitionId = externalSystemId,
+ ParameterDefinitions = m.ParameterDefinitions,
+ ReturnDefinition = m.ReturnDefinition
+ }).ToList();
+ }
+ catch (JsonException)
+ {
+ return Array.Empty();
+ }
+ }
+
+ ///
+ /// Generates a stable positive integer ID from a string name.
+ /// Uses a hash to produce a deterministic synthetic ID since the SQLite
+ /// tables are keyed by name rather than auto-increment integer.
+ ///
+ private static int GenerateSyntheticId(string name)
+ => name.GetHashCode() & 0x7FFFFFFF;
+
+ ///
+ /// DTO for deserializing individual method entries from the method_definitions JSON column.
+ ///
+ private sealed class MethodDefinitionDto
+ {
+ public string? Name { get; set; }
+ public string? HttpMethod { get; set; }
+ public string? Path { get; set; }
+ public string? ParameterDefinitions { get; set; }
+ public string? ReturnDefinition { get; set; }
+ }
+}