diff --git a/app/Actions/ActivityService.php b/app/Actions/ActivityService.php
index fb4a739cd..0b3b0f0bc 100644
--- a/app/Actions/ActivityService.php
+++ b/app/Actions/ActivityService.php
@@ -5,6 +5,7 @@ use BookStack\Auth\User;
 use BookStack\Entities\Chapter;
 use BookStack\Entities\Entity;
 use BookStack\Entities\Page;
+use BookStack\Interfaces\Loggable;
 use Illuminate\Database\Eloquent\Builder;
 use Illuminate\Database\Eloquent\Relations\Relation;
 use Illuminate\Support\Facades\Log;
@@ -30,6 +31,22 @@ class ActivityService
         $this->setNotification($type);
     }
 
+    /**
+     * Add a generic activity event to the database.
+     * @param string|Loggable $detail
+     */
+    public function add(string $type, $detail = '')
+    {
+        if ($detail instanceof Loggable) {
+            $detail = $detail->logDescriptor();
+        }
+
+        $activity = $this->newActivityForUser($type);
+        $activity->detail = $detail;
+        $activity->save();
+        $this->setNotification($type);
+    }
+
     /**
      * Get a new activity instance for the current user.
      */
diff --git a/app/Actions/ActivityType.php b/app/Actions/ActivityType.php
index 5404d884c..9b4e86a16 100644
--- a/app/Actions/ActivityType.php
+++ b/app/Actions/ActivityType.php
@@ -7,16 +7,46 @@ class ActivityType
     const PAGE_DELETE = 'page_delete';
     const PAGE_RESTORE = 'page_restore';
     const PAGE_MOVE = 'page_move';
-    const COMMENTED_ON = 'commented_on';
+
     const CHAPTER_CREATE = 'chapter_create';
     const CHAPTER_UPDATE = 'chapter_update';
     const CHAPTER_DELETE = 'chapter_delete';
     const CHAPTER_MOVE = 'chapter_move';
+
     const BOOK_CREATE = 'book_create';
     const BOOK_UPDATE = 'book_update';
     const BOOK_DELETE = 'book_delete';
     const BOOK_SORT = 'book_sort';
+
     const BOOKSHELF_CREATE = 'bookshelf_create';
     const BOOKSHELF_UPDATE = 'bookshelf_update';
     const BOOKSHELF_DELETE = 'bookshelf_delete';
+
+    const COMMENTED_ON = 'commented_on';
+    const PERMISSIONS_UPDATE = 'permissions_update';
+
+    const SETTINGS_UPDATE = 'settings_update';
+    const MAINTENANCE_ACTION_RUN = 'maintenance_action_run';
+
+    const RECYCLE_BIN_EMPTY = 'recycle_bin_empty';
+    const RECYCLE_BIN_RESTORE = 'recycle_bin_restore';
+    const RECYCLE_BIN_DESTROY = 'recycle_bin_destroy';
+
+    // TODO - Implement all below
+    const USER_CREATE = 'user_create';
+    const USER_UPDATE = 'user_update';
+    const USER_DELETE = 'user_delete';
+
+    const API_TOKEN_CREATE = 'api_token_create';
+    const API_TOKEN_UPDATE = 'api_token_update';
+    const API_TOKEN_DELETE = 'api_token_delete';
+
+    const ROLE_CREATE = 'role_create';
+    const ROLE_UPDATE = 'role_update';
+    const ROLE_DELETE = 'role_delete';
+
+    const ACCESS_PASSWORD_RESET = 'access_password_reset_request';
+    const ACCESS_PASSWORD_RESET_UPDATE = 'access_password_reset_update';
+    const ACCESS_LOGIN = 'access_login';
+    const ACCESS_FAILED_LOGIN = 'access_failed_login';
 }
\ No newline at end of file
diff --git a/app/Entities/Deletion.php b/app/Entities/Deletion.php
index 576862caa..dab44d4ad 100644
--- a/app/Entities/Deletion.php
+++ b/app/Entities/Deletion.php
@@ -1,11 +1,12 @@
 <?php namespace BookStack\Entities;
 
 use BookStack\Auth\User;
+use BookStack\Interfaces\Loggable;
 use Illuminate\Database\Eloquent\Model;
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
 use Illuminate\Database\Eloquent\Relations\MorphTo;
 
