diff --git a/Infrastructure/Clock.cs b/Infrastructure/Clock.cs
new file mode 100644
index 0000000..5c21d66
--- /dev/null
+++ b/Infrastructure/Clock.cs
@@ -0,0 +1,40 @@
+namespace NoticeBoard.Infrastructure;
+
+///
+/// Single source of "now" for Meow expiry checks, so the picker (browser wall-clock,
+/// no time zone info) and the server's comparison always agree on what "now" means.
+///
+/// Configured once at startup from the "TimeZone" setting (an IANA id like
+/// "Australia/Melbourne", or "UTC"). Leaving it blank falls back to the host
+/// machine's own local time zone — the original IIS behaviour, unchanged.
+///
+public static class Clock
+{
+ private static TimeZoneInfo _timeZone = TimeZoneInfo.Local;
+
+ public static void Configure(string? timeZoneId)
+ {
+ if (string.IsNullOrWhiteSpace(timeZoneId))
+ {
+ _timeZone = TimeZoneInfo.Local;
+ return;
+ }
+
+ try
+ {
+ _timeZone = TimeZoneInfo.FindSystemTimeZoneById(timeZoneId);
+ }
+ catch (TimeZoneNotFoundException)
+ {
+ // Unknown id (e.g. tzdata missing, or a typo) — UTC is a safe, unambiguous fallback.
+ _timeZone = TimeZoneInfo.Utc;
+ }
+ catch (InvalidTimeZoneException)
+ {
+ _timeZone = TimeZoneInfo.Utc;
+ }
+ }
+
+ /// Current time in the configured server time zone — directly comparable to a stored ExpiresAt value.
+ public static DateTime Now => TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, _timeZone);
+}