diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Host/Health/AdminRoleLeaderHealthCheck.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Host/Health/AdminRoleLeaderHealthCheck.cs
new file mode 100644
index 0000000..0f06251
--- /dev/null
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.Host/Health/AdminRoleLeaderHealthCheck.cs
@@ -0,0 +1,33 @@
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+using ZB.MOM.WW.OtOpcUa.Commons.Interfaces;
+
+namespace ZB.MOM.WW.OtOpcUa.Host.Health;
+
+///
+/// Reports Healthy on the admin-role leader, Degraded on a non-leader admin member. Used by
+/// the /health/active endpoint so external load balancers can route admin-singleton
+/// traffic to the current leader (cookie sessions still work on either node — DataProtection
+/// keys are shared).
+///
+public sealed class AdminRoleLeaderHealthCheck : IHealthCheck
+{
+ private readonly IClusterRoleInfo _roleInfo;
+
+ public AdminRoleLeaderHealthCheck(IClusterRoleInfo roleInfo)
+ {
+ _roleInfo = roleInfo;
+ }
+
+ public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
+ {
+ if (!_roleInfo.HasRole("admin"))
+ return Task.FromResult(HealthCheckResult.Healthy("Node does not carry admin role"));
+
+ var leader = _roleInfo.RoleLeader("admin");
+ var isLeader = leader is not null && leader.Value.Equals(_roleInfo.LocalNode);
+
+ return Task.FromResult(isLeader
+ ? HealthCheckResult.Healthy($"Admin leader ({_roleInfo.LocalNode})")
+ : HealthCheckResult.Degraded($"Admin member but not leader (leader={leader?.Value ?? ""})"));
+ }
+}
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Host/Health/AkkaClusterHealthCheck.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Host/Health/AkkaClusterHealthCheck.cs
new file mode 100644
index 0000000..e291bd8
--- /dev/null
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.Host/Health/AkkaClusterHealthCheck.cs
@@ -0,0 +1,26 @@
+using Akka.Actor;
+using Akka.Cluster;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+
+namespace ZB.MOM.WW.OtOpcUa.Host.Health;
+
+public sealed class AkkaClusterHealthCheck : IHealthCheck
+{
+ private readonly ActorSystem _system;
+
+ public AkkaClusterHealthCheck(ActorSystem system)
+ {
+ _system = system;
+ }
+
+ public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
+ {
+ var cluster = Akka.Cluster.Cluster.Get(_system);
+ var selfUp = cluster.State.Members.Any(m =>
+ m.Address == cluster.SelfAddress && m.Status == MemberStatus.Up);
+
+ return Task.FromResult(selfUp
+ ? HealthCheckResult.Healthy($"Self Up; {cluster.State.Members.Count} member(s)")
+ : HealthCheckResult.Degraded("Self not yet Up in cluster"));
+ }
+}
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Host/Health/DatabaseHealthCheck.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Host/Health/DatabaseHealthCheck.cs
new file mode 100644
index 0000000..b8f4781
--- /dev/null
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.Host/Health/DatabaseHealthCheck.cs
@@ -0,0 +1,29 @@
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+using ZB.MOM.WW.OtOpcUa.Configuration;
+
+namespace ZB.MOM.WW.OtOpcUa.Host.Health;
+
+public sealed class DatabaseHealthCheck : IHealthCheck
+{
+ private readonly IDbContextFactory _dbFactory;
+
+ public DatabaseHealthCheck(IDbContextFactory dbFactory)
+ {
+ _dbFactory = dbFactory;
+ }
+
+ public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ await using var db = await _dbFactory.CreateDbContextAsync(cancellationToken);
+ await db.Deployments.AsNoTracking().Take(1).ToListAsync(cancellationToken);
+ return HealthCheckResult.Healthy("ConfigDb reachable");
+ }
+ catch (Exception ex)
+ {
+ return HealthCheckResult.Unhealthy("ConfigDb unreachable", ex);
+ }
+ }
+}
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Host/Health/HealthEndpoints.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Host/Health/HealthEndpoints.cs
new file mode 100644
index 0000000..1d2e3cd
--- /dev/null
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.Host/Health/HealthEndpoints.cs
@@ -0,0 +1,41 @@
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Diagnostics.HealthChecks;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+
+namespace ZB.MOM.WW.OtOpcUa.Host.Health;
+
+public static class HealthEndpoints
+{
+ ///
+ /// Registers the standard ASP.NET Core health-check infrastructure plus the OtOpcUa-specific
+ /// probes. Mirrors ScadaLink's three-tier pattern: ready = boot ok; active =
+ /// fully serving traffic; healthz = bare process liveness.
+ ///
+ public static IServiceCollection AddOtOpcUaHealth(this IServiceCollection services)
+ {
+ services.AddHealthChecks()
+ .AddCheck("configdb", tags: new[] { "ready", "active" })
+ .AddCheck("akka", tags: new[] { "ready", "active" })
+ .AddCheck("admin-leader", tags: new[] { "active" });
+ return services;
+ }
+
+ public static IEndpointRouteBuilder MapOtOpcUaHealth(this IEndpointRouteBuilder app)
+ {
+ app.MapHealthChecks("/health/ready", new HealthCheckOptions
+ {
+ Predicate = c => c.Tags.Contains("ready"),
+ });
+ app.MapHealthChecks("/health/active", new HealthCheckOptions
+ {
+ Predicate = c => c.Tags.Contains("active"),
+ });
+ app.MapHealthChecks("/healthz", new HealthCheckOptions
+ {
+ Predicate = _ => false, // process-liveness only — no probes run.
+ });
+ return app;
+ }
+}
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Host/appsettings.Development.json b/src/Server/ZB.MOM.WW.OtOpcUa.Host/appsettings.Development.json
new file mode 100644
index 0000000..f106e0a
--- /dev/null
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.Host/appsettings.Development.json
@@ -0,0 +1,15 @@
+{
+ "Serilog": {
+ "MinimumLevel": {
+ "Default": "Debug",
+ "Override": {
+ "Akka": "Information"
+ }
+ }
+ },
+ "Security": {
+ "Ldap": {
+ "DevStubMode": true
+ }
+ }
+}