-class Deletion extends Model
+class Deletion extends Model implements Loggable
 {
 
     /**
@@ -38,4 +39,9 @@ class Deletion extends Model
         return $record;
     }
 
+    public function logDescriptor(): string
+    {
+        $deletable = $this->deletable()->first();
+        return "Deletion ({$this->id}) for {$deletable->getType()} ({$deletable->id}) {$deletable->name}";
+    }
 }
diff --git a/app/Entities/Repos/BaseRepo.php b/app/Entities/Repos/BaseRepo.php
index 7c25e4981..a9d599eb2 100644
--- a/app/Entities/Repos/BaseRepo.php
+++ b/app/Entities/Repos/BaseRepo.php
@@ -2,11 +2,12 @@
 
 namespace BookStack\Entities\Repos;
 
+use BookStack\Actions\ActivityType;
 use BookStack\Actions\TagRepo;
-use BookStack\Entities\Book;
 use BookStack\Entities\Entity;
 use BookStack\Entities\HasCoverImage;
 use BookStack\Exceptions\ImageUploadException;
+use BookStack\Facades\Activity;
 use BookStack\Uploads\ImageRepo;
 use Illuminate\Http\UploadedFile;
 use Illuminate\Support\Collection;
@@ -18,10 +19,6 @@ class BaseRepo
     protected $imageRepo;
 
 
-    /**
-     * BaseRepo constructor.
-     * @param $tagRepo
-     */
     public function __construct(TagRepo $tagRepo, ImageRepo $imageRepo)
     {
         $this->tagRepo = $tagRepo;
@@ -115,5 +112,6 @@ class BaseRepo
 
         $entity->save();
         $entity->rebuildPermissions();
+        Activity::addForEntity($entity, ActivityType::PERMISSIONS_UPDATE);
     }
 }
diff --git a/app/Http/Controllers/Api/BookshelfApiController.php b/app/Http/Controllers/Api/BookshelfApiController.php
index 4650e1dde..6792f9f45 100644
--- a/app/Http/Controllers/Api/BookshelfApiController.php
+++ b/app/Http/Controllers/Api/BookshelfApiController.php
@@ -1,7 +1,5 @@
 <?php namespace BookStack\Http\Controllers\Api;
 
-use BookStack\Actions\ActivityType;
-use BookStack\Facades\Activity;
 use BookStack\Entities\Repos\BookshelfRepo;
 use BookStack\Entities\Bookshelf;
 use Exception;
diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php
index 6a1dfcb01..3ac139249 100644
--- a/app/Http/Controllers/Controller.php
+++ b/app/Http/Controllers/Controller.php
@@ -2,21 +2,20 @@
 
 namespace BookStack\Http\Controllers;
 
+use BookStack\Facades\Activity;
+use BookStack\Interfaces\Loggable;
 use BookStack\Ownable;
 use Illuminate\Foundation\Bus\DispatchesJobs;
 use Illuminate\Foundation\Validation\ValidatesRequests;
 use Illuminate\Http\Exceptions\HttpResponseException;
-use Illuminate\Http\Request;
+use Illuminate\Http\JsonResponse;
+use Illuminate\Http\Response;
 use Illuminate\Routing\Controller as BaseController;
