diff --git a/app/Activity/Notifications/Handlers/CommentCreationNotificationHandler.php b/app/Activity/Notifications/Handlers/CommentCreationNotificationHandler.php
index 5f2e1c770..67c304339 100644
--- a/app/Activity/Notifications/Handlers/CommentCreationNotificationHandler.php
+++ b/app/Activity/Notifications/Handlers/CommentCreationNotificationHandler.php
@@ -3,10 +3,11 @@
 namespace BookStack\Activity\Notifications\Handlers;
 
 use BookStack\Activity\Models\Loggable;
+use BookStack\Users\Models\User;
 
 class CommentCreationNotificationHandler implements NotificationHandler
 {
-    public function handle(string $activityType, Loggable|string $detail): void
+    public function handle(string $activityType, Loggable|string $detail, User $user): void
     {
         // TODO
     }
diff --git a/app/Activity/Notifications/Handlers/NotificationHandler.php b/app/Activity/Notifications/Handlers/NotificationHandler.php
index fdf97eb79..fecca2181 100644
--- a/app/Activity/Notifications/Handlers/NotificationHandler.php
+++ b/app/Activity/Notifications/Handlers/NotificationHandler.php
@@ -3,11 +3,14 @@
 namespace BookStack\Activity\Notifications\Handlers;
 
 use BookStack\Activity\Models\Loggable;
+use BookStack\Users\Models\User;
 
 interface NotificationHandler
 {
     /**
      * Run this handler.
+     * Provides the activity type, related activity detail/model
+     * along with the user that triggered the activity.
      */
-    public function handle(string $activityType, string|Loggable $detail): void;
+    public function handle(string $activityType, string|Loggable $detail, User $user): void;
 }
diff --git a/app/Activity/Notifications/Handlers/PageCreationNotificationHandler.php b/app/Activity/Notifications/Handlers/PageCreationNotificationHandler.php
index 20189fc1e..a61df48ae 100644
--- a/app/Activity/Notifications/Handlers/PageCreationNotificationHandler.php
+++ b/app/Activity/Notifications/Handlers/PageCreationNotificationHandler.php
@@ -3,11 +3,32 @@
 namespace BookStack\Activity\Notifications\Handlers;
 
 use BookStack\Activity\Models\Loggable;
+use BookStack\Activity\Models\Watch;
+use BookStack\Activity\Tools\EntityWatchers;
+use BookStack\Activity\WatchLevels;
+use BookStack\Users\Models\User;
 
 class PageCreationNotificationHandler implements NotificationHandler
 {
-    public function handle(string $activityType, Loggable|string $detail): void
+    public function handle(string $activityType, Loggable|string $detail, User $user): void
     {
         // TODO
+
+        // No user-level preferences to care about here.
+        // Possible Scenarios:
+        // ✅ User watching parent chapter
+        // ✅ User watching parent book
+        // ❌ User ignoring parent book
+        // ❌ User ignoring parent chapter
+        // ❌ User watching parent book, ignoring chapter
+        // ✅ User watching parent book, watching chapter
+        // ❌ User ignoring parent book, ignoring chapter
+        // ✅ User ignoring parent book, watching chapter
+
+        // Get all relevant watchers
+        $watchers = new EntityWatchers($detail, WatchLevels::NEW);
+
+        // TODO - need to check entity visibility and receive-notifications permissions.
+        //   Maybe abstract this to a generic late-stage filter?
     }
 }
diff --git a/app/Activity/Notifications/Handlers/PageUpdateNotificationHandler.php b/app/Activity/Notifications/Handlers/PageUpdateNotificationHandler.php
index ef71dccbf..bbd189d52 100644
--- a/app/Activity/Notifications/Handlers/PageUpdateNotificationHandler.php
+++ b/app/Activity/Notifications/Handlers/PageUpdateNotificationHandler.php
@@ -3,10 +3,11 @@
 namespace BookStack\Activity\Notifications\Handlers;
 
 use BookStack\Activity\Models\Loggable;
+use BookStack\Users\Models\User;
 
 class PageUpdateNotificationHandler implements NotificationHandler
 {
-    public function handle(string $activityType, Loggable|string $detail): void
+    public function handle(string $activityType, Loggable|string $detail, User $user): void
     {
         // TODO
     }
diff --git a/app/Activity/Notifications/LinkedMailMessageLine.php b/app/Activity/Notifications/LinkedMailMessageLine.php
new file mode 100644
index 000000000..224d8e87c
--- /dev/null
+++ b/app/Activity/Notifications/LinkedMailMessageLine.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace BookStack\Activity\Notifications;
+
+use Illuminate\Contracts\Support\Htmlable;
+
+/**
+ * A line of text with linked text included, intended for use
+ * in MailMessages. The line should have a ':link' placeholder for
+ * where the link should be inserted within the line.
+ */
+class LinkedMailMessageLine implements Htmlable
+{
+    public function __construct(
+        protected string $url,
+        protected string $line,
+        protected string $linkText,
+    ) {
+    }
+
+    public function toHtml(): string
+    {
+        $link = '<a href="' . e($this->url) . '">' . e($this->linkText) . '</a>';
+        return str_replace(':link', $link, e($this->line));
+    }
+}
diff --git a/app/Activity/Notifications/Messages/BaseActivityNotification.php b/app/Activity/Notifications/Messages/BaseActivityNotification.php
new file mode 100644
index 000000000..285e2803e
--- /dev/null
+++ b/app/Activity/Notifications/Messages/BaseActivityNotification.php
@@ -0,0 +1,50 @@
+<?php
+
+namespace BookStack\Activity\Notifications\Messages;
+
+use BookStack\Activity\Models\Loggable;
+use BookStack\Users\Models\User;
+use Illuminate\Bus\Queueable;
+use Illuminate\Notifications\Messages\MailMessage;
+use Illuminate\Notifications\Notification;
+
+abstract class BaseActivityNotification extends Notification
+{
+    use Queueable;
+
+    public function __construct(
+        protected Loggable|string $detail,
+        protected User $user,
+    ) {
+    }
+
+    /**
+     * Get the notification's delivery channels.
+     *
+     * @param  mixed  $notifiable
+     * @return array
+     */
+    public function via($notifiable)
+    {
+        return ['mail'];
+    }
+
+    /**
+     * Get the mail representation of the notification.
+     */
+    abstract public function toMail(mixed $notifiable): MailMessage;
+
+    /**
+     * Get the array representation of the notification.
+     *
+     * @param  mixed  $notifiable
+     * @return array
+     */
+    public function toArray($notifiable)
+    {
+        return [
+            'activity_detail' => $this->detail,
+            'activity_creator' => $this->user,
+        ];
+    }
+}
diff --git a/app/Activity/Notifications/Messages/PageCreationNotification.php b/app/Activity/Notifications/Messages/PageCreationNotification.php
new file mode 100644
index 000000000..2e9a6debc
--- /dev/null
+++ b/app/Activity/Notifications/Messages/PageCreationNotification.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace BookStack\Activity\Notifications\Messages;
+
+use BookStack\Activity\Notifications\LinkedMailMessageLine;
+use BookStack\Entities\Models\Page;
+use Illuminate\Notifications\Messages\MailMessage;
+
+class PageCreationNotification extends BaseActivityNotification
+{
+    public function toMail(mixed $notifiable): MailMessage
+    {
+        /** @var Page $page */
+        $page = $this->detail;
+
+        return (new MailMessage())
+            ->subject("New Page: " . $page->getShortName())
+            ->line("A new page has been created in " . setting('app-name') . ':')
+            ->line("Page Name: " . $page->name)
+            ->line("Created By: " . $this->user->name)
+            ->action('View Page', $page->getUrl())
+            ->line(new LinkedMailMessageLine(
+                url('/preferences/notifications'),
+                'This notification was sent to you because :link cover this type of activity for this item.',
+                'your notification preferences',
+            ));
+    }
+}
diff --git a/app/Activity/Notifications/NotificationManager.php b/app/Activity/Notifications/NotificationManager.php
index 5864b8456..01361c1ee 100644
--- a/app/Activity/Notifications/NotificationManager.php
+++ b/app/Activity/Notifications/NotificationManager.php
@@ -8,6 +8,7 @@ use BookStack\Activity\Notifications\Handlers\CommentCreationNotificationHandler
 use BookStack\Activity\Notifications\Handlers\NotificationHandler;
 use BookStack\Activity\Notifications\Handlers\PageCreationNotificationHandler;
 use BookStack\Activity\Notifications\Handlers\PageUpdateNotificationHandler;
+use BookStack\Users\Models\User;
 
 class NotificationManager
 {
@@ -16,13 +17,13 @@ class NotificationManager
      */
     protected array $handlers = [];
 
-    public function handle(string $activityType, string|Loggable $detail): void
+    public function handle(string $activityType, string|Loggable $detail, User $user): void
     {
         $handlersToRun = $this->handlers[$activityType] ?? [];
         foreach ($handlersToRun as $handlerClass) {
             /** @var NotificationHandler $handler */
             $handler = app()->make($handlerClass);
-            $handler->handle($activityType, $detail);
+            $handler->handle($activityType, $detail, $user);
         }
     }
 
diff --git a/app/Activity/Tools/ActivityLogger.php b/app/Activity/Tools/ActivityLogger.php
index 3135f57a7..e8ea7c293 100644
--- a/app/Activity/Tools/ActivityLogger.php
+++ b/app/Activity/Tools/ActivityLogger.php
@@ -40,7 +40,7 @@ class ActivityLogger
 
         $this->setNotification($type);
         $this->dispatchWebhooks($type, $detail);
-        $this->notifications->handle($type, $detail);
+        $this->notifications->handle($type, $detail, user());
         Theme::dispatch(ThemeEvents::ACTIVITY_LOGGED, $type, $detail);
     }
 
diff --git a/app/Activity/Tools/EntityWatchers.php b/app/Activity/Tools/EntityWatchers.php
new file mode 100644
index 000000000..20375ef45
--- /dev/null
+++ b/app/Activity/Tools/EntityWatchers.php
@@ -0,0 +1,55 @@
+<?php
+
+namespace BookStack\Activity\Tools;
+
+use BookStack\Activity\Models\Watch;
+use BookStack\Entities\Models\BookChild;
+use BookStack\Entities\Models\Entity;
+use BookStack\Entities\Models\Page;
+use Illuminate\Database\Eloquent\Builder;
+
+class EntityWatchers
+{
+    protected array $watchers = [];
+    protected array $ignorers = [];
+
+    public function __construct(
+        protected Entity $entity,
+        protected int $watchLevel,
+    ) {
+        $this->build();
+    }
+
+    protected function build(): void
+    {
+        $watches = $this->getRelevantWatches();
+
+        // TODO - De-dupe down watches per-user across entity types
+        // so we end up with [user_id => status] values
+        // then filter to current watch level, considering ignores,
+        // then populate the class watchers/ignores with ids.
+    }
+
+    protected function getRelevantWatches(): array
+    {
+        /** @var Entity[] $entitiesInvolved */
+        $entitiesInvolved = array_filter([
+            $this->entity,
+            $this->entity instanceof BookChild ? $this->entity->book : null,
+            $this->entity instanceof Page ? $this->entity->chapter : null,
+        ]);
+
+        $query = Watch::query()->where(function (Builder $query) use ($entitiesInvolved) {
+            foreach ($entitiesInvolved as $entity) {
+                $query->orWhere(function (Builder $query) use ($entity) {
+                    $query->where('watchable_type', '=', $entity->getMorphClass())
+                        ->where('watchable_id', '=', $entity->id);
+                });
+            }
+        });
+
+        return $query->get([
+           'level', 'watchable_id', 'watchable_type', 'user_id'
+        ])->all();
+    }
+}
diff --git a/app/Activity/Tools/UserWatchOptions.php b/app/Activity/Tools/UserWatchOptions.php
index 0607d60a3..64c5f317f 100644
--- a/app/Activity/Tools/UserWatchOptions.php
+++ b/app/Activity/Tools/UserWatchOptions.php
@@ -3,20 +3,13 @@
 namespace BookStack\Activity\Tools;
 
 use BookStack\Activity\Models\Watch;
+use BookStack\Activity\WatchLevels;
 use BookStack\Entities\Models\Entity;
 use BookStack\Users\Models\User;
 use Illuminate\Database\Eloquent\Builder;
 
 class UserWatchOptions
 {
-    protected static array $levelByName = [
-        'default' => -1,
-        'ignore' => 0,
-        'new' => 1,
-        'updates' => 2,
-        'comments' => 3,
-    ];
-
     public function __construct(
         protected User $user,
     ) {
@@ -30,7 +23,7 @@ class UserWatchOptions
     public function getEntityWatchLevel(Entity $entity): string
     {
         $levelValue = $this->entityQuery($entity)->first(['level'])->level ?? -1;
-        return $this->levelValueToName($levelValue);
+        return WatchLevels::levelValueToName($levelValue);
     }
 
     public function isWatching(Entity $entity): bool
@@ -40,7 +33,7 @@ class UserWatchOptions
 
     public function updateEntityWatchLevel(Entity $entity, string $level): void
     {
-        $levelValue = $this->levelNameToValue($level);
+        $levelValue = WatchLevels::levelNameToValue($level);
         if ($levelValue < 0) {
             $this->removeForEntity($entity);
             return;
@@ -71,28 +64,4 @@ class UserWatchOptions
             ->where('watchable_type', '=', $entity->getMorphClass())
             ->where('user_id', '=', $this->user->id);
     }
-
-    /**
-     * @return string[]
-     */
-    public static function getAvailableLevelNames(): array
-    {
-        return array_keys(static::$levelByName);
-    }
-
-    protected static function levelNameToValue(string $level): int
-    {
-        return static::$levelByName[$level] ?? -1;
-    }
-
-    protected static function levelValueToName(int $level): string
-    {
-        foreach (static::$levelByName as $name => $value) {
-            if ($level === $value) {
-                return $name;
-            }
-        }
-
-        return 'default';
-    }
 }
diff --git a/app/Activity/WatchLevels.php b/app/Activity/WatchLevels.php
new file mode 100644
index 000000000..2951bc7a8
--- /dev/null
+++ b/app/Activity/WatchLevels.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace BookStack\Activity;
+
+class WatchLevels
+{
+    /**
+     * Default level, No specific option set
+     * Typically not a stored status
+     */
+    const DEFAULT = -1;
+
+    /**
+     * Ignore all notifications.
+     */
+    const IGNORE = 0;
+
+    /**
+     * Watch for new content.
+     */
+    const NEW = 1;
+
+    /**
+     * Watch for updates and new content
+     */
+    const UPDATES = 2;
+
+    /**
+     * Watch for comments, updates and new content.
+     */
+    const COMMENTS = 3;
+
+    /**
+     * Get all the possible values as an option_name => value array.
+     */
+    public static function all(): array
+    {
+        $options = [];
+        foreach ((new \ReflectionClass(static::class))->getConstants() as $name => $value) {
+            $options[strtolower($name)] = $value;
+        }
+
+        return $options;
+    }
+
+    public static function levelNameToValue(string $level): int
+    {
+        return static::all()[$level] ?? -1;
+    }
+
+    public static function levelValueToName(int $level): string
+    {
+        foreach (static::all() as $name => $value) {
+            if ($level === $value) {
+                return $name;
+            }
+        }
+
+        return 'default';
+    }
+}
diff --git a/resources/views/entities/watch-controls.blade.php b/resources/views/entities/watch-controls.blade.php
index e02db5800..adde98998 100644
--- a/resources/views/entities/watch-controls.blade.php
+++ b/resources/views/entities/watch-controls.blade.php
@@ -5,7 +5,7 @@
     <input type="hidden" name="id" value="{{ $entity->id }}">
 
     <ul refs="dropdown@menu" class="dropdown-menu xl-limited anchor-left pb-none">
-        @foreach(\BookStack\Activity\Tools\UserWatchOptions::getAvailableLevelNames() as $option)
+        @foreach(\BookStack\Activity\WatchLevels::all() as $option)
             <li>
                 <button name="level" value="{{ $option }}" class="icon-item">
                     @if($watchLevel === $option)