diff --git a/src/Backend/AutopilotMonitor.Functions/DataAccess/DataAccessServiceExtensions.cs b/src/Backend/AutopilotMonitor.Functions/DataAccess/DataAccessServiceExtensions.cs
new file mode 100644
index 00000000..71b3877d
--- /dev/null
+++ b/src/Backend/AutopilotMonitor.Functions/DataAccess/DataAccessServiceExtensions.cs
@@ -0,0 +1,58 @@
+using AutopilotMonitor.Shared.DataAccess;
+using AutopilotMonitor.Functions.DataAccess.TableStorage;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace AutopilotMonitor.Functions.DataAccess
+{
+ ///
+ /// Extension methods for registering Data Access Layer services.
+ /// Currently registers Azure Table Storage implementations.
+ /// To switch to Cosmos DB, replace the implementation registrations here.
+ ///
+ public static class DataAccessServiceExtensions
+ {
+ ///
+ /// Registers all Data Access Layer services with Table Storage implementations.
+ ///
+ public static IServiceCollection AddTableStorageDataAccess(this IServiceCollection services)
+ {
+ // Event publisher (no-op by default; replace with Event Hub/Service Bus implementation)
+ services.AddSingleton();
+
+ // Storage initializer
+ services.AddSingleton();
+
+ // Repository implementations (delegate to existing TableStorageService)
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+
+ // Note: IConfigRepository, IAdminRepository, IDeviceSecurityRepository,
+ // IBootstrapRepository, and INotificationRepository implementations will be
+ // added as existing services are migrated to use the DAL interfaces.
+ // Currently those services (TenantConfigurationService, GlobalAdminService, etc.)
+ // manage their own Table Storage connections directly.
+
+ return services;
+ }
+
+ ///
+ /// Replaces the default NullDataEventPublisher with a custom implementation.
+ /// Call this AFTER AddTableStorageDataAccess() to override.
+ /// Example: services.AddEventStreaming<EventHubPublisher>();
+ ///
+ public static IServiceCollection AddEventStreaming(this IServiceCollection services)
+ where TPublisher : class, IDataEventPublisher
+ {
+ // Remove existing registration and replace
+ var descriptor = services.FirstOrDefault(d => d.ServiceType == typeof(IDataEventPublisher));
+ if (descriptor != null)
+ services.Remove(descriptor);
+
+ services.AddSingleton();
+ return services;
+ }
+ }
+}
diff --git a/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableMaintenanceRepository.cs b/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableMaintenanceRepository.cs
new file mode 100644
index 00000000..d4492572
--- /dev/null
+++ b/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableMaintenanceRepository.cs
@@ -0,0 +1,63 @@
+using AutopilotMonitor.Shared.DataAccess;
+using AutopilotMonitor.Shared.Models;
+using AutopilotMonitor.Functions.Services;
+
+namespace AutopilotMonitor.Functions.DataAccess.TableStorage
+{
+ ///
+ /// Table Storage implementation of IMaintenanceRepository.
+ /// Delegates to existing TableStorageService for backwards compatibility.
+ ///
+ public class TableMaintenanceRepository : IMaintenanceRepository
+ {
+ private readonly TableStorageService _storage;
+
+ public TableMaintenanceRepository(TableStorageService storage)
+ {
+ _storage = storage;
+ }
+
+ public Task LogAuditEntryAsync(string tenantId, string action, string entityType,
+ string entityId, string performedBy, Dictionary? details = null)
+ => _storage.LogAuditEntryAsync(tenantId, action, entityType, entityId, performedBy, details);
+
+ public Task> GetAuditLogsAsync(string tenantId, int maxResults = 100)
+ => _storage.GetAuditLogsAsync(tenantId, maxResults);
+
+ public Task> GetAllAuditLogsAsync(int maxResults = 100)
+ => _storage.GetAllAuditLogsAsync(maxResults);
+
+ public Task> GetSessionsOlderThanAsync(string tenantId, DateTime cutoffDate)
+ => _storage.GetSessionsOlderThanAsync(tenantId, cutoffDate);
+
+ public Task> GetSessionsByDateRangeAsync(DateTime startDate, DateTime endDate, string? tenantId = null)
+ => _storage.GetSessionsByDateRangeAsync(startDate, endDate, tenantId);
+
+ public Task> GetStalledSessionsAsync(string tenantId, DateTime cutoffTime)
+ => _storage.GetStalledSessionsAsync(tenantId, cutoffTime);
+
+ public Task> GetExcessiveDataSendersAsync(string tenantId, DateTime windowCutoff, int maxSessionWindowHours)
+ => _storage.GetExcessiveDataSendersAsync(tenantId, windowCutoff, maxSessionWindowHours);
+
+ public Task> GetAllTenantIdsAsync()
+ => _storage.GetAllTenantIdsAsync();
+
+ public Task DeleteSessionEventsAsync(string tenantId, string sessionId)
+ => _storage.DeleteSessionEventsAsync(tenantId, sessionId);
+
+ public Task DeleteSessionRuleResultsAsync(string tenantId, string sessionId)
+ => _storage.DeleteSessionRuleResultsAsync(tenantId, sessionId);
+
+ public Task DeleteSessionAppInstallSummariesAsync(string tenantId, string sessionId)
+ => _storage.DeleteSessionAppInstallSummariesAsync(tenantId, sessionId);
+
+ public Task BackfillSessionIndexAsync()
+ => _storage.BackfillSessionIndexAsync();
+
+ public Task CleanupGhostSessionIndexEntriesAsync()
+ => _storage.CleanupGhostSessionIndexEntriesAsync();
+
+ public Task IsSessionIndexEmptyAsync()
+ => _storage.IsSessionIndexEmptyAsync();
+ }
+}
diff --git a/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableMetricsRepository.cs b/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableMetricsRepository.cs
new file mode 100644
index 00000000..63d2943f
--- /dev/null
+++ b/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableMetricsRepository.cs
@@ -0,0 +1,65 @@
+using AutopilotMonitor.Shared.DataAccess;
+using AutopilotMonitor.Shared.Models;
+using AutopilotMonitor.Functions.Services;
+
+namespace AutopilotMonitor.Functions.DataAccess.TableStorage
+{
+ ///
+ /// Table Storage implementation of IMetricsRepository.
+ /// Delegates to existing TableStorageService for backwards compatibility.
+ ///
+ public class TableMetricsRepository : IMetricsRepository
+ {
+ private readonly TableStorageService _storage;
+ private readonly IDataEventPublisher _publisher;
+
+ public TableMetricsRepository(TableStorageService storage, IDataEventPublisher publisher)
+ {
+ _storage = storage;
+ _publisher = publisher;
+ }
+
+ public Task SaveUsageMetricsSnapshotAsync(UsageMetricsSnapshot metrics)
+ => _storage.SaveUsageMetricsSnapshotAsync(metrics);
+
+ public Task> GetUsageMetricsSnapshotAsync(
+ string? tenantId = null, string? startDate = null, string? endDate = null, int maxResults = 100)
+ => _storage.GetUsageMetricsSnapshotAsync(tenantId, startDate, endDate, maxResults);
+
+ public Task HasUsageMetricsSnapshotAsync(string date)
+ => _storage.HasUsageMetricsSnapshotAsync(date);
+
+ public Task StoreAppInstallSummaryAsync(AppInstallSummary summary)
+ => _storage.StoreAppInstallSummaryAsync(summary);
+
+ public Task> GetAppInstallSummariesByTenantAsync(string tenantId)
+ => _storage.GetAppInstallSummariesByTenantAsync(tenantId);
+
+ public Task> GetAllAppInstallSummariesAsync()
+ => _storage.GetAllAppInstallSummariesAsync();
+
+ public Task GetPlatformStatsAsync()
+ => _storage.GetPlatformStatsAsync();
+
+ public Task SavePlatformStatsAsync(PlatformStats stats)
+ => _storage.SavePlatformStatsAsync(stats);
+
+ public Task IncrementPlatformStatAsync(string field, long amount = 1)
+ => _storage.IncrementPlatformStatAsync(field, amount);
+
+ public Task RecordUserLoginAsync(string tenantId, string upn, string? displayName, string? objectId)
+ => _storage.RecordUserLoginAsync(tenantId, upn, displayName, objectId);
+
+ public Task GetUserActivityMetricsAsync(string tenantId)
+ => _storage.GetUserActivityMetricsAsync(tenantId);
+
+ public Task GetAllUserActivityMetricsAsync()
+ => _storage.GetAllUserActivityMetricsAsync();
+
+ public Task<(int uniqueUsers, int loginCount)> GetUserActivityForDateAsync(string? tenantId, DateTime date)
+ => _storage.GetUserActivityForDateAsync(tenantId, date);
+
+ public Task> GetMetricsSummaryAsync(string? tenantId)
+ => _storage.GetMetricsSummaryAsync(tenantId);
+ }
+}
diff --git a/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableRuleRepository.cs b/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableRuleRepository.cs
new file mode 100644
index 00000000..1e885547
--- /dev/null
+++ b/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableRuleRepository.cs
@@ -0,0 +1,69 @@
+using AutopilotMonitor.Shared.DataAccess;
+using AutopilotMonitor.Shared.Models;
+using AutopilotMonitor.Functions.Services;
+
+namespace AutopilotMonitor.Functions.DataAccess.TableStorage
+{
+ ///
+ /// Table Storage implementation of IRuleRepository.
+ /// Delegates to existing TableStorageService for backwards compatibility.
+ ///
+ public class TableRuleRepository : IRuleRepository
+ {
+ private readonly TableStorageService _storage;
+ private readonly IDataEventPublisher _publisher;
+
+ public TableRuleRepository(TableStorageService storage, IDataEventPublisher publisher)
+ {
+ _storage = storage;
+ _publisher = publisher;
+ }
+
+ public async Task StoreRuleResultAsync(RuleResult result)
+ {
+ var success = await _storage.StoreRuleResultAsync(result);
+ if (success)
+ await _publisher.PublishAsync("rule.evaluated", result, result.TenantId);
+ return success;
+ }
+
+ public Task> GetRuleResultsAsync(string tenantId, string sessionId)
+ => _storage.GetRuleResultsAsync(tenantId, sessionId);
+
+ public Task StoreGatherRuleAsync(GatherRule rule, string tenantId = "global")
+ => _storage.StoreGatherRuleAsync(rule, tenantId);
+
+ public Task> GetGatherRulesAsync(string partitionKey)
+ => _storage.GetGatherRulesAsync(partitionKey);
+
+ public Task DeleteGatherRuleAsync(string tenantId, string ruleId)
+ => _storage.DeleteGatherRuleAsync(tenantId, ruleId);
+
+ public Task StoreRuleStateAsync(string tenantId, string ruleId, bool enabled)
+ => _storage.StoreRuleStateAsync(tenantId, ruleId, enabled);
+
+ public Task> GetRuleStatesAsync(string tenantId)
+ => _storage.GetRuleStatesAsync(tenantId);
+
+ public Task DeleteRuleStateAsync(string tenantId, string ruleId)
+ => _storage.DeleteRuleStateAsync(tenantId, ruleId);
+
+ public Task StoreAnalyzeRuleAsync(AnalyzeRule rule, string tenantId = "global")
+ => _storage.StoreAnalyzeRuleAsync(rule, tenantId);
+
+ public Task> GetAnalyzeRulesAsync(string partitionKey)
+ => _storage.GetAnalyzeRulesAsync(partitionKey);
+
+ public Task DeleteAnalyzeRuleAsync(string tenantId, string ruleId)
+ => _storage.DeleteAnalyzeRuleAsync(tenantId, ruleId);
+
+ public Task StoreImeLogPatternAsync(ImeLogPattern pattern, string tenantId = "global")
+ => _storage.StoreImeLogPatternAsync(pattern, tenantId);
+
+ public Task> GetImeLogPatternsAsync(string partitionKey)
+ => _storage.GetImeLogPatternsAsync(partitionKey);
+
+ public Task DeleteImeLogPatternAsync(string tenantId, string patternId)
+ => _storage.DeleteImeLogPatternAsync(tenantId, patternId);
+ }
+}
diff --git a/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableSessionRepository.cs b/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableSessionRepository.cs
new file mode 100644
index 00000000..5eb89389
--- /dev/null
+++ b/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableSessionRepository.cs
@@ -0,0 +1,102 @@
+using AutopilotMonitor.Shared.DataAccess;
+using AutopilotMonitor.Shared.Models;
+using AutopilotMonitor.Functions.Services;
+
+namespace AutopilotMonitor.Functions.DataAccess.TableStorage
+{
+ ///
+ /// Table Storage implementation of ISessionRepository.
+ /// Delegates to existing TableStorageService for backwards compatibility.
+ ///
+ public class TableSessionRepository : ISessionRepository
+ {
+ private readonly TableStorageService _storage;
+ private readonly IDataEventPublisher _publisher;
+
+ public TableSessionRepository(TableStorageService storage, IDataEventPublisher publisher)
+ {
+ _storage = storage;
+ _publisher = publisher;
+ }
+
+ public async Task StoreSessionAsync(SessionRegistration registration)
+ {
+ var result = await _storage.StoreSessionAsync(registration);
+ if (result)
+ await _publisher.PublishAsync("session.created", registration, registration.TenantId);
+ return result;
+ }
+
+ public Task GetSessionAsync(string tenantId, string sessionId)
+ => _storage.GetSessionAsync(tenantId, sessionId);
+
+ public Task GetSessionsAsync(string tenantId, int maxResults = 100, string? cursor = null)
+ => _storage.GetSessionsAsync(tenantId, maxResults, cursor);
+
+ public Task GetAllSessionsAsync(int maxResults = 100, string? cursor = null)
+ => _storage.GetAllSessionsAsync(maxResults, cursor);
+
+ public Task DeleteSessionAsync(string tenantId, string sessionId)
+ => _storage.DeleteSessionAsync(tenantId, sessionId);
+
+ public Task UpdateSessionStatusAsync(
+ string tenantId, string sessionId, SessionStatus status,
+ EnrollmentPhase? currentPhase = null, string? failureReason = null,
+ DateTime? completedAt = null, DateTime? earliestEventTimestamp = null,
+ DateTime? latestEventTimestamp = null, bool? isPreProvisioned = null,
+ bool? isUserDriven = null, DateTime? resumedAt = null)
+ => _storage.UpdateSessionStatusAsync(tenantId, sessionId, status,
+ currentPhase, failureReason, completedAt, earliestEventTimestamp,
+ latestEventTimestamp, isPreProvisioned, isUserDriven, resumedAt);
+
+ public Task IncrementSessionEventCountAsync(
+ string tenantId, string sessionId, int increment,
+ DateTime? earliestEventTimestamp = null, DateTime? latestEventTimestamp = null,
+ EnrollmentPhase? currentPhase = null)
+ => _storage.IncrementSessionEventCountAsync(tenantId, sessionId, increment,
+ earliestEventTimestamp, latestEventTimestamp, currentPhase);
+
+ public Task UpdateSessionDiagnosticsBlobAsync(string tenantId, string sessionId, string blobName)
+ => _storage.UpdateSessionDiagnosticsBlobAsync(tenantId, sessionId, blobName);
+
+ public Task SetSessionPreProvisionedAsync(string tenantId, string sessionId, bool isPreProvisioned,
+ SessionStatus? status = null, bool? isUserDriven = null)
+ => _storage.SetSessionPreProvisionedAsync(tenantId, sessionId, isPreProvisioned, status, isUserDriven);
+
+ public Task UpdateSessionGeoAsync(string tenantId, string sessionId,
+ string? country, string? region, string? city, string? loc)
+ => _storage.UpdateSessionGeoAsync(tenantId, sessionId, country, region, city, loc);
+
+ public Task UpdateSessionImeAgentVersionAsync(string tenantId, string sessionId, string version)
+ => _storage.UpdateSessionImeAgentVersionAsync(tenantId, sessionId, version);
+
+ public async Task StoreEventAsync(EnrollmentEvent evt)
+ {
+ var result = await _storage.StoreEventAsync(evt);
+ if (result)
+ await _publisher.PublishAsync("event.ingested", evt, evt.TenantId);
+ return result;
+ }
+
+ public Task> SearchSessionsAsync(string? tenantId, SessionSearchFilter filter)
+ => _storage.SearchSessionsAsync(tenantId, filter);
+
+ public Task> SearchSessionsByEventAsync(
+ string? tenantId, string eventType, string? source, string? severity,
+ string? phase, int limit = 50)
+ => _storage.SearchSessionsByEventAsync(tenantId, eventType, source, severity, phase, limit);
+
+ public Task> SearchSessionsByCveAsync(
+ string? tenantId, string cveId, double? minCvssScore, string? overallRisk, int limit = 50)
+ => _storage.SearchSessionsByCveAsync(tenantId, cveId, minCvssScore, overallRisk, limit);
+
+ public Task UpsertEventTypeIndexBatchAsync(string tenantId, string sessionId, IEnumerable eventTypes)
+ => _storage.UpsertEventTypeIndexBatchAsync(tenantId, sessionId, eventTypes);
+
+ public Task UpsertDeviceSnapshotAsync(string tenantId, string sessionId, IEnumerable events)
+ => _storage.UpsertDeviceSnapshotAsync(tenantId, sessionId, events);
+
+ public Task UpsertCveIndexEntriesAsync(string tenantId, string sessionId, List> findings)
+ => _storage.UpsertCveIndexEntriesAsync(tenantId, sessionId, findings);
+ }
+}
diff --git a/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableStorageInitializer.cs b/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableStorageInitializer.cs
new file mode 100644
index 00000000..107e8e59
--- /dev/null
+++ b/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableStorageInitializer.cs
@@ -0,0 +1,22 @@
+using AutopilotMonitor.Shared.DataAccess;
+using AutopilotMonitor.Functions.Services;
+
+namespace AutopilotMonitor.Functions.DataAccess.TableStorage
+{
+ ///
+ /// Table Storage implementation of IStorageInitializer.
+ /// Delegates to existing TableStorageService for backwards compatibility.
+ ///
+ public class TableStorageInitializer : IStorageInitializer
+ {
+ private readonly TableStorageService _storage;
+
+ public TableStorageInitializer(TableStorageService storage)
+ {
+ _storage = storage;
+ }
+
+ public Task InitializeAsync()
+ => _storage.InitializeTablesAsync();
+ }
+}
diff --git a/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableVulnerabilityRepository.cs b/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableVulnerabilityRepository.cs
new file mode 100644
index 00000000..07d395a9
--- /dev/null
+++ b/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableVulnerabilityRepository.cs
@@ -0,0 +1,43 @@
+using AutopilotMonitor.Shared.DataAccess;
+using AutopilotMonitor.Functions.Services;
+
+namespace AutopilotMonitor.Functions.DataAccess.TableStorage
+{
+ ///
+ /// Table Storage implementation of IVulnerabilityRepository.
+ /// Delegates to existing TableStorageService for backwards compatibility.
+ ///
+ public class TableVulnerabilityRepository : IVulnerabilityRepository
+ {
+ private readonly TableStorageService _storage;
+ private readonly IDataEventPublisher _publisher;
+
+ public TableVulnerabilityRepository(TableStorageService storage, IDataEventPublisher publisher)
+ {
+ _storage = storage;
+ _publisher = publisher;
+ }
+
+ public async Task StoreVulnerabilityReportAsync(string tenantId, string sessionId, Dictionary reportData)
+ {
+ await _storage.StoreVulnerabilityReportAsync(tenantId, sessionId, reportData);
+ await _publisher.PublishAsync("vulnerability.reported", new { tenantId, sessionId, reportData }, tenantId);
+ }
+
+ public Task?> GetVulnerabilityReportAsync(string tenantId, string sessionId)
+ => _storage.GetVulnerabilityReportAsync(tenantId, sessionId);
+
+ public Task UpsertSoftwareInventoryAsync(string tenantId, List> inventoryItems,
+ string sessionId, Dictionary? cpeMappings = null)
+ => _storage.UpsertSoftwareInventoryAsync(tenantId, inventoryItems, sessionId, cpeMappings);
+
+ public Task>> GetSoftwareInventoryAsync(string tenantId)
+ => _storage.GetSoftwareInventoryAsync(tenantId);
+
+ public Task ImportCpeMappingSeedFromJsonAsync(string json)
+ => _storage.ImportCpeMappingSeedFromJsonAsync(json);
+
+ public Task ImportCpeCommunityMappingsFromJsonAsync(string json)
+ => _storage.ImportCpeCommunityMappingsFromJsonAsync(json);
+ }
+}
diff --git a/src/Backend/AutopilotMonitor.Functions/Program.cs b/src/Backend/AutopilotMonitor.Functions/Program.cs
index 6afbb024..3c73116e 100644
--- a/src/Backend/AutopilotMonitor.Functions/Program.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Program.cs
@@ -1,5 +1,6 @@
using System.Text.Json;
using System.Text.Json.Serialization;
+using AutopilotMonitor.Functions.DataAccess;
using AutopilotMonitor.Functions.Functions.Config;
using AutopilotMonitor.Functions.Functions.Ingest;
using AutopilotMonitor.Functions.Functions.Sessions;
@@ -100,6 +101,11 @@ builder.Services.AddMemoryCache();
builder.Services.AddHttpClient();
builder.Services.AddSingleton();
builder.Services.AddHostedService(); // Initialize all tables at startup
+
+// Data Access Layer — repository interfaces backed by Table Storage.
+// To switch to Cosmos DB: replace AddTableStorageDataAccess() with AddCosmosDataAccess().
+// To add event streaming: chain .AddEventStreaming() after this call.
+builder.Services.AddTableStorageDataAccess();
builder.Services.AddSingleton();
builder.Services.AddSingleton();
builder.Services.AddSingleton();
diff --git a/src/Shared/AutopilotMonitor.Shared/DataAccess/IAdminRepository.cs b/src/Shared/AutopilotMonitor.Shared/DataAccess/IAdminRepository.cs
new file mode 100644
index 00000000..d05446fe
--- /dev/null
+++ b/src/Shared/AutopilotMonitor.Shared/DataAccess/IAdminRepository.cs
@@ -0,0 +1,64 @@
+namespace AutopilotMonitor.Shared.DataAccess
+{
+ ///
+ /// Repository for admin and tenant member management.
+ /// Covers: GlobalAdmins, TenantAdmins, ApiKeys tables.
+ ///
+ public interface IAdminRepository
+ {
+ // --- Global Admins ---
+ Task IsGlobalAdminAsync(string upn);
+ Task> GetAllGlobalAdminsAsync();
+ Task AddGlobalAdminAsync(string upn, string addedBy);
+ Task RemoveGlobalAdminAsync(string upn);
+ Task DisableGlobalAdminAsync(string upn);
+
+ // --- Tenant Members ---
+ Task> GetTenantMembersAsync(string tenantId);
+ Task AddTenantMemberAsync(string tenantId, string upn, string addedBy, string role, bool canManageBootstrapTokens = false);
+ Task RemoveTenantMemberAsync(string tenantId, string upn);
+ Task UpdateMemberPermissionsAsync(string tenantId, string upn, string role, bool canManageBootstrapTokens);
+ Task GetTenantMemberAsync(string tenantId, string upn);
+ Task IsTenantAdminAsync(string tenantId, string upn);
+ Task IsTenantMemberAsync(string tenantId, string upn);
+
+ // --- API Keys ---
+ Task ValidateApiKeyAsync(string apiKeyHash);
+ Task StoreApiKeyAsync(string tenantId, ApiKeyEntry entry);
+ Task> GetApiKeysAsync(string tenantId);
+ Task RevokeApiKeyAsync(string tenantId, string keyId);
+ }
+
+ public class GlobalAdminEntry
+ {
+ public string Upn { get; set; } = string.Empty;
+ public bool IsEnabled { get; set; } = true;
+ public DateTime AddedAt { get; set; }
+ public string AddedBy { get; set; } = string.Empty;
+ }
+
+ public class TenantMember
+ {
+ public string Upn { get; set; } = string.Empty;
+ public string TenantId { get; set; } = string.Empty;
+ public string Role { get; set; } = "Admin";
+ public bool IsEnabled { get; set; } = true;
+ public bool CanManageBootstrapTokens { get; set; }
+ public DateTime AddedAt { get; set; }
+ public string AddedBy { get; set; } = string.Empty;
+ }
+
+ public class ApiKeyEntry
+ {
+ public string KeyId { get; set; } = string.Empty;
+ public string TenantId { get; set; } = string.Empty;
+ public string KeyHash { get; set; } = string.Empty;
+ public string Name { get; set; } = string.Empty;
+ public string Scope { get; set; } = "tenant";
+ public string CreatedBy { get; set; } = string.Empty;
+ public DateTime CreatedAt { get; set; }
+ public DateTime? ExpiresAt { get; set; }
+ public bool IsActive { get; set; } = true;
+ public long RequestCount { get; set; }
+ }
+}
diff --git a/src/Shared/AutopilotMonitor.Shared/DataAccess/IBootstrapRepository.cs b/src/Shared/AutopilotMonitor.Shared/DataAccess/IBootstrapRepository.cs
new file mode 100644
index 00000000..efedf96a
--- /dev/null
+++ b/src/Shared/AutopilotMonitor.Shared/DataAccess/IBootstrapRepository.cs
@@ -0,0 +1,18 @@
+using AutopilotMonitor.Shared.Models;
+
+namespace AutopilotMonitor.Shared.DataAccess
+{
+ ///
+ /// Repository for bootstrap session management.
+ /// Covers: BootstrapSessions table.
+ ///
+ public interface IBootstrapRepository
+ {
+ Task CreateBootstrapSessionAsync(BootstrapSession session);
+ Task GetBootstrapSessionByCodeAsync(string shortCode);
+ Task ValidateBootstrapTokenAsync(string token);
+ Task> GetBootstrapSessionsAsync(string tenantId);
+ Task RevokeBootstrapSessionAsync(string shortCode);
+ Task IncrementBootstrapUsageAsync(string shortCode);
+ }
+}
diff --git a/src/Shared/AutopilotMonitor.Shared/DataAccess/IConfigRepository.cs b/src/Shared/AutopilotMonitor.Shared/DataAccess/IConfigRepository.cs
new file mode 100644
index 00000000..e0cab840
--- /dev/null
+++ b/src/Shared/AutopilotMonitor.Shared/DataAccess/IConfigRepository.cs
@@ -0,0 +1,30 @@
+using AutopilotMonitor.Shared.Models;
+
+namespace AutopilotMonitor.Shared.DataAccess
+{
+ ///
+ /// Repository for tenant and admin configuration.
+ /// Covers: TenantConfiguration, AdminConfiguration, PreviewWhitelist, PreviewConfig tables.
+ ///
+ public interface IConfigRepository
+ {
+ // --- Tenant Configuration ---
+ Task GetTenantConfigurationAsync(string tenantId);
+ Task SaveTenantConfigurationAsync(TenantConfiguration config);
+ Task> GetAllTenantConfigurationsAsync();
+
+ // --- Admin Configuration ---
+ Task GetAdminConfigurationAsync();
+ Task SaveAdminConfigurationAsync(AdminConfiguration config);
+
+ // --- Preview Whitelist ---
+ Task IsInPreviewWhitelistAsync(string tenantId);
+ Task AddToPreviewWhitelistAsync(string tenantId, string addedBy);
+ Task RemoveFromPreviewWhitelistAsync(string tenantId);
+ Task> GetPreviewWhitelistAsync();
+
+ // --- Preview Config ---
+ Task> GetPreviewConfigAsync();
+ Task SavePreviewConfigAsync(string key, string value);
+ }
+}
diff --git a/src/Shared/AutopilotMonitor.Shared/DataAccess/IDataEventPublisher.cs b/src/Shared/AutopilotMonitor.Shared/DataAccess/IDataEventPublisher.cs
new file mode 100644
index 00000000..a7e71fb1
--- /dev/null
+++ b/src/Shared/AutopilotMonitor.Shared/DataAccess/IDataEventPublisher.cs
@@ -0,0 +1,27 @@
+namespace AutopilotMonitor.Shared.DataAccess
+{
+ ///
+ /// Publishes data change events for downstream consumers (Event Hub, Service Bus, etc.).
+ /// Default implementation is NullDataEventPublisher (no-op).
+ ///
+ public interface IDataEventPublisher
+ {
+ ///
+ /// Publishes a data change event.
+ ///
+ /// Domain event type (e.g. "session.created", "event.ingested", "rule.evaluated")
+ /// The event payload (typically the domain object)
+ /// Optional tenant context for routing
+ Task PublishAsync(string eventType, object payload, string? tenantId = null);
+ }
+
+ ///
+ /// No-op implementation of IDataEventPublisher.
+ /// Used when event streaming is not configured.
+ ///
+ public class NullDataEventPublisher : IDataEventPublisher
+ {
+ public Task PublishAsync(string eventType, object payload, string? tenantId = null)
+ => Task.CompletedTask;
+ }
+}
diff --git a/src/Shared/AutopilotMonitor.Shared/DataAccess/IDeviceSecurityRepository.cs b/src/Shared/AutopilotMonitor.Shared/DataAccess/IDeviceSecurityRepository.cs
new file mode 100644
index 00000000..bececb33
--- /dev/null
+++ b/src/Shared/AutopilotMonitor.Shared/DataAccess/IDeviceSecurityRepository.cs
@@ -0,0 +1,44 @@
+namespace AutopilotMonitor.Shared.DataAccess
+{
+ ///
+ /// Repository for device and version blocking.
+ /// Covers: BlockedDevices, BlockedVersions tables.
+ ///
+ public interface IDeviceSecurityRepository
+ {
+ // --- Blocked Devices ---
+ Task<(bool isBlocked, DateTime? unblockAt, string action)> IsDeviceBlockedAsync(string tenantId, string serialNumber);
+ Task> GetBlockedDevicesAsync(string tenantId);
+ Task> GetAllBlockedDevicesAsync();
+ Task BlockDeviceAsync(string tenantId, string serialNumber, int durationHours,
+ string blockedByEmail, string? reason = null, string action = "Block");
+ Task UnblockDeviceAsync(string tenantId, string serialNumber);
+
+ // --- Blocked Versions ---
+ Task<(bool isBlocked, string action, string? matchedPattern)> IsVersionBlockedAsync(string agentVersion);
+ Task> GetBlockedVersionsAsync();
+ Task BlockVersionAsync(string versionPattern, string action, string createdByEmail, string? reason = null);
+ Task UnblockVersionAsync(string versionPattern);
+ }
+
+ public class BlockedDeviceEntry
+ {
+ public string TenantId { get; set; } = string.Empty;
+ public string SerialNumber { get; set; } = string.Empty;
+ public DateTime BlockedAt { get; set; }
+ public DateTime? UnblockAt { get; set; }
+ public string? BlockedByEmail { get; set; }
+ public int DurationHours { get; set; }
+ public string? Reason { get; set; }
+ public string Action { get; set; } = "Block";
+ }
+
+ public class BlockedVersionEntry
+ {
+ public string VersionPattern { get; set; } = string.Empty;
+ public string Action { get; set; } = "Block";
+ public string CreatedByEmail { get; set; } = string.Empty;
+ public DateTime CreatedAt { get; set; }
+ public string? Reason { get; set; }
+ }
+}
diff --git a/src/Shared/AutopilotMonitor.Shared/DataAccess/IMaintenanceRepository.cs b/src/Shared/AutopilotMonitor.Shared/DataAccess/IMaintenanceRepository.cs
new file mode 100644
index 00000000..8f0240dd
--- /dev/null
+++ b/src/Shared/AutopilotMonitor.Shared/DataAccess/IMaintenanceRepository.cs
@@ -0,0 +1,36 @@
+using AutopilotMonitor.Shared.Models;
+
+namespace AutopilotMonitor.Shared.DataAccess
+{
+ ///
+ /// Repository for audit logs and data retention/maintenance operations.
+ /// Covers: AuditLogs table + maintenance queries across Sessions/Events/RuleResults.
+ ///
+ public interface IMaintenanceRepository
+ {
+ // --- Audit Logs ---
+ Task LogAuditEntryAsync(string tenantId, string action, string entityType,
+ string entityId, string performedBy, Dictionary? details = null);
+ Task> GetAuditLogsAsync(string tenantId, int maxResults = 100);
+ Task> GetAllAuditLogsAsync(int maxResults = 100);
+
+ // --- Data Retention Queries ---
+ Task> GetSessionsOlderThanAsync(string tenantId, DateTime cutoffDate);
+ Task> GetSessionsByDateRangeAsync(DateTime startDate, DateTime endDate, string? tenantId = null);
+ Task> GetStalledSessionsAsync(string tenantId, DateTime cutoffTime);
+ Task> GetExcessiveDataSendersAsync(string tenantId, DateTime windowCutoff, int maxSessionWindowHours);
+
+ // --- Tenant Discovery ---
+ Task> GetAllTenantIdsAsync();
+
+ // --- Cleanup ---
+ Task DeleteSessionEventsAsync(string tenantId, string sessionId);
+ Task DeleteSessionRuleResultsAsync(string tenantId, string sessionId);
+ Task DeleteSessionAppInstallSummariesAsync(string tenantId, string sessionId);
+
+ // --- Index Maintenance ---
+ Task BackfillSessionIndexAsync();
+ Task CleanupGhostSessionIndexEntriesAsync();
+ Task IsSessionIndexEmptyAsync();
+ }
+}
diff --git a/src/Shared/AutopilotMonitor.Shared/DataAccess/IMetricsRepository.cs b/src/Shared/AutopilotMonitor.Shared/DataAccess/IMetricsRepository.cs
new file mode 100644
index 00000000..c0cad9fd
--- /dev/null
+++ b/src/Shared/AutopilotMonitor.Shared/DataAccess/IMetricsRepository.cs
@@ -0,0 +1,36 @@
+using AutopilotMonitor.Shared.Models;
+
+namespace AutopilotMonitor.Shared.DataAccess
+{
+ ///
+ /// Repository for usage metrics, platform stats, and user activity tracking.
+ /// Covers: UsageMetrics, PlatformStats, UserActivity, AppInstallSummaries tables.
+ ///
+ public interface IMetricsRepository
+ {
+ // --- Usage Metrics Snapshots ---
+ Task SaveUsageMetricsSnapshotAsync(UsageMetricsSnapshot metrics);
+ Task> GetUsageMetricsSnapshotAsync(
+ string? tenantId = null, string? startDate = null, string? endDate = null, int maxResults = 100);
+ Task HasUsageMetricsSnapshotAsync(string date);
+
+ // --- App Install Summaries ---
+ Task StoreAppInstallSummaryAsync(AppInstallSummary summary);
+ Task> GetAppInstallSummariesByTenantAsync(string tenantId);
+ Task> GetAllAppInstallSummariesAsync();
+
+ // --- Platform Stats ---
+ Task GetPlatformStatsAsync();
+ Task SavePlatformStatsAsync(PlatformStats stats);
+ Task IncrementPlatformStatAsync(string field, long amount = 1);
+
+ // --- User Activity ---
+ Task RecordUserLoginAsync(string tenantId, string upn, string? displayName, string? objectId);
+ Task GetUserActivityMetricsAsync(string tenantId);
+ Task GetAllUserActivityMetricsAsync();
+ Task<(int uniqueUsers, int loginCount)> GetUserActivityForDateAsync(string? tenantId, DateTime date);
+
+ // --- Metrics Summary (Agent API) ---
+ Task> GetMetricsSummaryAsync(string? tenantId);
+ }
+}
diff --git a/src/Shared/AutopilotMonitor.Shared/DataAccess/INotificationRepository.cs b/src/Shared/AutopilotMonitor.Shared/DataAccess/INotificationRepository.cs
new file mode 100644
index 00000000..5a8b9044
--- /dev/null
+++ b/src/Shared/AutopilotMonitor.Shared/DataAccess/INotificationRepository.cs
@@ -0,0 +1,34 @@
+using AutopilotMonitor.Shared.Models;
+
+namespace AutopilotMonitor.Shared.DataAccess
+{
+ ///
+ /// Repository for global notifications and session reports.
+ /// Covers: GlobalNotifications, SessionReports tables.
+ ///
+ public interface INotificationRepository
+ {
+ // --- Global Notifications ---
+ Task AddNotificationAsync(GlobalNotification notification);
+ Task> GetNotificationsAsync(int maxResults = 50);
+ Task DismissNotificationAsync(string notificationId, string dismissedBy);
+
+ // --- Session Reports ---
+ Task StoreSessionReportMetadataAsync(SessionReportMetadata metadata);
+ Task> GetSessionReportsAsync(string? tenantId = null, int maxResults = 50);
+ Task GetSessionReportAsync(string reportId);
+ }
+
+ public class GlobalNotification
+ {
+ public string NotificationId { get; set; } = string.Empty;
+ public string Title { get; set; } = string.Empty;
+ public string Message { get; set; } = string.Empty;
+ public string Severity { get; set; } = "info";
+ public DateTime CreatedAt { get; set; }
+ public string CreatedBy { get; set; } = string.Empty;
+ public bool IsDismissed { get; set; }
+ public string? DismissedBy { get; set; }
+ public DateTime? DismissedAt { get; set; }
+ }
+}
diff --git a/src/Shared/AutopilotMonitor.Shared/DataAccess/IRuleRepository.cs b/src/Shared/AutopilotMonitor.Shared/DataAccess/IRuleRepository.cs
new file mode 100644
index 00000000..a490782d
--- /dev/null
+++ b/src/Shared/AutopilotMonitor.Shared/DataAccess/IRuleRepository.cs
@@ -0,0 +1,35 @@
+using AutopilotMonitor.Shared.Models;
+
+namespace AutopilotMonitor.Shared.DataAccess
+{
+ ///
+ /// Repository for rule definitions, results, and patterns.
+ /// Covers: RuleResults, GatherRules, AnalyzeRules, ImeLogPatterns, RuleStates tables.
+ ///
+ public interface IRuleRepository
+ {
+ // --- Rule Results ---
+ Task StoreRuleResultAsync(RuleResult result);
+ Task> GetRuleResultsAsync(string tenantId, string sessionId);
+
+ // --- Gather Rules ---
+ Task StoreGatherRuleAsync(GatherRule rule, string tenantId = "global");
+ Task> GetGatherRulesAsync(string partitionKey);
+ Task DeleteGatherRuleAsync(string tenantId, string ruleId);
+
+ // --- Rule States ---
+ Task StoreRuleStateAsync(string tenantId, string ruleId, bool enabled);
+ Task> GetRuleStatesAsync(string tenantId);
+ Task DeleteRuleStateAsync(string tenantId, string ruleId);
+
+ // --- Analyze Rules ---
+ Task StoreAnalyzeRuleAsync(AnalyzeRule rule, string tenantId = "global");
+ Task> GetAnalyzeRulesAsync(string partitionKey);
+ Task DeleteAnalyzeRuleAsync(string tenantId, string ruleId);
+
+ // --- IME Log Patterns ---
+ Task StoreImeLogPatternAsync(ImeLogPattern pattern, string tenantId = "global");
+ Task> GetImeLogPatternsAsync(string partitionKey);
+ Task DeleteImeLogPatternAsync(string tenantId, string patternId);
+ }
+}
diff --git a/src/Shared/AutopilotMonitor.Shared/DataAccess/ISessionRepository.cs b/src/Shared/AutopilotMonitor.Shared/DataAccess/ISessionRepository.cs
new file mode 100644
index 00000000..96dad8aa
--- /dev/null
+++ b/src/Shared/AutopilotMonitor.Shared/DataAccess/ISessionRepository.cs
@@ -0,0 +1,52 @@
+using AutopilotMonitor.Shared.Models;
+
+namespace AutopilotMonitor.Shared.DataAccess
+{
+ ///
+ /// Repository for session and event data.
+ /// Covers: Sessions, SessionsIndex, Events tables.
+ ///
+ public interface ISessionRepository
+ {
+ // --- Session CRUD ---
+ Task StoreSessionAsync(SessionRegistration registration);
+ Task GetSessionAsync(string tenantId, string sessionId);
+ Task GetSessionsAsync(string tenantId, int maxResults = 100, string? cursor = null);
+ Task GetAllSessionsAsync(int maxResults = 100, string? cursor = null);
+ Task DeleteSessionAsync(string tenantId, string sessionId);
+
+ // --- Session Updates ---
+ Task UpdateSessionStatusAsync(
+ string tenantId, string sessionId, SessionStatus status,
+ EnrollmentPhase? currentPhase = null, string? failureReason = null,
+ DateTime? completedAt = null, DateTime? earliestEventTimestamp = null,
+ DateTime? latestEventTimestamp = null, bool? isPreProvisioned = null,
+ bool? isUserDriven = null, DateTime? resumedAt = null);
+ Task IncrementSessionEventCountAsync(
+ string tenantId, string sessionId, int increment,
+ DateTime? earliestEventTimestamp = null, DateTime? latestEventTimestamp = null,
+ EnrollmentPhase? currentPhase = null);
+ Task UpdateSessionDiagnosticsBlobAsync(string tenantId, string sessionId, string blobName);
+ Task SetSessionPreProvisionedAsync(string tenantId, string sessionId, bool isPreProvisioned,
+ SessionStatus? status = null, bool? isUserDriven = null);
+ Task UpdateSessionGeoAsync(string tenantId, string sessionId,
+ string? country, string? region, string? city, string? loc);
+ Task UpdateSessionImeAgentVersionAsync(string tenantId, string sessionId, string version);
+
+ // --- Events ---
+ Task StoreEventAsync(EnrollmentEvent evt);
+
+ // --- Search ---
+ Task> SearchSessionsAsync(string? tenantId, SessionSearchFilter filter);
+ Task> SearchSessionsByEventAsync(
+ string? tenantId, string eventType, string? source, string? severity,
+ string? phase, int limit = 50);
+ Task> SearchSessionsByCveAsync(
+ string? tenantId, string cveId, double? minCvssScore, string? overallRisk, int limit = 50);
+
+ // --- Agent Indexes ---
+ Task UpsertEventTypeIndexBatchAsync(string tenantId, string sessionId, IEnumerable eventTypes);
+ Task UpsertDeviceSnapshotAsync(string tenantId, string sessionId, IEnumerable events);
+ Task UpsertCveIndexEntriesAsync(string tenantId, string sessionId, List> findings);
+ }
+}
diff --git a/src/Shared/AutopilotMonitor.Shared/DataAccess/IStorageInitializer.cs b/src/Shared/AutopilotMonitor.Shared/DataAccess/IStorageInitializer.cs
new file mode 100644
index 00000000..34b7f646
--- /dev/null
+++ b/src/Shared/AutopilotMonitor.Shared/DataAccess/IStorageInitializer.cs
@@ -0,0 +1,11 @@
+namespace AutopilotMonitor.Shared.DataAccess
+{
+ ///
+ /// Initializes the storage backend (creates tables, containers, collections, etc.).
+ /// Each storage provider implements this for its specific setup needs.
+ ///
+ public interface IStorageInitializer
+ {
+ Task InitializeAsync();
+ }
+}
diff --git a/src/Shared/AutopilotMonitor.Shared/DataAccess/IVulnerabilityRepository.cs b/src/Shared/AutopilotMonitor.Shared/DataAccess/IVulnerabilityRepository.cs
new file mode 100644
index 00000000..d9c5905a
--- /dev/null
+++ b/src/Shared/AutopilotMonitor.Shared/DataAccess/IVulnerabilityRepository.cs
@@ -0,0 +1,17 @@
+namespace AutopilotMonitor.Shared.DataAccess
+{
+ ///
+ /// Repository for vulnerability data (CPE mappings, CVE reports, software inventory).
+ /// Covers: VulnerabilityCache, VulnerabilityReports, SoftwareInventory, CveIndex tables.
+ ///
+ public interface IVulnerabilityRepository
+ {
+ Task StoreVulnerabilityReportAsync(string tenantId, string sessionId, Dictionary reportData);
+ Task?> GetVulnerabilityReportAsync(string tenantId, string sessionId);
+ Task UpsertSoftwareInventoryAsync(string tenantId, List> inventoryItems,
+ string sessionId, Dictionary? cpeMappings = null);
+ Task>> GetSoftwareInventoryAsync(string tenantId);
+ Task ImportCpeMappingSeedFromJsonAsync(string json);
+ Task ImportCpeCommunityMappingsFromJsonAsync(string json);
+ }
+}
diff --git a/tasks/todo.md b/tasks/todo.md
index 7958cf89..aa34177e 100644
--- a/tasks/todo.md
+++ b/tasks/todo.md
@@ -1,16 +1,74 @@
-# Tenant Config Report (Global Admin)
+# Data Access Layer — Migration Plan
-## Goal
-New global-admin-only page that shows a read-only report of a tenant's full configuration,
-including admin-set parameters and computed runtime (agent) parameters.
+## Phase 1: Foundation ✅
+- [x] Define 10 Repository Interfaces in `Shared/DataAccess/`
+- [x] Define `IDataEventPublisher` + `NullDataEventPublisher`
+- [x] Define `IStorageInitializer`
+- [x] Create 5 core Table Storage implementations (delegate to `TableStorageService`)
+- [x] Create `DataAccessServiceExtensions` with `AddTableStorageDataAccess()`
+- [x] Register DAL in `Program.cs`
-## Plan
-- [x] Write plan
-- [ ] Create `/app/tenant-config-report/page.tsx` — global admin only, read-only report
- - Tenant selector (reuse pattern from gather-rules page)
- - Fetch config via `GET /api/config/{tenantId}` (existing endpoint, GA cross-tenant)
- - Display all TenantConfiguration fields grouped by section
- - Show computed runtime parameters (AgentConfigResponse equivalents with defaults)
- - Highlight non-default values for quick scanning
-- [ ] Add nav item to global admin section in `globalNavConfig.tsx`
-- [ ] Commit and push to feature branch
+## Phase 2: Migrate Core Functions to Use Interfaces
+These functions currently inject `TableStorageService` directly. Migrate to inject repository interfaces instead.
+
+### Session Functions
+- [ ] `GetSessionsFunction` → `ISessionRepository`
+- [ ] `GetSessionFunction` → `ISessionRepository`
+- [ ] `DeleteSessionFunction` → `ISessionRepository`
+- [ ] `RegisterSessionFunction` → `ISessionRepository`
+- [ ] `IngestEventsFunction` → `ISessionRepository`
+- [ ] `SearchSessionsFunction` → `ISessionRepository`
+- [ ] `SearchSessionsByEventFunction` → `ISessionRepository`
+- [ ] `SearchSessionsByCveFunction` → `ISessionRepository`
+
+### Rule Functions
+- [ ] `GetRuleResultsFunction` → `IRuleRepository`
+- [ ] `RunRulesFunction` → `IRuleRepository`
+- [ ] Rule management functions → `IRuleRepository`
+
+### Metrics Functions
+- [ ] `GetMetricsFunction` → `IMetricsRepository`
+- [ ] `GetHistoricalMetricsFunction` → `IMetricsRepository`
+- [ ] `GetPlatformStatsFunction` → `IMetricsRepository`
+
+### Maintenance Functions
+- [ ] `GetAuditLogsFunction` → `IMaintenanceRepository`
+- [ ] `MaintenanceFunction` → `IMaintenanceRepository`
+
+### Vulnerability Functions
+- [ ] All vulnerability functions → `IVulnerabilityRepository`
+
+## Phase 3: Migrate "Bypass" Services
+These services create their own `TableServiceClient` instances. Create proper repository implementations and migrate.
+
+- [ ] `TenantConfigurationService` → `IConfigRepository`
+- [ ] `AdminConfigurationService` → `IConfigRepository`
+- [ ] `PreviewWhitelistService` → `IConfigRepository`
+- [ ] `GlobalAdminService` → `IAdminRepository`
+- [ ] `TenantAdminsService` → `IAdminRepository`
+- [ ] `BootstrapSessionService` → `IBootstrapRepository`
+- [ ] `GlobalNotificationService` → `INotificationRepository`
+- [ ] `SessionReportService` → `INotificationRepository`
+- [ ] `BlockedDeviceService` → `IDeviceSecurityRepository`
+- [ ] `BlockedVersionService` → `IDeviceSecurityRepository`
+- [ ] `ApiKeyMiddleware` → `IAdminRepository`
+
+## Phase 4: Remove Legacy Code
+- [ ] Remove `GetTableClient()` and `GetTableServiceClient()` from `TableStorageService`
+- [ ] Move `TableStorageService` logic into repository implementations
+- [ ] Delete `TableStorageService` partial files once fully migrated
+- [ ] Remove direct `TableServiceClient` creation from all services
+
+## Phase 5: Event Streaming (When Ready)
+- [ ] Implement `EventHubPublisher` (or `ServiceBusPublisher`)
+- [ ] Register via `services.AddEventStreaming()`
+- [ ] Define event schemas for key domain events
+- [ ] Add event consumers for downstream processing
+
+## Phase 6: Cosmos DB Migration (When Ready)
+- [ ] Create `CosmosSessionRepository`, `CosmosRuleRepository`, etc.
+- [ ] Create `AddCosmosDataAccess()` extension method
+- [ ] Replace inverted-tick indexing with native ORDER BY DESC
+- [ ] Replace denormalized indexes with Cosmos queries
+- [ ] Handle 2MB document limit vs. 64KB Table Storage limit
+- [ ] Swap `AddTableStorageDataAccess()` → `AddCosmosDataAccess()` in Program.cs