-use Illuminate\Validation\ValidationException;
 
 abstract class Controller extends BaseController
 {
     use DispatchesJobs, ValidatesRequests;
 
-    /**
-     * Controller constructor.
-     */
     public function __construct()
     {
         //
@@ -43,9 +42,8 @@ abstract class Controller extends BaseController
 
     /**
      * Adds the page title into the view.
-     * @param $title
      */
-    public function setPageTitle($title)
+    public function setPageTitle(string $title)
     {
         view()->share('pageTitle', $title);
     }
@@ -67,79 +65,59 @@ abstract class Controller extends BaseController
     }
 
     /**
-     * Checks for a permission.
-     * @param string $permissionName
-     * @return bool|\Illuminate\Http\RedirectResponse
+     * Checks that the current user has the given permission otherwise throw an exception.
      */
-    protected function checkPermission($permissionName)
+    protected function checkPermission(string $permission): void
     {
-        if (!user() || !user()->can($permissionName)) {
+        if (!user() || !user()->can($permission)) {
             $this->showPermissionError();
         }
-        return true;
     }
 
     /**
-     * Check the current user's permissions against an ownable item.
-     * @param $permission
-     * @param Ownable $ownable
-     * @return bool
+     * Check the current user's permissions against an ownable item otherwise throw an exception.
      */
-    protected function checkOwnablePermission($permission, Ownable $ownable)
+    protected function checkOwnablePermission(string $permission, Ownable $ownable): void
     {
-        if (userCan($permission, $ownable)) {
-            return true;
+        if (!userCan($permission, $ownable)) {
+            $this->showPermissionError();
         }
-        return $this->showPermissionError();
     }
 
     /**
-     * Check if a user has a permission or bypass if the callback is true.
-     * @param $permissionName
-     * @param $callback
-     * @return bool
+     * Check if a user has a permission or bypass the permission
+     * check if the given callback resolves true.
      */
-    protected function checkPermissionOr($permissionName, $callback)
+    protected function checkPermissionOr(string $permission, callable $callback): void
     {
-        $callbackResult = $callback();
-        if ($callbackResult === false) {
-            $this->checkPermission($permissionName);
+        if ($callback() !== true) {
+            $this->checkPermission($permission);
         }
-        return true;
     }
 
     /**
      * Check if the current user has a permission or bypass if the provided user
      * id matches the current user.
-     * @param string $permissionName
-     * @param int $userId
-     * @return bool
      */
-    protected function checkPermissionOrCurrentUser(string $permissionName, int $userId)
+    protected function checkPermissionOrCurrentUser(string $permission, int $userId): void
     {
-        return $this->checkPermissionOr($permissionName, function () use ($userId) {
+        $this->checkPermissionOr($permission, function () use ($userId) {
             return $userId === user()->id;
         });
     }
 
     /**
      * Send back a json error message.
-     * @param string $messageText
-     * @param int $statusCode
-     * @return mixed
      */
-    protected function jsonError($messageText = "", $statusCode = 500)
+    protected function jsonError(string $messageText = "", int $statusCode = 500): JsonResponse
     {
         return response()->json(['message' => $messageText, 'status' => 'error'], $statusCode);
     }
 
     /**
      * Create a response that forces a download in the browser.
-     * @param string $content
-     * @param string $fileName
-     * @return \Illuminate\Http\Response
      */
-    protected function downloadResponse(string $content, string $fileName)
+    protected function downloadResponse(string $content, string $fileName): Response
     {
         return response()->make($content, 200, [
             'Content-Type'        => 'application/octet-stream',
@@ -149,31 +127,37 @@ abstract class Controller extends BaseController
 
     /**
      * Show a positive, successful notification to the user on next view load.
-     * @param string $message
      */
-    protected function showSuccessNotification(string $message)
+    protected function showSuccessNotification(string $message): void
     {
         session()->flash('success', $message);
     }
 
     /**
      * Show a warning notification to the user on next view load.
-     * @param string $message
      */
-    protected function showWarningNotification(string $message)
+    protected function showWarningNotification(string $message): void
     {
         session()->flash('warning', $message);
     }
 
     /**
      * Show an error notification to the user on next view load.
-     * @param string $message
      */
-    protected function showErrorNotification(string $message)
+    protected function showErrorNotification(string $message): void
     {
         session()->flash('error', $message);
     }
 
+    /**
+     * Log an activity in the system.
+     * @param string|Loggable
+     */
+    protected function logActivity(string $type, $detail = ''): void
+    {
+        Activity::add($type, $detail);
+    }
+
     /**
      * Get the validation rules for image files.
      */
diff --git a/app/Http/Controllers/MaintenanceController.php b/app/Http/Controllers/MaintenanceController.php
index 0d6265f90..a638b69cd 100644
--- a/app/Http/Controllers/MaintenanceController.php
+++ b/app/Http/Controllers/MaintenanceController.php
@@ -2,6 +2,7 @@
 
 namespace BookStack\Http\Controllers;
 
+use BookStack\Actions\ActivityType;
 use BookStack\Entities\Managers\TrashCan;
 use BookStack\Notifications\TestEmail;
 use BookStack\Uploads\ImageService;
@@ -35,6 +36,7 @@ class MaintenanceController extends Controller
     public function cleanupImages(Request $request, ImageService $imageService)
     {
         $this->checkPermission('settings-manage');
+        $this->logActivity(ActivityType::MAINTENANCE_ACTION_RUN, 'cleanup-images');
 
         $checkRevisions = !($request->get('ignore_revisions', 'false') === 'true');
         $dryRun = !($request->has('confirm'));
@@ -61,6 +63,7 @@ class MaintenanceController extends Controller
     public function sendTestEmail()
     {
         $this->checkPermission('settings-manage');
+        $this->logActivity(ActivityType::MAINTENANCE_ACTION_RUN, 'send-test-email');
 
         try {
             user()->notify(new TestEmail());
diff --git a/app/Http/Controllers/RecycleBinController.php b/app/Http/Controllers/RecycleBinController.php
index 459dbb39d..57038046a 100644
--- a/app/Http/Controllers/RecycleBinController.php
+++ b/app/Http/Controllers/RecycleBinController.php
@@ -1,5 +1,6 @@
 <?php namespace BookStack\Http\Controllers;
 
+use BookStack\Actions\ActivityType;
 use BookStack\Entities\Deletion;
 use BookStack\Entities\Managers\TrashCan;
 
@@ -56,6 +57,7 @@ class RecycleBinController extends Controller
     {
         /** @var Deletion $deletion */
         $deletion = Deletion::query()->findOrFail($id);
+        $this->logActivity(ActivityType::RECYCLE_BIN_RESTORE, $deletion);
         $restoreCount = (new TrashCan())->restoreFromDeletion($deletion);
 
         $this->showSuccessNotification(trans('settings.recycle_bin_restore_notification', ['count' => $restoreCount]));
@@ -85,6 +87,7 @@ class RecycleBinController extends Controller
         $deletion = Deletion::query()->findOrFail($id);
         $deleteCount = (new TrashCan())->destroyFromDeletion($deletion);
 
+        $this->logActivity(ActivityType::RECYCLE_BIN_DESTROY, $deletion);
         $this->showSuccessNotification(trans('settings.recycle_bin_destroy_notification', ['count' => $deleteCount]));
         return redirect($this->recycleBinBaseUrl);
     }
@@ -97,6 +100,7 @@ class RecycleBinController extends Controller
     {
         $deleteCount = (new TrashCan())->empty();
 
+        $this->logActivity(ActivityType::RECYCLE_BIN_EMPTY);
         $this->showSuccessNotification(trans('settings.recycle_bin_destroy_notification', ['count' => $deleteCount]));
         return redirect($this->recycleBinBaseUrl);
     }
diff --git a/app/Http/Controllers/SettingController.php b/app/Http/Controllers/SettingController.php
index 50d91d388..92435f924 100644
--- a/app/Http/Controllers/SettingController.php
+++ b/app/Http/Controllers/SettingController.php
@@ -1,5 +1,6 @@
 <?php namespace BookStack\Http\Controllers;
 
+use BookStack\Actions\ActivityType;
 use BookStack\Auth\User;
 use BookStack\Uploads\ImageRepo;
 use Illuminate\Http\Request;
@@ -47,10 +48,10 @@ class SettingController extends Controller
 
         // Cycles through posted settings and update them
         foreach ($request->all() as $name => $value) {
+            $key = str_replace('setting-', '', trim($name));
             if (strpos($name, 'setting-') !== 0) {
                 continue;
             }
-            $key = str_replace('setting-', '', trim($name));
             setting()->put($key, $value);
         }
 
@@ -68,8 +69,10 @@ class SettingController extends Controller
             setting()->remove('app-logo');
         }
 
+        $section = $request->get('section', '');
+        $this->logActivity(ActivityType::SETTINGS_UPDATE, $section);
         $this->showSuccessNotification(trans('settings.settings_save_success'));
-        $redirectLocation = '/settings#' . $request->get('section', '');
+        $redirectLocation = '/settings#' . $section;
         return redirect(rtrim($redirectLocation, '#'));
     }
 }
diff --git a/app/Interfaces/Loggable.php b/app/Interfaces/Loggable.php
new file mode 100644
index 000000000..33e1d7c95
--- /dev/null
+++ b/app/Interfaces/Loggable.php
@@ -0,0 +1,11 @@
+<?php
+
+namespace BookStack\Interfaces;
+
+interface Loggable
+{
+    /**
+     * Get the string descriptor for this item.
+     */
+    public function logDescriptor(): string;
+}
\ No newline at end of file
diff --git a/resources/lang/en/activities.php b/resources/lang/en/activities.php
index 4cac54b2a..fe937b061 100644
--- a/resources/lang/en/activities.php
+++ b/resources/lang/en/activities.php
@@ -45,4 +45,5 @@ return [
 
     // Other
     'commented_on'                => 'commented on',
+    'permissions_update'          => 'updated permissions',
 ];