diff --git a/src/app/Console/Commands/Domain/SuspendCommand.php b/src/app/Console/Commands/Domain/SuspendCommand.php
--- a/src/app/Console/Commands/Domain/SuspendCommand.php
+++ b/src/app/Console/Commands/Domain/SuspendCommand.php
@@ -11,7 +11,7 @@
      *
      * @var string
      */
-    protected $signature = 'domain:suspend {domain}';
+    protected $signature = 'domain:suspend {domain} {--comment=}';
 
     /**
      * The console command description.
@@ -35,5 +35,7 @@
         }
 
         $domain->suspend();
+
+        \App\EventLog::createFor($domain, \App\EventLog::TYPE_SUSPENDED, $this->option('comment'));
     }
 }
diff --git a/src/app/Console/Commands/Domain/UnsuspendCommand.php b/src/app/Console/Commands/Domain/UnsuspendCommand.php
--- a/src/app/Console/Commands/Domain/UnsuspendCommand.php
+++ b/src/app/Console/Commands/Domain/UnsuspendCommand.php
@@ -11,7 +11,7 @@
      *
      * @var string
      */
-    protected $signature = 'domain:unsuspend {domain}';
+    protected $signature = 'domain:unsuspend {domain} {--comment=}';
 
     /**
      * The console command description.
@@ -35,5 +35,7 @@
         }
 
         $domain->unsuspend();
+
+        \App\EventLog::createFor($domain, \App\EventLog::TYPE_UNSUSPENDED, $this->option('comment'));
     }
 }
diff --git a/src/app/Console/Commands/Group/SuspendCommand.php b/src/app/Console/Commands/Group/SuspendCommand.php
new file mode 100644
--- /dev/null
+++ b/src/app/Console/Commands/Group/SuspendCommand.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace App\Console\Commands\Group;
+
+use App\Console\Command;
+
+class SuspendCommand extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'group:suspend {group} {--comment=}';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Suspend a distribution list (group)';
+
+    /**
+     * Execute the console command.
+     *
+     * @return mixed
+     */
+    public function handle()
+    {
+        $group = $this->getGroup($this->argument('group'));
+
+        if (!$group) {
+            $this->error("Group not found.");
+            return 1;
+        }
+
+        $group->suspend();
+
+        \App\EventLog::createFor($group, \App\EventLog::TYPE_SUSPENDED, $this->option('comment'));
+    }
+}
diff --git a/src/app/Console/Commands/Group/UnsuspendCommand.php b/src/app/Console/Commands/Group/UnsuspendCommand.php
new file mode 100644
--- /dev/null
+++ b/src/app/Console/Commands/Group/UnsuspendCommand.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace App\Console\Commands\Group;
+
+use App\Console\Command;
+
+class UnsuspendCommand extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'group:unsuspend {group} {--comment=}';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Remove a group suspension';
+
+    /**
+     * Execute the console command.
+     *
+     * @return mixed
+     */
+    public function handle()
+    {
+        $group = $this->getGroup($this->argument('group'));
+
+        if (!$group) {
+            $this->error("Group not found.");
+            return 1;
+        }
+
+        $group->unsuspend();
+
+        \App\EventLog::createFor($group, \App\EventLog::TYPE_UNSUSPENDED, $this->option('comment'));
+    }
+}
diff --git a/src/app/Console/Commands/User/SuspendCommand.php b/src/app/Console/Commands/User/SuspendCommand.php
--- a/src/app/Console/Commands/User/SuspendCommand.php
+++ b/src/app/Console/Commands/User/SuspendCommand.php
@@ -11,7 +11,7 @@
      *
      * @var string
      */
-    protected $signature = 'user:suspend {user}';
+    protected $signature = 'user:suspend {user} {--comment=}';
 
     /**
      * The console command description.
@@ -35,5 +35,7 @@
         }
 
         $user->suspend();
+
+        \App\EventLog::createFor($user, \App\EventLog::TYPE_SUSPENDED, $this->option('comment'));
     }
 }
diff --git a/src/app/Console/Commands/User/UnsuspendCommand.php b/src/app/Console/Commands/User/UnsuspendCommand.php
--- a/src/app/Console/Commands/User/UnsuspendCommand.php
+++ b/src/app/Console/Commands/User/UnsuspendCommand.php
@@ -11,7 +11,7 @@
      *
      * @var string
      */
-    protected $signature = 'user:unsuspend {user}';
+    protected $signature = 'user:unsuspend {user} {--comment=}';
 
     /**
      * The console command description.
@@ -35,5 +35,7 @@
         }
 
         $user->unsuspend();
+
+        \App\EventLog::createFor($user, \App\EventLog::TYPE_UNSUSPENDED, $this->option('comment'));
     }
 }
diff --git a/src/app/EventLog.php b/src/app/EventLog.php
new file mode 100644
--- /dev/null
+++ b/src/app/EventLog.php
@@ -0,0 +1,131 @@
+<?php
+
+namespace App;
+
+use App\Traits\UuidStrKeyTrait;
+use Dyrynda\Database\Support\NullableFields;
+use Illuminate\Database\Eloquent\Model;
+
+/**
+ * The eloquent definition of an EventLog record.
+ *
+ * @property ?string $comment     Optional event description
+ * @property ?array  $data        Optional event data
+ * @property string  $id          Log record identifier
+ * @property string  $object_id   Object identifier
+ * @property string  $object_type Object type (class)
+ * @property int     $type        Event type (0-255)
+ * @property ?string $user_email  Acting user email
+ */
+class EventLog extends Model
+{
+    use NullableFields;
+    use UuidStrKeyTrait;
+
+    public const TYPE_SUSPENDED = 1;
+    public const TYPE_UNSUSPENDED = 2;
+    public const TYPE_COMMENT = 3;
+
+    /** @var array<int, string> The attributes that are mass assignable */
+    protected $fillable = [
+        'comment',
+        // extra event info (json)
+        'data',
+        'type',
+        // user, domain, etc.
+        'object_id',
+        'object_type',
+        // actor, if any
+        'user_email',
+    ];
+
+    /** @var array<string, string> Casts properties as type */
+    protected $casts = [
+        'created_at' => 'datetime:Y-m-d H:i:s',
+        'data' => 'array',
+        'type' => 'integer',
+    ];
+
+    /** @var array<int, string> The attributes that can be not set */
+    protected $nullable = ['comment', 'data', 'user_email'];
+
+    /** @var string Database table name */
+    protected $table = 'eventlog';
+
+    /** @var bool Indicates if the model should be timestamped. */
+    public $timestamps = false;
+
+
+    /**
+     * Create an eventlog object for a specified object.
+     *
+     * @param object  $object  Object (User, Domain, etc.)
+     * @param int     $type    Event type (use one of EventLog::TYPE_* consts)
+     * @param ?string $comment Event description
+     * @param ?array  $data    Extra information
+     *
+     * @return EventLog
+     */
+    public static function createFor($object, int $type, string $comment = null, array $data = null): EventLog
+    {
+        $event = self::create([
+                'object_id' => $object->id,
+                'object_type' => get_class($object),
+                'type' => $type,
+                'comment' => $comment,
+                'data' => $data,
+        ]);
+
+        return $event;
+    }
+
+    /**
+     * Principally an object such as Domain, User, Group.
+     * Note that it may be trashed (soft-deleted).
+     *
+     * @return mixed
+     */
+    public function object()
+    {
+        return $this->morphTo()->withTrashed(); // @phpstan-ignore-line
+    }
+
+    /**
+     * Get an event type name.
+     *
+     * @return ?string Event type name
+     */
+    public function eventName(): ?string
+    {
+        switch ($this->type) {
+        case self::TYPE_SUSPENDED:
+            return \trans('app.event-suspended');
+        case self::TYPE_UNSUSPENDED:
+            return \trans('app.event-unsuspended');
+        case self::TYPE_COMMENT:
+            return \trans('app.event-comment');
+        default:
+            return null;
+        }
+    }
+
+    /**
+     * Event type mutator
+     *
+     * @throws \Exception
+     */
+    public function setTypeAttribute($type)
+    {
+        if (!is_numeric($type)) {
+            throw new \Exception("Expecting an event type to be numeric");
+        }
+
+        $type = (int) $type;
+
+        if ($type < 0 || $type > 255) {
+            throw new \Exception("Expecting an event type between 0 and 255");
+        }
+
+        $this->attributes['type'] = $type;
+    }
+}
diff --git a/src/app/Http/Controllers/API/V4/Admin/DomainsController.php b/src/app/Http/Controllers/API/V4/Admin/DomainsController.php
--- a/src/app/Http/Controllers/API/V4/Admin/DomainsController.php
+++ b/src/app/Http/Controllers/API/V4/Admin/DomainsController.php
@@ -3,8 +3,10 @@
 namespace App\Http\Controllers\API\V4\Admin;
 
 use App\Domain;
+use App\EventLog;
 use App\User;
 use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Validator;
 
 class DomainsController extends \App\Http\Controllers\API\V4\DomainsController
 {
@@ -94,8 +96,16 @@
             return $this->errorResponse(404);
         }
 
+        $v = Validator::make($request->all(), ['comment' => 'nullable|string|max:1024']);
+
+        if ($v->fails()) {
+            return response()->json(['status' => 'error', 'errors' => $v->errors()], 422);
+        }
+
         $domain->suspend();
 
+        EventLog::createFor($domain, EventLog::TYPE_SUSPENDED, $request->comment);
+
         return response()->json([
                 'status' => 'success',
                 'message' => self::trans('app.domain-suspend-success'),
@@ -118,8 +128,16 @@
             return $this->errorResponse(404);
         }
 
+        $v = Validator::make($request->all(), ['comment' => 'nullable|string|max:1024']);
+
+        if ($v->fails()) {
+            return response()->json(['status' => 'error', 'errors' => $v->errors()], 422);
+        }
+
         $domain->unsuspend();
 
+        EventLog::createFor($domain, EventLog::TYPE_UNSUSPENDED, $request->comment);
+
         return response()->json([
                 'status' => 'success',
                 'message' => self::trans('app.domain-unsuspend-success'),
diff --git a/src/app/Http/Controllers/API/V4/Admin/EventLogController.php b/src/app/Http/Controllers/API/V4/Admin/EventLogController.php
new file mode 100644
--- /dev/null
+++ b/src/app/Http/Controllers/API/V4/Admin/EventLogController.php
@@ -0,0 +1,67 @@
+<?php
+
+namespace App\Http\Controllers\API\V4\Admin;
+
+use App\EventLog;
+use App\Http\Controllers\Controller;
+use Illuminate\Http\Request;
+
+class EventLogController extends Controller
+{
+    /**
+     * Listing of eventlog entries.
+     *
+     * @param \Illuminate\Http\Request $request     HTTP Request
+     * @param string                   $object_type Object type
+     * @param string                   $object_id   Object id
+     *
+     * @return \Illuminate\Http\JsonResponse
+     */
+    public function index(Request $request, string $object_type, string $object_id)
+    {
+        $object_type = "App\\" . ucfirst($object_type);
+
+        if (!class_exists($object_type)) {
+            return $this->errorResponse(404);
+        }
+
+        $object = (new $object_type)->find($object_id);
+
+        if (!$this->checkTenant($object)) {
+            return $this->errorResponse(404);
+        }
+
+        $page = intval($request->input('page')) ?: 1;
+        $pageSize = 20;
+        $hasMore = false;
+
+        $result = EventLog::where('object_id', $object_id)
+            ->where('object_type', $object_type)
+            ->orderBy('created_at', 'desc')
+            ->limit($pageSize + 1)
+            ->offset($pageSize * ($page - 1))
+            ->get();
+
+        if (count($result) > $pageSize) {
+            $result->pop();
+            $hasMore = true;
+        }
+
+        $result = $result->map(function ($event) {
+            return [
+                'id' => $event->id,
+                'comment' => $event->comment,
+                'createdAt' => $event->created_at->toDateTimeString(),
+                'event' => $event->eventName(),
+                'data' => $event->data,
+                'user' => $event->user_email,
+            ];
+        });
+
+        return response()->json([
+            'list' => $result,
+            'count' => count($result),
+            'hasMore' => $hasMore,
+        ]);
+    }
+}
diff --git a/src/app/Http/Controllers/API/V4/Admin/GroupsController.php b/src/app/Http/Controllers/API/V4/Admin/GroupsController.php
--- a/src/app/Http/Controllers/API/V4/Admin/GroupsController.php
+++ b/src/app/Http/Controllers/API/V4/Admin/GroupsController.php
@@ -2,9 +2,11 @@
 
 namespace App\Http\Controllers\API\V4\Admin;
 
+use App\EventLog;
 use App\Group;
 use App\User;
 use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Validator;
 
 class GroupsController extends \App\Http\Controllers\API\V4\GroupsController
 {
@@ -73,8 +75,16 @@
             return $this->errorResponse(404);
         }
 
+        $v = Validator::make($request->all(), ['comment' => 'nullable|string|max:1024']);
+
+        if ($v->fails()) {
+            return response()->json(['status' => 'error', 'errors' => $v->errors()], 422);
+        }
+
         $group->suspend();
 
+        EventLog::createFor($group, EventLog::TYPE_SUSPENDED, $request->comment);
+
         return response()->json([
                 'status' => 'success',
                 'message' => self::trans('app.distlist-suspend-success'),
@@ -97,8 +107,16 @@
             return $this->errorResponse(404);
         }
 
+        $v = Validator::make($request->all(), ['comment' => 'nullable|string|max:1024']);
+
+        if ($v->fails()) {
+            return response()->json(['status' => 'error', 'errors' => $v->errors()], 422);
+        }
+
         $group->unsuspend();
 
+        EventLog::createFor($group, EventLog::TYPE_UNSUSPENDED, $request->comment);
+
         return response()->json([
                 'status' => 'success',
                 'message' => self::trans('app.distlist-unsuspend-success'),
diff --git a/src/app/Http/Controllers/API/V4/Admin/UsersController.php b/src/app/Http/Controllers/API/V4/Admin/UsersController.php
--- a/src/app/Http/Controllers/API/V4/Admin/UsersController.php
+++ b/src/app/Http/Controllers/API/V4/Admin/UsersController.php
@@ -3,6 +3,7 @@
 namespace App\Http\Controllers\API\V4\Admin;
 
 use App\Domain;
+use App\EventLog;
 use App\Sku;
 use App\User;
 use App\Wallet;
@@ -321,8 +322,16 @@
             return $this->errorResponse(403);
         }
 
+        $v = Validator::make($request->all(), ['comment' => 'nullable|string|max:1024']);
+
+        if ($v->fails()) {
+            return response()->json(['status' => 'error', 'errors' => $v->errors()], 422);
+        }
+
         $user->suspend();
 
+        EventLog::createFor($user, EventLog::TYPE_SUSPENDED, $request->comment);
+
         return response()->json([
                 'status' => 'success',
                 'message' => self::trans('app.user-suspend-success'),
@@ -349,8 +358,16 @@
             return $this->errorResponse(403);
         }
 
+        $v = Validator::make($request->all(), ['comment' => 'nullable|string|max:1024']);
+
+        if ($v->fails()) {
+            return response()->json(['status' => 'error', 'errors' => $v->errors()], 422);
+        }
+
         $user->unsuspend();
 
+        EventLog::createFor($user, EventLog::TYPE_UNSUSPENDED, $request->comment);
+
         return response()->json([
                 'status' => 'success',
                 'message' => self::trans('app.user-unsuspend-success'),
diff --git a/src/app/Http/Controllers/API/V4/Reseller/EventLogController.php b/src/app/Http/Controllers/API/V4/Reseller/EventLogController.php
new file mode 100644
--- /dev/null
+++ b/src/app/Http/Controllers/API/V4/Reseller/EventLogController.php
@@ -0,0 +1,7 @@
+<?php
+
+namespace App\Http\Controllers\API\V4\Reseller;
+
+class EventLogController extends \App\Http\Controllers\API\V4\Admin\EventLogController
+{
+}
diff --git a/src/app/Jobs/User/CreateJob.php b/src/app/Jobs/User/CreateJob.php
--- a/src/app/Jobs/User/CreateJob.php
+++ b/src/app/Jobs/User/CreateJob.php
@@ -76,6 +76,8 @@
             $code = \Artisan::call("user:abuse-check {$this->userId}");
             if ($code == 2) {
                 \Log::info("Suspending user due to suspected abuse: {$this->userId} {$user->email}");
+                \App\EventLog::createFor($user, \App\EventLog::TYPE_SUSPENDED, "Suspected spammer");
+
                 $user->status |= \App\User::STATUS_SUSPENDED;
             }
         }
diff --git a/src/app/Observers/DomainObserver.php b/src/app/Observers/DomainObserver.php
--- a/src/app/Observers/DomainObserver.php
+++ b/src/app/Observers/DomainObserver.php
@@ -45,6 +45,9 @@
     public function deleted(Domain $domain)
     {
         if ($domain->isForceDeleting()) {
+            // Remove EventLog records
+            \App\EventLog::where('object_id', $domain->id)->where('object_type', Domain::class)->delete();
+
             return;
         }
 
diff --git a/src/app/Observers/EventLogObserver.php b/src/app/Observers/EventLogObserver.php
new file mode 100644
--- /dev/null
+++ b/src/app/Observers/EventLogObserver.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace App\Observers;
+
+use App\EventLog;
+
+class EventLogObserver
+{
+    /**
+     * Ensure the event entry ID is a custom ID (uuid).
+     *
+     * @param \App\EventLog $eventlog The EventLog object
+     */
+    public function creating(EventLog $eventlog): void
+    {
+        if (!isset($eventlog->user_email)) {
+            $eventlog->user_email = \App\Utils::userEmailOrNull();
+        }
+
+        if (!isset($eventlog->type)) {
+            throw new \Exception("Unset event type");
+        }
+    }
+}
diff --git a/src/app/Observers/GroupObserver.php b/src/app/Observers/GroupObserver.php
--- a/src/app/Observers/GroupObserver.php
+++ b/src/app/Observers/GroupObserver.php
@@ -45,6 +45,9 @@
     public function deleted(Group $group)
     {
         if ($group->isForceDeleting()) {
+            // Remove EventLog records
+            \App\EventLog::where('object_id', $group->id)->where('object_type', Group::class)->delete();
+
             return;
         }
 
diff --git a/src/app/Observers/UserObserver.php b/src/app/Observers/UserObserver.php
--- a/src/app/Observers/UserObserver.php
+++ b/src/app/Observers/UserObserver.php
@@ -224,7 +224,7 @@
     }
 
     /**
-     * Remove entitleables/transactions related to the user (in user's wallets)
+     * Remove entities related to the user (in user's wallets), entitlements, transactions, etc.
      *
      * @param \App\User $user  The user
      * @param bool      $force Force-delete mode
@@ -261,6 +261,9 @@
             \App\Transaction::where('object_type', Wallet::class)
                 ->whereIn('object_id', $wallets)
                 ->delete();
+
+            // Remove EventLog records
+            \App\EventLog::where('object_id', $user->id)->where('object_type', User::class)->delete();
         }
 
         // regardless of force delete, we're always purging whitelists... just in case
diff --git a/src/app/Providers/AppServiceProvider.php b/src/app/Providers/AppServiceProvider.php
--- a/src/app/Providers/AppServiceProvider.php
+++ b/src/app/Providers/AppServiceProvider.php
@@ -65,6 +65,7 @@
     {
         \App\Domain::observe(\App\Observers\DomainObserver::class);
         \App\Entitlement::observe(\App\Observers\EntitlementObserver::class);
+        \App\EventLog::observe(\App\Observers\EventLogObserver::class);
         \App\Group::observe(\App\Observers\GroupObserver::class);
         \App\GroupSetting::observe(\App\Observers\GroupSettingObserver::class);
         \App\Meet\Room::observe(\App\Observers\Meet\RoomObserver::class);
diff --git a/src/database/migrations/2023_06_06_100000_create_eventlog_table.php b/src/database/migrations/2023_06_06_100000_create_eventlog_table.php
new file mode 100644
--- /dev/null
+++ b/src/database/migrations/2023_06_06_100000_create_eventlog_table.php
@@ -0,0 +1,43 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::create(
+            'eventlog',
+            function (Blueprint $table) {
+                $table->string('id', 36)->primary();
+                $table->string('object_id', 36);
+                $table->string('object_type', 36);
+                $table->tinyInteger('type')->unsigned();
+                $table->string('user_email')->nullable();
+                $table->string('comment', 1024)->nullable();
+                $table->text('data')->nullable(); // json
+                $table->timestamp('created_at')->useCurrent();
+
+                $table->index(['object_id', 'object_type', 'type']);
+                $table->index('created_at');
+            }
+        );
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::dropIfExists('eventlog');
+    }
+};
diff --git a/src/resources/lang/en/app.php b/src/resources/lang/en/app.php
--- a/src/resources/lang/en/app.php
+++ b/src/resources/lang/en/app.php
@@ -23,6 +23,10 @@
     'companion-create-success' => 'Companion app has been created.',
     'companion-delete-success' => 'Companion app has been removed.',
 
+    'event-suspended' => 'Suspended',
+    'event-unsuspended' => 'Unsuspended',
+    'event-comment' => 'Commented',
+
     'mandate-delete-success' => 'The auto-payment has been removed.',
     'mandate-update-success' => 'The auto-payment has been updated.',
 
diff --git a/src/resources/lang/en/ui.php b/src/resources/lang/en/ui.php
--- a/src/resources/lang/en/ui.php
+++ b/src/resources/lang/en/ui.php
@@ -43,6 +43,12 @@
         'verify' => "Verify",
     ],
 
+    'collection' => [
+        'create' => "Create collection",
+        'new' => "New Collection",
+        'name' => "Name",
+    ],
+
     'companion' => [
         'title' => "Companion Apps",
         'companion' => "Companion App",
@@ -170,12 +176,6 @@
             . "to the file via a unique link.",
     ],
 
-    'collection' => [
-        'create' => "Create collection",
-        'new' => "New Collection",
-        'name' => "Name",
-    ],
-
     'form' => [
         'acl' => "Access rights",
         'acl-full' => "All",
@@ -185,6 +185,7 @@
         'anyone' => "Anyone",
         'code' => "Confirmation Code",
         'config' => "Configuration",
+        'comment' => "Comment",
         'companion' => "Companion App",
         'date' => "Date",
         'description' => "Description",
@@ -198,8 +199,10 @@
         'general' => "General",
         'geolocation' => "Your current location: {location}",
         'lastname' => "Last Name",
+        'less' => "Less",
         'name' => "Name",
         'months' => "months",
+        'more' => "More",
         'none' => "none",
         'norestrictions' => "No restrictions",
         'or' => "or",
@@ -245,6 +248,12 @@
         'it' => "Italian",
     ],
 
+    'log' => [
+        'event' => "Event",
+        'list-none' => "There's no events in the log",
+        'history' => "History",
+    ],
+
     'login' => [
         '2fa' => "Second factor code",
         '2fa_desc' => "Second factor code is optional for users with no 2-Factor Authentication setup.",
diff --git a/src/resources/themes/app.scss b/src/resources/themes/app.scss
--- a/src/resources/themes/app.scss
+++ b/src/resources/themes/app.scss
@@ -205,6 +205,31 @@
             width: 50px;
         }
     }
+
+    &.eventlog {
+        .details,
+        .btn-less {
+            display: none;
+        }
+
+        tr.open {
+            .btn-more {
+                display: none;
+            }
+
+            .details {
+                display: block;
+            }
+
+            .btn-less {
+                display: initial;
+            }
+        }
+
+        td.description {
+            width: 98%;
+        }
+    }
 }
 
 .table > :not(:first-child) {
diff --git a/src/resources/vue/Admin/Distlist.vue b/src/resources/vue/Admin/Distlist.vue
--- a/src/resources/vue/Admin/Distlist.vue
+++ b/src/resources/vue/Admin/Distlist.vue
@@ -1,6 +1,6 @@
 <template>
-    <div v-if="list.id" class="container">
-        <div class="card" id="distlist-info">
+    <div class="container">
+        <div v-if="list.id" class="card" id="distlist-info">
             <div class="card-body">
                 <div class="card-title">{{ list.email }}</div>
                 <div class="card-text">
@@ -37,18 +37,15 @@
                         </div>
                     </form>
                     <div class="mt-2 buttons">
-                        <button v-if="!list.isSuspended" id="button-suspend" class="btn btn-warning" type="button" @click="suspendList">
-                            {{ $t('btn.suspend') }}
-                        </button>
-                        <button v-if="list.isSuspended" id="button-unsuspend" class="btn btn-warning" type="button" @click="unsuspendList">
-                            {{ $t('btn.unsuspend') }}
-                        </button>
+                        <btn :id="`button-${suspendAction}`" class="btn-outline-primary" @click="setSuspendState">
+                            {{ $t(`btn.${suspendAction}`) }}
+                        </btn>
                     </div>
                 </div>
             </div>
         </div>
-        <tabs class="mt-3" :tabs="['form.settings']"></tabs>
-        <div class="tab-content">
+        <tabs class="mt-3" :tabs="['form.settings', 'log.history']" ref="tabs"></tabs>
+        <div v-if="list.id" class="tab-content">
             <div class="tab-pane show active" id="settings" role="tabpanel" aria-labelledby="tab-settings">
                 <div class="card-body">
                     <div class="card-text">
@@ -65,15 +62,38 @@
                     </div>
                 </div>
             </div>
+            <div class="tab-pane" id="history" role="tabpanel" aria-labelledby="tab-history">
+                <div class="card-body">
+                    <event-log v-if="loadEventLog" :object-id="list.id" object-type="group" ref="eventLog" class="card-text mb-0"></event-log>
+                </div>
+            </div>
         </div>
+
+        <modal-dialog id="suspend-dialog" ref="suspendDialog" :title="$t(`btn.${suspendAction}`)" @click="submitSuspend()" :buttons="['submit']">
+            <textarea v-model="comment" name="comment" class="form-control" :placeholder="$t('form.comment')" rows="3"></textarea>
+        </modal-dialog>
     </div>
 </template>
 
 <script>
+    import EventLog from '../Widgets/EventLog'
+    import ModalDialog from '../Widgets/ModalDialog'
+
     export default {
+        components: {
+            EventLog,
+            ModalDialog
+        },
         data() {
             return {
-                list: { members: [], config: {} }
+                comment: '',
+                list: { members: [], config: {} },
+                loadEventLog: false
+            }
+        },
+        computed: {
+            suspendAction() {
+                return this.list.isSuspended ? 'unsuspend' : 'suspend'
             }
         },
         created() {
@@ -83,22 +103,29 @@
                 })
                 .catch(this.$root.errorHandler)
         },
+        mounted() {
+            this.$refs.tabs.clickHandler('history', () => { this.loadEventLog = true })
+        },
         methods: {
-            suspendList() {
-                axios.post('/api/v4/groups/' + this.list.id + '/suspend')
-                    .then(response => {
-                        if (response.data.status == 'success') {
-                            this.$toast.success(response.data.message)
-                            this.list = Object.assign({}, this.list, { isSuspended: true })
-                        }
-                    })
+            setSuspendState() {
+                this.$root.clearFormValidation($('#suspend-dialog'))
+                this.$refs.suspendDialog.show()
             },
-            unsuspendList() {
-                axios.post('/api/v4/groups/' + this.list.id + '/unsuspend')
+            submitSuspend() {
+                const post = { comment: this.comment }
+
+                axios.post(`/api/v4/groups/${this.list.id}/${this.suspendAction}`, post)
                     .then(response => {
                         if (response.data.status == 'success') {
                             this.$toast.success(response.data.message)
-                            this.list = Object.assign({}, this.list, { isSuspended: false })
+                            this.list = Object.assign({}, this.list, { isSuspended: !this.list.isSuspended })
+
+                            this.$refs.suspendDialog.hide()
+                            this.comment = ''
+
+                            if (this.loadEventLog) {
+                                this.$refs.eventLog.loadLog({ reset: true })
+                            }
                         }
                     })
             }
diff --git a/src/resources/vue/Admin/Domain.vue b/src/resources/vue/Admin/Domain.vue
--- a/src/resources/vue/Admin/Domain.vue
+++ b/src/resources/vue/Admin/Domain.vue
@@ -1,5 +1,5 @@
 <template>
-    <div v-if="domain" class="container">
+    <div class="container">
         <div class="card" id="domain-info">
             <div class="card-body">
                 <div class="card-title">{{ domain.namespace }}</div>
@@ -25,19 +25,16 @@
                         </div>
                     </form>
                     <div class="mt-2 buttons">
-                        <btn v-if="!domain.isSuspended" id="button-suspend" class="btn-warning" @click="suspendDomain">
-                            {{ $t('btn.suspend') }}
-                        </btn>
-                        <btn v-if="domain.isSuspended" id="button-unsuspend" class="btn-warning" @click="unsuspendDomain">
-                            {{ $t('btn.unsuspend') }}
+                        <btn :id="`button-${suspendAction}`" class="btn-outline-primary" @click="setSuspendState">
+                            {{ $t(`btn.${suspendAction}`) }}
                         </btn>
                     </div>
                 </div>
             </div>
         </div>
-        <tabs class="mt-3" :tabs="['form.config', 'form.settings']"></tabs>
+        <tabs class="mt-3" :tabs="['form.config', 'form.settings', 'log.history']" ref="tabs"></tabs>
         <div class="tab-content">
-            <div class="tab-pane show active" id="config" role="tabpanel" aria-labelledby="tab-config">
+            <div v-if="domain.id" class="tab-pane show active" id="config" role="tabpanel" aria-labelledby="tab-config">
                 <div class="card-body">
                     <div class="card-text">
                         <p>{{ $t('domain.dns-confirm') }}</p>
@@ -47,7 +44,7 @@
                     </div>
                 </div>
             </div>
-            <div class="tab-pane" id="settings" role="tabpanel" aria-labelledby="tab-settings">
+            <div v-if="domain.id" class="tab-pane" id="settings" role="tabpanel" aria-labelledby="tab-settings">
                 <div class="card-body">
                     <div class="card-text">
                         <form class="read-only short">
@@ -63,15 +60,38 @@
                     </div>
                 </div>
             </div>
+            <div class="tab-pane" id="history" role="tabpanel" aria-labelledby="tab-history">
+                <div class="card-body">
+                    <event-log v-if="loadEventLog" :object-id="domain.id" object-type="domain" ref="eventLog" class="card-text mb-0"></event-log>
+                </div>
+            </div>
         </div>
+
+        <modal-dialog id="suspend-dialog" ref="suspendDialog" :title="$t(`btn.${suspendAction}`)" @click="submitSuspend()" :buttons="['submit']">
+            <textarea v-model="comment" name="comment" class="form-control" :placeholder="$t('form.comment')" rows="3"></textarea>
+        </modal-dialog>
     </div>
 </template>
 
 <script>
+    import EventLog from '../Widgets/EventLog'
+    import ModalDialog from '../Widgets/ModalDialog'
+
     export default {
+        components: {
+            EventLog,
+            ModalDialog
+        },
         data() {
             return {
-                domain: null
+                comment: '',
+                domain: {},
+                loadEventLog: false
+            }
+        },
+        computed: {
+            suspendAction() {
+                return this.domain.isSuspended ? 'unsuspend' : 'suspend'
             }
         },
         created() {
@@ -83,22 +103,29 @@
                 })
                 .catch(this.$root.errorHandler)
         },
+        mounted() {
+            this.$refs.tabs.clickHandler('history', () => { this.loadEventLog = true })
+        },
         methods: {
-            suspendDomain() {
-                axios.post('/api/v4/domains/' + this.domain.id + '/suspend')
-                    .then(response => {
-                        if (response.data.status == 'success') {
-                            this.$toast.success(response.data.message)
-                            this.domain = Object.assign({}, this.domain, { isSuspended: true })
-                        }
-                    })
+            setSuspendState() {
+                this.$root.clearFormValidation($('#suspend-dialog'))
+                this.$refs.suspendDialog.show()
             },
-            unsuspendDomain() {
-                axios.post('/api/v4/domains/' + this.domain.id + '/unsuspend')
+            submitSuspend() {
+                const post = { comment: this.comment }
+
+                axios.post(`/api/v4/domains/${this.domain.id}/${this.suspendAction}`, post)
                     .then(response => {
                         if (response.data.status == 'success') {
                             this.$toast.success(response.data.message)
-                            this.domain = Object.assign({}, this.domain, { isSuspended: false })
+                            this.domain = Object.assign({}, this.domain, { isSuspended: !this.domain.isSuspended })
+
+                            this.$refs.suspendDialog.hide()
+                            this.comment = ''
+
+                            if (this.loadEventLog) {
+                                this.$refs.eventLog.loadLog({ reset: true })
+                            }
                         }
                     })
             }
diff --git a/src/resources/vue/Admin/User.vue b/src/resources/vue/Admin/User.vue
--- a/src/resources/vue/Admin/User.vue
+++ b/src/resources/vue/Admin/User.vue
@@ -77,8 +77,8 @@
                         </div>
                     </form>
                     <div class="mt-2 buttons">
-                        <btn :id="'button-' + (user.isSuspended ? 'unsuspend' : 'suspend')" class="btn-outline-primary" @click="setSuspendState">
-                            {{ $t(user.isSuspended ? 'btn.unsuspend' : 'btn.suspend') }}
+                        <btn :id="`button-${suspendAction}`" class="btn-outline-primary" @click="setSuspendState">
+                            {{ $t(`btn.${suspendAction}`) }}
                         </btn>
                         <btn id="button-resync" class="btn-outline-primary" @click="resyncUser">
                             {{ $t('btn.resync') }}
@@ -228,21 +228,26 @@
                     </div>
                 </div>
             </div>
+            <div class="tab-pane" id="history" role="tabpanel" aria-labelledby="tab-history">
+                <div class="card-body">
+                    <event-log v-if="loadEventLog" :object-id="user.id" object-type="user" ref="eventLog" class="card-text mb-0"></event-log>
+                </div>
+            </div>
         </div>
 
         <modal-dialog id="discount-dialog" ref="discountDialog" :title="$t('user.discount-title')" @click="submitDiscount()" :buttons="['submit']">
-            <div>
-                <select v-model="wallet.discount_id" class="form-select">
-                    <option value="">- {{ $t('form.none') }} -</option>
-                    <option v-for="item in discounts" :value="item.id" :key="item.id">{{ item.label }}</option>
-                </select>
-            </div>
+            <select v-model="wallet.discount_id" class="form-select">
+                <option value="">- {{ $t('form.none') }} -</option>
+                <option v-for="item in discounts" :value="item.id" :key="item.id">{{ item.label }}</option>
+            </select>
         </modal-dialog>
 
         <modal-dialog id="email-dialog" ref="emailDialog" :title="$t('user.ext-email')" @click="submitEmail()" :buttons="['submit']">
-            <div>
-                <input v-model="external_email" name="external_email" class="form-control">
-            </div>
+            <input v-model="external_email" name="external_email" class="form-control">
+        </modal-dialog>
+
+        <modal-dialog id="suspend-dialog" ref="suspendDialog" :title="$t(`btn.${suspendAction}`)" @click="submitSuspend()" :buttons="['submit']">
+            <textarea v-model="comment" name="comment" class="form-control" :placeholder="$t('form.comment')" rows="3"></textarea>
         </modal-dialog>
 
         <modal-dialog id="oneoff-dialog" ref="oneoffDialog" @click="submitOneOff()" :buttons="['submit']"
@@ -273,6 +278,7 @@
 </template>
 
 <script>
+    import EventLog from '../Widgets/EventLog'
     import ModalDialog from '../Widgets/ModalDialog'
     import TransactionLog from '../Widgets/TransactionLog'
     import { ListTable } from '../Widgets/ListTools'
@@ -295,6 +301,7 @@
         components: {
             DistlistList,
             DomainList,
+            EventLog,
             ListTable,
             ModalDialog,
             ResourceList,
@@ -320,9 +327,7 @@
                     ],
                     footLabel: 'user.aliases-none'
                 },
-                oneoff_amount: '',
-                oneoff_description: '',
-                oneoff_negative: false,
+                comment: '',
                 discount: 0,
                 discount_description: '',
                 discounts: [],
@@ -330,6 +335,10 @@
                 folders: [],
                 has2FA: false,
                 hasBeta: false,
+                loadEventLog: false,
+                oneoff_amount: '',
+                oneoff_description: '',
+                oneoff_negative: false,
                 wallet: {},
                 walletReload: false,
                 distlists: [],
@@ -361,7 +370,8 @@
                     { label: 'user.distlists', count: 0 },
                     { label: 'user.resources', count: 0 },
                     { label: 'dashboard.shared-folders', count: 0 },
-                    { label: 'form.settings' }
+                    { label: 'form.settings' },
+                    { label: 'log.history' }
                 ],
                 users: [],
                 user: {
@@ -372,6 +382,11 @@
                 }
             }
         },
+        computed: {
+            suspendAction() {
+                return this.user.isSuspended ? 'unsuspend' : 'suspend'
+            }
+        },
         created() {
             const user_id = this.$route.params.user
 
@@ -476,6 +491,7 @@
                 .catch(this.$root.errorHandler)
         },
         mounted() {
+            this.$refs.tabs.clickHandler('history', () => { this.loadEventLog = true })
             this.$refs.discountDialog.events({
                 shown: () => {
                     // Note: Vue v-model is strict, convert null to a string
@@ -633,11 +649,23 @@
                     })
             },
             setSuspendState() {
-                axios.post('/api/v4/users/' + this.user.id + '/' + (this.user.isSuspended ? 'unsuspend' : 'suspend'))
+                this.$root.clearFormValidation($('#suspend-dialog'))
+                this.$refs.suspendDialog.show()
+            },
+            submitSuspend() {
+                const post = { comment: this.comment }
+
+                axios.post(`/api/v4/users/${this.user.id}/${this.suspendAction}`, post)
                     .then(response => {
                         if (response.data.status == 'success') {
                             this.$toast.success(response.data.message)
                             this.user = Object.assign({}, this.user, { isSuspended: !this.user.isSuspended })
+                            this.$refs.suspendDialog.hide()
+                            this.comment = ''
+
+                            if (this.loadEventLog) {
+                                this.$refs.eventLog.loadLog({ reset: true })
+                            }
                         }
                     })
             }
diff --git a/src/resources/vue/Widgets/EventLog.vue b/src/resources/vue/Widgets/EventLog.vue
new file mode 100644
--- /dev/null
+++ b/src/resources/vue/Widgets/EventLog.vue
@@ -0,0 +1,69 @@
+<template>
+    <div>
+        <table class="table table-sm m-0 eventlog">
+            <thead>
+                <tr>
+                    <th scope="col">{{ $t('form.date') }}</th>
+                    <th scope="col">{{ $t('log.event') }}</th>
+                    <th scope="col">{{ $t('form.comment') }}</th>
+                </tr>
+            </thead>
+            <tbody>
+                <tr v-for="event in log" :id="'log' + event.id" :key="event.id">
+                    <td class="datetime">{{ event.createdAt }}</td>
+                    <td>{{ event.event }}</td>
+                    <td class="description">
+                        <btn v-if="event.data || event.user" class="btn-link btn-action btn-more" icon="angle-right" :title="$t('form.more')" @click="loadDetails"></btn>
+                        <btn v-if="event.data || event.user" class="btn-link btn-action btn-less" icon="angle-down" :title="$t('form.less')" @click="hideDetails"></btn>
+                        {{ event.comment }}
+                        <pre v-if="event.data" class="details text-monospace p-1 m-1 ms-3">{{ JSON.stringify(event.data, null, 2) }}</pre>
+                        <div v-if="event.user" class="details email text-nowrap text-secondary ms-3">
+                            <svg-icon icon="user" class="me-1"></svg-icon>{{ event.user }}
+                        </div>
+                    </td>
+                </tr>
+            </tbody>
+            <list-foot :text="$t('log.list-none')" :colspan="3"></list-foot>
+        </table>
+        <list-more v-if="hasMore" :on-click="loadLog"></list-more>
+    </div>
+</template>
+
+<script>
+    import ListTools from './ListTools'
+    import { library } from '@fortawesome/fontawesome-svg-core'
+
+    library.add(
+        require('@fortawesome/free-solid-svg-icons/faAngleDown').definition,
+        require('@fortawesome/free-solid-svg-icons/faAngleRight').definition
+    )
+
+    export default {
+        mixins: [ ListTools ],
+        props: {
+            objectId: { type: [ String, Number ], default: null },
+            objectType: { type: String, default: null },
+        },
+        data() {
+            return {
+                log: []
+            }
+        },
+        mounted() {
+            this.loadLog({ reset: true })
+        },
+        methods: {
+            loadDetails(event) {
+                $(event.target).closest('tr').addClass('open')
+            },
+            hideDetails(event) {
+                $(event.target).closest('tr').removeClass('open')
+            },
+            loadLog(params) {
+                if (this.objectId && this.objectType) {
+                    this.listSearch('log', `/api/v4/eventlog/${this.objectType}/${this.objectId}`, params)
+                }
+            }
+        }
+    }
+</script>
diff --git a/src/resources/vue/Widgets/ListTools.vue b/src/resources/vue/Widgets/ListTools.vue
--- a/src/resources/vue/Widgets/ListTools.vue
+++ b/src/resources/vue/Widgets/ListTools.vue
@@ -125,7 +125,6 @@
                         get.search = this.currentSearch
                     }
 
-
                     if ('parent' in params) {
                         get.parent = params.parent
                         this.currentParent = params.parent
diff --git a/src/resources/vue/Widgets/ModalDialog.vue b/src/resources/vue/Widgets/ModalDialog.vue
--- a/src/resources/vue/Widgets/ModalDialog.vue
+++ b/src/resources/vue/Widgets/ModalDialog.vue
@@ -62,7 +62,7 @@
                 if (this.cancelFocus) {
                     $(event.target).find('button.modal-cancel').focus()
                 } else {
-                    $(event.target).find('input,select').first().focus()
+                    $(event.target).find('input,select,textarea').first().focus()
                 }
             })
 
diff --git a/src/routes/api.php b/src/routes/api.php
--- a/src/routes/api.php
+++ b/src/routes/api.php
@@ -235,6 +235,8 @@
             Route::post('domains/{id}/suspend', [API\V4\Admin\DomainsController::class, 'suspend']);
             Route::post('domains/{id}/unsuspend', [API\V4\Admin\DomainsController::class, 'unsuspend']);
 
+            Route::get('eventlog/{type}/{id}', [API\V4\Admin\EventLogController::class, 'index']);
+
             Route::apiResource('groups', API\V4\Admin\GroupsController::class);
             Route::post('groups/{id}/suspend', [API\V4\Admin\GroupsController::class, 'suspend']);
             Route::post('groups/{id}/unsuspend', [API\V4\Admin\GroupsController::class, 'unsuspend']);
@@ -275,6 +277,8 @@
             Route::post('domains/{id}/suspend', [API\V4\Reseller\DomainsController::class, 'suspend']);
             Route::post('domains/{id}/unsuspend', [API\V4\Reseller\DomainsController::class, 'unsuspend']);
 
+            Route::get('eventlog/{type}/{id}', [API\V4\Reseller\EventLogController::class, 'index']);
+
             Route::apiResource('groups', API\V4\Reseller\GroupsController::class);
             Route::post('groups/{id}/suspend', [API\V4\Reseller\GroupsController::class, 'suspend']);
             Route::post('groups/{id}/unsuspend', [API\V4\Reseller\GroupsController::class, 'unsuspend']);
diff --git a/src/tests/Browser/Admin/DistlistTest.php b/src/tests/Browser/Admin/DistlistTest.php
--- a/src/tests/Browser/Admin/DistlistTest.php
+++ b/src/tests/Browser/Admin/DistlistTest.php
@@ -2,9 +2,11 @@
 
 namespace Tests\Browser\Admin;
 
+use App\EventLog;
 use App\Group;
 use Illuminate\Support\Facades\Queue;
 use Tests\Browser;
+use Tests\Browser\Components\Dialog;
 use Tests\Browser\Components\Toast;
 use Tests\Browser\Pages\Admin\Distlist as DistlistPage;
 use Tests\Browser\Pages\Admin\User as UserPage;
@@ -23,6 +25,7 @@
         self::useAdminUrl();
 
         $this->deleteTestGroup('group-test@kolab.org');
+        Eventlog::query()->delete();
     }
 
     /**
@@ -31,6 +34,7 @@
     public function tearDown(): void
     {
         $this->deleteTestGroup('group-test@kolab.org');
+        Eventlog::query()->delete();
 
         parent::tearDown();
     }
@@ -65,6 +69,12 @@
             $group->save();
             $group->setConfig(['sender_policy' => ['test1.com', 'test2.com']]);
 
+            $event1 = EventLog::createFor($group, EventLog::TYPE_SUSPENDED, 'Event 1');
+            $event2 = EventLog::createFor($group, EventLog::TYPE_UNSUSPENDED, 'Event 2', ['test' => 'test-data']);
+            $event2->refresh();
+            $event1->created_at = (clone $event2->created_at)->subDay();
+            $event1->save();
+
             $distlist_page = new DistlistPage($group->id);
             $user_page = new UserPage($user->id);
 
@@ -75,7 +85,7 @@
                 ->visit($user_page)
                 ->on($user_page)
                 ->click('@nav #tab-distlists')
-                ->pause(1000)
+                ->waitFor('@user-distlists table tbody')
                 ->click('@user-distlists table tbody tr:first-child td a')
                 ->on($distlist_page)
                 ->assertSeeIn('@distlist-info .card-title', $group->email)
@@ -91,14 +101,42 @@
                         ->assertSeeIn('.row:nth-child(4) #members', $group->members[0])
                         ->assertSeeIn('.row:nth-child(4) #members', $group->members[1]);
                 })
-                ->assertElementsCount('ul.nav-tabs', 1)
-                ->assertSeeIn('ul.nav-tabs .nav-link', 'Settings')
+                ->assertElementsCount('ul.nav-tabs li', 2)
+                ->assertSeeIn('ul.nav-tabs #tab-settings', 'Settings')
                 ->with('@distlist-settings form', function (Browser $browser) {
                     $browser->assertElementsCount('.row', 1)
                         ->assertSeeIn('.row:nth-child(1) label', 'Sender Access List')
                         ->assertSeeIn('.row:nth-child(1) #sender_policy', 'test1.com, test2.com');
                 });
 
+            // Assert History tab
+            $browser->assertSeeIn('ul.nav-tabs #tab-history', 'History')
+                ->click('ul.nav-tabs #tab-history')
+                ->whenAvailable('@distlist-history table', function (Browser $browser) use ($event1, $event2) {
+                    $browser->waitFor('tbody tr')->assertElementsCount('tbody tr', 2)
+                        // row 1
+                        ->assertSeeIn('tr:nth-child(1) td:nth-child(1)', $event2->created_at->toDateTimeString())
+                        ->assertSeeIn('tr:nth-child(1) td:nth-child(2)', 'Unsuspended')
+                        ->assertSeeIn('tr:nth-child(1) td:nth-child(3)', $event2->comment)
+                        ->assertMissing('tr:nth-child(1) td:nth-child(3) div')
+                        ->assertMissing('tr:nth-child(1) td:nth-child(3) pre')
+                        ->assertMissing('tr:nth-child(1) td:nth-child(3) .btn-less')
+                        ->click('tr:nth-child(1) td:nth-child(3) .btn-more')
+                        ->assertMissing('tr:nth-child(1) td:nth-child(3) div.email')
+                        ->assertSeeIn('tr:nth-child(1) td:nth-child(3) pre', 'test-data')
+                        ->assertMissing('tr:nth-child(1) td:nth-child(3) .btn-more')
+                        ->click('tr:nth-child(1) td:nth-child(3) .btn-less')
+                        ->assertMissing('tr:nth-child(1) td:nth-child(3) div')
+                        ->assertMissing('tr:nth-child(1) td:nth-child(3) pre')
+                        ->assertMissing('tr:nth-child(1) td:nth-child(3) .btn-less')
+                        // row 2
+                        ->assertSeeIn('tr:nth-child(2) td:nth-child(1)', $event1->created_at->toDateTimeString())
+                        ->assertSeeIn('tr:nth-child(2) td:nth-child(2)', 'Suspended')
+                        ->assertSeeIn('tr:nth-child(2) td:nth-child(3)', $event1->comment)
+                        ->assertMissing('tr:nth-child(2) td:nth-child(3) .btn-more')
+                        ->assertMissing('tr:nth-child(2) td:nth-child(3) .btn-less');
+                });
+
             // Test invalid group identifier
             $browser->visit('/distlist/abc')->assertErrorPage(404);
         });
@@ -112,6 +150,7 @@
     public function testSuspendAndUnsuspend(): void
     {
         Queue::fake();
+        Eventlog::query()->delete();
 
         $this->browse(function (Browser $browser) {
             $user = $this->getTestUser('john@kolab.org');
@@ -125,14 +164,36 @@
                 ->assertMissing('@distlist-info #button-unsuspend')
                 ->assertSeeIn('@distlist-info #status.text-success', 'Active')
                 ->click('@distlist-info #button-suspend')
+                ->with(new Dialog('#suspend-dialog'), function (Browser $browser) {
+                    $browser->assertSeeIn('@title', 'Suspend')
+                        ->assertSeeIn('@button-cancel', 'Cancel')
+                        ->assertSeeIn('@button-action', 'Submit')
+                        ->type('textarea', 'test suspend')
+                        ->click('@button-action');
+                })
                 ->assertToast(Toast::TYPE_SUCCESS, 'Distribution list suspended successfully.')
                 ->assertSeeIn('@distlist-info #status.text-warning', 'Suspended')
-                ->assertMissing('@distlist-info #button-suspend')
-                ->click('@distlist-info #button-unsuspend')
+                ->assertMissing('@distlist-info #button-suspend');
+
+            $event = EventLog::where('type', EventLog::TYPE_SUSPENDED)->first();
+            $this->assertSame('test suspend', $event->comment);
+            $this->assertEquals($group->id, $event->object_id);
+
+            $browser->click('@distlist-info #button-unsuspend')
+                ->with(new Dialog('#suspend-dialog'), function (Browser $browser) {
+                    $browser->assertSeeIn('@title', 'Unsuspend')
+                        ->assertSeeIn('@button-cancel', 'Cancel')
+                        ->assertSeeIn('@button-action', 'Submit')
+                        ->click('@button-action');
+                })
                 ->assertToast(Toast::TYPE_SUCCESS, 'Distribution list unsuspended successfully.')
                 ->assertSeeIn('@distlist-info #status.text-success', 'Active')
                 ->assertVisible('@distlist-info #button-suspend')
                 ->assertMissing('@distlist-info #button-unsuspend');
+
+            $event = EventLog::where('type', EventLog::TYPE_UNSUSPENDED)->first();
+            $this->assertSame(null, $event->comment);
+            $this->assertEquals($group->id, $event->object_id);
         });
     }
 }
diff --git a/src/tests/Browser/Admin/DomainTest.php b/src/tests/Browser/Admin/DomainTest.php
--- a/src/tests/Browser/Admin/DomainTest.php
+++ b/src/tests/Browser/Admin/DomainTest.php
@@ -3,7 +3,9 @@
 namespace Tests\Browser\Admin;
 
 use App\Domain;
+use App\EventLog;
 use Tests\Browser;
+use Tests\Browser\Components\Dialog;
 use Tests\Browser\Components\Toast;
 use Tests\Browser\Pages\Admin\Domain as DomainPage;
 use Tests\Browser\Pages\Admin\User as UserPage;
@@ -24,6 +26,8 @@
         $this->deleteTestUser('test1@domainscontroller.com');
         $this->deleteTestDomain('domainscontroller.com');
 
+        Eventlog::query()->delete();
+
         self::useAdminUrl();
     }
 
@@ -38,6 +42,8 @@
         $this->deleteTestUser('test1@domainscontroller.com');
         $this->deleteTestDomain('domainscontroller.com');
 
+        Eventlog::query()->delete();
+
         parent::tearDown();
     }
 
@@ -66,6 +72,12 @@
 
             $domain->setSetting('spf_whitelist', null);
 
+            $event1 = EventLog::createFor($domain, EventLog::TYPE_SUSPENDED, 'Event 1');
+            $event2 = EventLog::createFor($domain, EventLog::TYPE_UNSUSPENDED, 'Event 2', ['test' => 'test-data']);
+            $event2->refresh();
+            $event1->created_at = (clone $event2->created_at)->subDay();
+            $event1->save();
+
             // Goto the domain page
             $browser->visit(new Home())
                 ->submitLogon('jeroen@jeroen.jeroen', \App\Utils::generatePassphrase(), true)
@@ -73,7 +85,7 @@
                 ->visit($user_page)
                 ->on($user_page)
                 ->click('@nav #tab-domains')
-                ->pause(1000)
+                ->waitFor('@user-domains table tbody')
                 ->click('@user-domains table tbody tr:first-child td a');
 
             $browser->on($domain_page)
@@ -88,7 +100,7 @@
 
             // Some tabs are loaded in background, wait a second
             $browser->pause(500)
-                ->assertElementsCount('@nav a', 2);
+                ->assertElementsCount('@nav a', 3);
 
             // Assert Configuration tab
             $browser->assertSeeIn('@nav #tab-config', 'Configuration')
@@ -110,11 +122,40 @@
             $domain->setSetting('spf_whitelist', json_encode(['.test1.com', '.test2.com']));
 
             $browser->refresh()
+                ->on($domain_page)
                 ->waitFor('@nav #tab-settings')
                 ->click('@nav #tab-settings')
-                ->with('@domain-settings form', function (Browser $browser) {
+                ->whenAvailable('@domain-settings form', function (Browser $browser) {
                     $browser->assertSeeIn('.row:first-child .form-control-plaintext', '.test1.com, .test2.com');
                 });
+
+            // Assert History tab
+            $browser->assertSeeIn('@nav #tab-history', 'History')
+                ->click('@nav #tab-history')
+                ->whenAvailable('@domain-history table', function (Browser $browser) use ($event1, $event2) {
+                    $browser->waitFor('tbody tr')->assertElementsCount('tbody tr', 2)
+                        // row 1
+                        ->assertSeeIn('tr:nth-child(1) td:nth-child(1)', $event2->created_at->toDateTimeString())
+                        ->assertSeeIn('tr:nth-child(1) td:nth-child(2)', 'Unsuspended')
+                        ->assertSeeIn('tr:nth-child(1) td:nth-child(3)', $event2->comment)
+                        ->assertMissing('tr:nth-child(1) td:nth-child(3) div')
+                        ->assertMissing('tr:nth-child(1) td:nth-child(3) pre')
+                        ->assertMissing('tr:nth-child(1) td:nth-child(3) .btn-less')
+                        ->click('tr:nth-child(1) td:nth-child(3) .btn-more')
+                        ->assertMissing('tr:nth-child(1) td:nth-child(3) div.email')
+                        ->assertSeeIn('tr:nth-child(1) td:nth-child(3) pre', 'test-data')
+                        ->assertMissing('tr:nth-child(1) td:nth-child(3) .btn-more')
+                        ->click('tr:nth-child(1) td:nth-child(3) .btn-less')
+                        ->assertMissing('tr:nth-child(1) td:nth-child(3) div')
+                        ->assertMissing('tr:nth-child(1) td:nth-child(3) pre')
+                        ->assertMissing('tr:nth-child(1) td:nth-child(3) .btn-less')
+                        // row 2
+                        ->assertSeeIn('tr:nth-child(2) td:nth-child(1)', $event1->created_at->toDateTimeString())
+                        ->assertSeeIn('tr:nth-child(2) td:nth-child(2)', 'Suspended')
+                        ->assertSeeIn('tr:nth-child(2) td:nth-child(3)', $event1->comment)
+                        ->assertMissing('tr:nth-child(2) td:nth-child(3) .btn-more')
+                        ->assertMissing('tr:nth-child(2) td:nth-child(3) .btn-less');
+                });
         });
     }
 
@@ -125,6 +166,8 @@
      */
     public function testSuspendAndUnsuspend(): void
     {
+        EventLog::query()->delete();
+
         $this->browse(function (Browser $browser) {
             $sku_domain = \App\Sku::withEnvTenantContext()->where('title', 'domain-hosting')->first();
             $user = $this->getTestUser('test1@domainscontroller.com');
@@ -146,14 +189,36 @@
                 ->assertVisible('@domain-info #button-suspend')
                 ->assertMissing('@domain-info #button-unsuspend')
                 ->click('@domain-info #button-suspend')
+                ->with(new Dialog('#suspend-dialog'), function (Browser $browser) {
+                    $browser->assertSeeIn('@title', 'Suspend')
+                        ->assertSeeIn('@button-cancel', 'Cancel')
+                        ->assertSeeIn('@button-action', 'Submit')
+                        ->type('textarea', 'test suspend')
+                        ->click('@button-action');
+                })
                 ->assertToast(Toast::TYPE_SUCCESS, 'Domain suspended successfully.')
                 ->assertSeeIn('@domain-info #status span.text-warning', 'Suspended')
-                ->assertMissing('@domain-info #button-suspend')
-                ->click('@domain-info #button-unsuspend')
+                ->assertMissing('@domain-info #button-suspend');
+
+            $event = EventLog::where('type', EventLog::TYPE_SUSPENDED)->first();
+            $this->assertSame('test suspend', $event->comment);
+            $this->assertEquals($domain->id, $event->object_id);
+
+            $browser->click('@domain-info #button-unsuspend')
+                ->with(new Dialog('#suspend-dialog'), function (Browser $browser) {
+                    $browser->assertSeeIn('@title', 'Unsuspend')
+                        ->assertSeeIn('@button-cancel', 'Cancel')
+                        ->assertSeeIn('@button-action', 'Submit')
+                        ->click('@button-action');
+                })
                 ->assertToast(Toast::TYPE_SUCCESS, 'Domain unsuspended successfully.')
                 ->assertSeeIn('@domain-info #status span.text-success', 'Active')
                 ->assertVisible('@domain-info #button-suspend')
                 ->assertMissing('@domain-info #button-unsuspend');
+
+            $event = EventLog::where('type', EventLog::TYPE_UNSUSPENDED)->first();
+            $this->assertSame(null, $event->comment);
+            $this->assertEquals($domain->id, $event->object_id);
         });
     }
 }
diff --git a/src/tests/Browser/Admin/UserTest.php b/src/tests/Browser/Admin/UserTest.php
--- a/src/tests/Browser/Admin/UserTest.php
+++ b/src/tests/Browser/Admin/UserTest.php
@@ -5,6 +5,7 @@
 use App\Auth\SecondFactor;
 use App\Discount;
 use App\Entitlement;
+use App\EventLog;
 use App\Sku;
 use App\User;
 use Tests\Browser;
@@ -39,6 +40,8 @@
         $wallet->save();
 
         Entitlement::where('cost', '>=', 5000)->delete();
+        Eventlog::query()->delete();
+
         $this->deleteTestGroup('group-test@kolab.org');
         $this->deleteTestUser('userstest1@kolabnow.com');
     }
@@ -61,6 +64,8 @@
         $wallet->save();
 
         Entitlement::where('cost', '>=', 5000)->delete();
+        Eventlog::query()->delete();
+
         $this->deleteTestGroup('group-test@kolab.org');
         $this->deleteTestUser('userstest1@kolabnow.com');
 
@@ -92,6 +97,15 @@
                     'guam_enabled' => null,
             ]);
 
+            $event1 = EventLog::createFor($jack, EventLog::TYPE_SUSPENDED, 'Event 1');
+            $event2 = EventLog::createFor($jack, EventLog::TYPE_UNSUSPENDED, 'Event 2', ['test' => 'test-data']);
+            $event2->refresh();
+            $event1->created_at = (clone $event2->created_at)->subDay();
+            $event1->user_email = 'jeroen@jeroen.jeroen';
+            $event1->save();
+            $event2->user_email = 'test@test.com';
+            $event2->save();
+
             $page = new UserPage($jack->id);
 
             $browser->visit(new Home())
@@ -123,7 +137,7 @@
 
             // Some tabs are loaded in background, wait a second
             $browser->pause(500)
-                ->assertElementsCount('@nav a', 9);
+                ->assertElementsCount('@nav a', 10);
 
             // Note: Finances tab is tested in UserFinancesTest.php
             $browser->assertSeeIn('@nav #tab-finances', 'Finances');
@@ -205,6 +219,35 @@
                         ->assertSeeIn('.row:nth-child(3) #limit_geo', 'No restrictions')
                         ->assertMissing('#limit_geo + button');
                 });
+
+            // Assert History tab
+            $browser->assertSeeIn('@nav #tab-history', 'History')
+                ->click('@nav #tab-history')
+                ->whenAvailable('@user-history table', function (Browser $browser) use ($event1, $event2) {
+                    $browser->waitFor('tbody tr')->assertElementsCount('tbody tr', 2)
+                        // row 1
+                        ->assertSeeIn('tr:nth-child(1) td:nth-child(1)', $event2->created_at->toDateTimeString())
+                        ->assertSeeIn('tr:nth-child(1) td:nth-child(2)', 'Unsuspended')
+                        ->assertSeeIn('tr:nth-child(1) td:nth-child(3)', $event2->comment)
+                        ->assertMissing('tr:nth-child(1) td:nth-child(3) div')
+                        ->assertMissing('tr:nth-child(1) td:nth-child(3) pre')
+                        ->assertMissing('tr:nth-child(1) td:nth-child(3) .btn-less')
+                        ->click('tr:nth-child(1) td:nth-child(3) .btn-more')
+                        ->assertSeeIn('tr:nth-child(1) td:nth-child(3) div.email', $event2->user_email)
+                        ->assertSeeIn('tr:nth-child(1) td:nth-child(3) pre', 'test-data')
+                        ->assertMissing('tr:nth-child(1) td:nth-child(3) .btn-more')
+                        ->click('tr:nth-child(1) td:nth-child(3) .btn-less')
+                        ->assertMissing('tr:nth-child(1) td:nth-child(3) div')
+                        ->assertMissing('tr:nth-child(1) td:nth-child(3) pre')
+                        ->assertMissing('tr:nth-child(1) td:nth-child(3) .btn-less')
+                        // row 2
+                        ->assertSeeIn('tr:nth-child(2) td:nth-child(1)', $event1->created_at->toDateTimeString())
+                        ->assertSeeIn('tr:nth-child(2) td:nth-child(2)', 'Suspended')
+                        ->assertSeeIn('tr:nth-child(2) td:nth-child(3)', $event1->comment)
+                        ->click('tr:nth-child(2) td:nth-child(3) .btn-more')
+                        ->assertSeeIn('tr:nth-child(2) td:nth-child(3) div.email', $event1->user_email)
+                        ->assertMissing('tr:nth-child(2) td:nth-child(3) pre');
+                });
         });
     }
 
@@ -260,7 +303,7 @@
 
             // Some tabs are loaded in background, wait a second
             $browser->pause(500)
-                ->assertElementsCount('@nav a', 9);
+                ->assertElementsCount('@nav a', 10);
 
             // Note: Finances tab is tested in UserFinancesTest.php
             $browser->assertSeeIn('@nav #tab-finances', 'Finances');
@@ -351,6 +394,14 @@
                         ->assertSeeIn('table tbody tr:nth-child(2) td:last-child', 'folder-contact@kolab.org')
                         ->assertMissing('table tfoot');
                 });
+
+            // Assert History tab
+            $browser->assertSeeIn('@nav #tab-history', 'History')
+                ->click('@nav #tab-history')
+                ->whenAvailable('@user-history table', function (Browser $browser) {
+                    $browser->assertElementsCount('tbody tr', 0)
+                        ->assertSeeIn('tfoot tr td', "There's no events in the log");
+                });
         });
 
         // Now we go to Ned's info page, he's a controller on John's wallet
@@ -392,7 +443,7 @@
 
             // Some tabs are loaded in background, wait a second
             $browser->pause(500)
-                ->assertElementsCount('@nav a', 9);
+                ->assertElementsCount('@nav a', 10);
 
             // Note: Finances tab is tested in UserFinancesTest.php
             $browser->assertSeeIn('@nav #tab-finances', 'Finances');
@@ -546,14 +597,34 @@
                 ->assertVisible('@user-info #button-suspend')
                 ->assertMissing('@user-info #button-unsuspend')
                 ->click('@user-info #button-suspend')
+                ->with(new Dialog('#suspend-dialog'), function (Browser $browser) {
+                    $browser->assertSeeIn('@title', 'Suspend')
+                        ->assertSeeIn('@button-cancel', 'Cancel')
+                        ->assertSeeIn('@button-action', 'Submit')
+                        ->type('textarea', 'test suspend')
+                        ->click('@button-action');
+                })
                 ->assertToast(Toast::TYPE_SUCCESS, 'User suspended successfully.')
                 ->assertSeeIn('@user-info #status span.text-warning', 'Suspended')
-                ->assertMissing('@user-info #button-suspend')
-                ->click('@user-info #button-unsuspend')
+                ->assertMissing('@user-info #button-suspend');
+
+            $event = EventLog::where('type', EventLog::TYPE_SUSPENDED)->first();
+            $this->assertSame('test suspend', $event->comment);
+
+            $browser->click('@user-info #button-unsuspend')
+                ->with(new Dialog('#suspend-dialog'), function (Browser $browser) {
+                    $browser->assertSeeIn('@title', 'Unsuspend')
+                        ->assertSeeIn('@button-cancel', 'Cancel')
+                        ->assertSeeIn('@button-action', 'Submit')
+                        ->click('@button-action');
+                })
                 ->assertToast(Toast::TYPE_SUCCESS, 'User unsuspended successfully.')
                 ->assertSeeIn('@user-info #status span.text-success', 'Active')
                 ->assertVisible('@user-info #button-suspend')
                 ->assertMissing('@user-info #button-unsuspend');
+
+            $event = EventLog::where('type', EventLog::TYPE_UNSUSPENDED)->first();
+            $this->assertSame(null, $event->comment);
         });
     }
 
diff --git a/src/tests/Browser/Pages/Admin/Distlist.php b/src/tests/Browser/Pages/Admin/Distlist.php
--- a/src/tests/Browser/Pages/Admin/Distlist.php
+++ b/src/tests/Browser/Pages/Admin/Distlist.php
@@ -38,6 +38,7 @@
     public function assert($browser): void
     {
         $browser->waitForLocation($this->url())
+            ->waitUntilMissing('@app .app-loader')
             ->waitFor('@distlist-info');
     }
 
@@ -52,6 +53,7 @@
             '@app' => '#app',
             '@distlist-info' => '#distlist-info',
             '@distlist-settings' => '#settings',
+            '@distlist-history' => '#history',
         ];
     }
 }
diff --git a/src/tests/Browser/Pages/Admin/Domain.php b/src/tests/Browser/Pages/Admin/Domain.php
--- a/src/tests/Browser/Pages/Admin/Domain.php
+++ b/src/tests/Browser/Pages/Admin/Domain.php
@@ -38,6 +38,7 @@
     public function assert($browser): void
     {
         $browser->waitForLocation($this->url())
+            ->waitUntilMissing('@app .app-loader')
             ->waitFor('@domain-info');
     }
 
@@ -54,6 +55,7 @@
             '@nav' => 'ul.nav-tabs',
             '@domain-config' => '#config',
             '@domain-settings' => '#settings',
+            '@domain-history' => '#history',
         ];
     }
 }
diff --git a/src/tests/Browser/Pages/Admin/Resource.php b/src/tests/Browser/Pages/Admin/Resource.php
--- a/src/tests/Browser/Pages/Admin/Resource.php
+++ b/src/tests/Browser/Pages/Admin/Resource.php
@@ -38,6 +38,7 @@
     public function assert($browser): void
     {
         $browser->waitForLocation($this->url())
+            ->waitUntilMissing('@app .app-loader')
             ->waitFor('@resource-info');
     }
 
diff --git a/src/tests/Browser/Pages/Admin/SharedFolder.php b/src/tests/Browser/Pages/Admin/SharedFolder.php
--- a/src/tests/Browser/Pages/Admin/SharedFolder.php
+++ b/src/tests/Browser/Pages/Admin/SharedFolder.php
@@ -38,6 +38,7 @@
     public function assert($browser): void
     {
         $browser->waitForLocation($this->url())
+            ->waitUntilMissing('@app .app-loader')
             ->waitFor('@folder-info');
     }
 
diff --git a/src/tests/Browser/Pages/Admin/User.php b/src/tests/Browser/Pages/Admin/User.php
--- a/src/tests/Browser/Pages/Admin/User.php
+++ b/src/tests/Browser/Pages/Admin/User.php
@@ -58,6 +58,7 @@
             '@user-subscriptions' => '#subscriptions',
             '@user-distlists' => '#distlists',
             '@user-domains' => '#domains',
+            '@user-history' => '#history',
             '@user-resources' => '#resources',
             '@user-folders' => '#folders',
             '@user-users' => '#users',
diff --git a/src/tests/Browser/Reseller/DistlistTest.php b/src/tests/Browser/Reseller/DistlistTest.php
--- a/src/tests/Browser/Reseller/DistlistTest.php
+++ b/src/tests/Browser/Reseller/DistlistTest.php
@@ -2,9 +2,11 @@
 
 namespace Tests\Browser\Reseller;
 
+use App\EventLog;
 use App\Group;
 use Illuminate\Support\Facades\Queue;
 use Tests\Browser;
+use Tests\Browser\Components\Dialog;
 use Tests\Browser\Components\Toast;
 use Tests\Browser\Pages\Admin\Distlist as DistlistPage;
 use Tests\Browser\Pages\Admin\User as UserPage;
@@ -23,6 +25,7 @@
         self::useResellerUrl();
 
         $this->deleteTestGroup('group-test@kolab.org');
+        Eventlog::query()->delete();
     }
 
     /**
@@ -31,6 +34,7 @@
     public function tearDown(): void
     {
         $this->deleteTestGroup('group-test@kolab.org');
+        Eventlog::query()->delete();
 
         parent::tearDown();
     }
@@ -75,7 +79,7 @@
                 ->visit($user_page)
                 ->on($user_page)
                 ->click('@nav #tab-distlists')
-                ->pause(1000)
+                ->waitFor('@user-distlists table tbody')
                 ->click('@user-distlists table tbody tr:first-child td a')
                 ->on($distlist_page)
                 ->assertSeeIn('@distlist-info .card-title', $group->email)
@@ -91,13 +95,14 @@
                         ->assertSeeIn('.row:nth-child(4) #members', $group->members[0])
                         ->assertSeeIn('.row:nth-child(4) #members', $group->members[1]);
                 })
-                ->assertElementsCount('ul.nav-tabs', 1)
-                ->assertSeeIn('ul.nav-tabs .nav-link', 'Settings')
+                ->assertElementsCount('ul.nav-tabs li', 2)
+                ->assertSeeIn('ul.nav-tabs #tab-settings', 'Settings')
                 ->with('@distlist-settings form', function (Browser $browser) {
                     $browser->assertElementsCount('.row', 1)
                         ->assertSeeIn('.row:nth-child(1) label', 'Sender Access List')
                         ->assertSeeIn('.row:nth-child(1) #sender_policy', 'test1.com, test2.com');
-                });
+                })
+                ->assertSeeIn('ul.nav-tabs #tab-history', 'History');
 
             // Test invalid group identifier
             $browser->visit('/distlist/abc')->assertErrorPage(404);
@@ -112,6 +117,7 @@
     public function testSuspendAndUnsuspend(): void
     {
         Queue::fake();
+        Eventlog::query()->delete();
 
         $this->browse(function (Browser $browser) {
             $user = $this->getTestUser('john@kolab.org');
@@ -125,14 +131,36 @@
                 ->assertMissing('@distlist-info #button-unsuspend')
                 ->assertSeeIn('@distlist-info #status.text-success', 'Active')
                 ->click('@distlist-info #button-suspend')
+                ->with(new Dialog('#suspend-dialog'), function (Browser $browser) {
+                    $browser->assertSeeIn('@title', 'Suspend')
+                        ->assertSeeIn('@button-cancel', 'Cancel')
+                        ->assertSeeIn('@button-action', 'Submit')
+                        ->type('textarea', 'test suspend')
+                        ->click('@button-action');
+                })
                 ->assertToast(Toast::TYPE_SUCCESS, 'Distribution list suspended successfully.')
                 ->assertSeeIn('@distlist-info #status.text-warning', 'Suspended')
-                ->assertMissing('@distlist-info #button-suspend')
-                ->click('@distlist-info #button-unsuspend')
+                ->assertMissing('@distlist-info #button-suspend');
+
+            $event = EventLog::where('type', EventLog::TYPE_SUSPENDED)->first();
+            $this->assertSame('test suspend', $event->comment);
+            $this->assertEquals($group->id, $event->object_id);
+
+            $browser->click('@distlist-info #button-unsuspend')
+                ->with(new Dialog('#suspend-dialog'), function (Browser $browser) {
+                    $browser->assertSeeIn('@title', 'Unsuspend')
+                        ->assertSeeIn('@button-cancel', 'Cancel')
+                        ->assertSeeIn('@button-action', 'Submit')
+                        ->click('@button-action');
+                })
                 ->assertToast(Toast::TYPE_SUCCESS, 'Distribution list unsuspended successfully.')
                 ->assertSeeIn('@distlist-info #status.text-success', 'Active')
                 ->assertVisible('@distlist-info #button-suspend')
                 ->assertMissing('@distlist-info #button-unsuspend');
+
+            $event = EventLog::where('type', EventLog::TYPE_UNSUSPENDED)->first();
+            $this->assertSame(null, $event->comment);
+            $this->assertEquals($group->id, $event->object_id);
         });
     }
 }
diff --git a/src/tests/Browser/Reseller/DomainTest.php b/src/tests/Browser/Reseller/DomainTest.php
--- a/src/tests/Browser/Reseller/DomainTest.php
+++ b/src/tests/Browser/Reseller/DomainTest.php
@@ -3,7 +3,9 @@
 namespace Tests\Browser\Reseller;
 
 use App\Domain;
+use App\EventLog;
 use Tests\Browser;
+use Tests\Browser\Components\Dialog;
 use Tests\Browser\Components\Toast;
 use Tests\Browser\Pages\Admin\Domain as DomainPage;
 use Tests\Browser\Pages\Admin\User as UserPage;
@@ -24,6 +26,8 @@
         $this->deleteTestUser('test1@domainscontroller.com');
         $this->deleteTestDomain('domainscontroller.com');
 
+        Eventlog::query()->delete();
+
         self::useResellerUrl();
     }
 
@@ -35,6 +39,8 @@
         $this->deleteTestUser('test1@domainscontroller.com');
         $this->deleteTestDomain('domainscontroller.com');
 
+        Eventlog::query()->delete();
+
         parent::tearDown();
     }
 
@@ -84,7 +90,9 @@
 
             // Some tabs are loaded in background, wait a second
             $browser->pause(500)
-                ->assertElementsCount('@nav a', 2);
+                ->assertElementsCount('@nav a', 3)
+                ->assertSeeIn('@nav #tab-settings', 'Settings')
+                ->assertSeeIn('@nav #tab-history', 'History');
 
             // Assert Configuration tab
             $browser->assertSeeIn('@nav #tab-config', 'Configuration')
@@ -102,6 +110,8 @@
      */
     public function testSuspendAndUnsuspend(): void
     {
+        EventLog::query()->delete();
+
         $this->browse(function (Browser $browser) {
             $sku_domain = \App\Sku::withEnvTenantContext()->where('title', 'domain-hosting')->first();
             $user = $this->getTestUser('test1@domainscontroller.com');
@@ -123,14 +133,36 @@
                 ->assertVisible('@domain-info #button-suspend')
                 ->assertMissing('@domain-info #button-unsuspend')
                 ->click('@domain-info #button-suspend')
+                ->with(new Dialog('#suspend-dialog'), function (Browser $browser) {
+                    $browser->assertSeeIn('@title', 'Suspend')
+                        ->assertSeeIn('@button-cancel', 'Cancel')
+                        ->assertSeeIn('@button-action', 'Submit')
+                        ->type('textarea', 'test suspend')
+                        ->click('@button-action');
+                })
                 ->assertToast(Toast::TYPE_SUCCESS, 'Domain suspended successfully.')
                 ->assertSeeIn('@domain-info #status span.text-warning', 'Suspended')
-                ->assertMissing('@domain-info #button-suspend')
-                ->click('@domain-info #button-unsuspend')
+                ->assertMissing('@domain-info #button-suspend');
+
+            $event = EventLog::where('type', EventLog::TYPE_SUSPENDED)->first();
+            $this->assertSame('test suspend', $event->comment);
+            $this->assertEquals($domain->id, $event->object_id);
+
+            $browser->click('@domain-info #button-unsuspend')
+                ->with(new Dialog('#suspend-dialog'), function (Browser $browser) {
+                    $browser->assertSeeIn('@title', 'Unsuspend')
+                        ->assertSeeIn('@button-cancel', 'Cancel')
+                        ->assertSeeIn('@button-action', 'Submit')
+                        ->click('@button-action');
+                })
                 ->assertToast(Toast::TYPE_SUCCESS, 'Domain unsuspended successfully.')
                 ->assertSeeIn('@domain-info #status span.text-success', 'Active')
                 ->assertVisible('@domain-info #button-suspend')
                 ->assertMissing('@domain-info #button-unsuspend');
+
+            $event = EventLog::where('type', EventLog::TYPE_UNSUSPENDED)->first();
+            $this->assertSame(null, $event->comment);
+            $this->assertEquals($domain->id, $event->object_id);
         });
     }
 }
diff --git a/src/tests/Browser/Reseller/UserTest.php b/src/tests/Browser/Reseller/UserTest.php
--- a/src/tests/Browser/Reseller/UserTest.php
+++ b/src/tests/Browser/Reseller/UserTest.php
@@ -4,6 +4,7 @@
 
 use App\Auth\SecondFactor;
 use App\Discount;
+use App\EventLog;
 use App\Sku;
 use App\User;
 use Tests\Browser;
@@ -117,7 +118,7 @@
 
             // Some tabs are loaded in background, wait a second
             $browser->pause(500)
-                ->assertElementsCount('@nav a', 9);
+                ->assertElementsCount('@nav a', 10);
 
             // Note: Finances tab is tested in UserFinancesTest.php
             $browser->assertSeeIn('@nav #tab-finances', 'Finances');
@@ -253,7 +254,7 @@
 
             // Some tabs are loaded in background, wait a second
             $browser->pause(500)
-                ->assertElementsCount('@nav a', 9);
+                ->assertElementsCount('@nav a', 10);
 
             // Note: Finances tab is tested in UserFinancesTest.php
             $browser->assertSeeIn('@nav #tab-finances', 'Finances');
@@ -344,6 +345,9 @@
                         ->assertSeeIn('table tbody tr:nth-child(2) td:last-child', 'folder-contact@kolab.org')
                         ->assertMissing('table tfoot');
                 });
+
+            // Assert History tab
+            $browser->assertSeeIn('@nav #tab-history', 'History');
         });
 
         // Now we go to Ned's info page, he's a controller on John's wallet
@@ -365,7 +369,7 @@
 
             // Some tabs are loaded in background, wait a second
             $browser->pause(500)
-                ->assertElementsCount('@nav a', 9);
+                ->assertElementsCount('@nav a', 10);
 
             // Note: Finances tab is tested in UserFinancesTest.php
             $browser->assertSeeIn('@nav #tab-finances', 'Finances');
@@ -450,6 +454,9 @@
                         ->assertSeeIn('.row:nth-child(3) label', 'Geo-lockin')
                         ->assertSeeIn('.row:nth-child(3) #limit_geo', 'No restrictions');
                 });
+
+            // Assert History tab
+            $browser->assertSeeIn('@nav #tab-history', 'History');
         });
     }
 
@@ -513,6 +520,8 @@
      */
     public function testSuspendAndUnsuspend(): void
     {
+        EventLog::query()->delete();
+
         $this->browse(function (Browser $browser) {
             $john = $this->getTestUser('john@kolab.org');
 
@@ -520,14 +529,34 @@
                 ->assertVisible('@user-info #button-suspend')
                 ->assertMissing('@user-info #button-unsuspend')
                 ->click('@user-info #button-suspend')
+                ->with(new Dialog('#suspend-dialog'), function (Browser $browser) {
+                    $browser->assertSeeIn('@title', 'Suspend')
+                        ->assertSeeIn('@button-cancel', 'Cancel')
+                        ->assertSeeIn('@button-action', 'Submit')
+                        ->type('textarea', 'test suspend')
+                        ->click('@button-action');
+                })
                 ->assertToast(Toast::TYPE_SUCCESS, 'User suspended successfully.')
                 ->assertSeeIn('@user-info #status span.text-warning', 'Suspended')
-                ->assertMissing('@user-info #button-suspend')
-                ->click('@user-info #button-unsuspend')
+                ->assertMissing('@user-info #button-suspend');
+
+            $event = EventLog::where('type', EventLog::TYPE_SUSPENDED)->first();
+            $this->assertSame('test suspend', $event->comment);
+
+            $browser->click('@user-info #button-unsuspend')
+                ->with(new Dialog('#suspend-dialog'), function (Browser $browser) {
+                    $browser->assertSeeIn('@title', 'Unsuspend')
+                        ->assertSeeIn('@button-cancel', 'Cancel')
+                        ->assertSeeIn('@button-action', 'Submit')
+                        ->click('@button-action');
+                })
                 ->assertToast(Toast::TYPE_SUCCESS, 'User unsuspended successfully.')
                 ->assertSeeIn('@user-info #status span.text-success', 'Active')
                 ->assertVisible('@user-info #button-suspend')
                 ->assertMissing('@user-info #button-unsuspend');
+
+            $event = EventLog::where('type', EventLog::TYPE_UNSUSPENDED)->first();
+            $this->assertSame(null, $event->comment);
         });
     }
 
diff --git a/src/tests/Feature/Console/Group/SuspendTest.php b/src/tests/Feature/Console/Group/SuspendTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Console/Group/SuspendTest.php
@@ -0,0 +1,75 @@
+<?php
+
+namespace Tests\Feature\Console\Group;
+
+use App\EventLog;
+use App\Group;
+use Illuminate\Support\Facades\Queue;
+use Tests\TestCase;
+
+class SuspendTest extends TestCase
+{
+    /**
+     * {@inheritDoc}
+     */
+    public function setUp(): void
+    {
+        parent::setUp();
+
+        $this->deleteTestGroup('group-test@kolabnow.com');
+        EventLog::truncate();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function tearDown(): void
+    {
+        $this->deleteTestGroup('group-test@kolabnow.com');
+        EventLog::truncate();
+
+        parent::tearDown();
+    }
+
+    /**
+     * Test the command
+     */
+    public function testHandle(): void
+    {
+        Queue::fake();
+
+        // Non-existing user
+        $code = \Artisan::call("group:suspend unknown");
+        $output = trim(\Artisan::output());
+
+        $this->assertSame(1, $code);
+        $this->assertSame("Group not found.", $output);
+
+        $group = $this->getTestGroup('group-test@kolabnow.com');
+
+        // Test success (no --comment)
+        $code = \Artisan::call("group:suspend {$group->email}");
+        $output = trim(\Artisan::output());
+
+        $this->assertSame(0, $code);
+        $this->assertSame("", $output);
+        $this->assertTrue($group->fresh()->isSuspended());
+        $event = EventLog::where('object_id', $group->id)->where('object_type', Group::class)->first();
+        $this->assertSame(null, $event->comment);
+        $this->assertSame(EventLog::TYPE_SUSPENDED, $event->type);
+
+        $group->unsuspend();
+        EventLog::truncate();
+
+        // Test success (no --comment)
+        $code = \Artisan::call("group:suspend --comment=\"Test comment\" {$group->email}");
+        $output = trim(\Artisan::output());
+
+        $this->assertSame(0, $code);
+        $this->assertSame("", $output);
+        $this->assertTrue($group->fresh()->isSuspended());
+        $event = EventLog::where('object_id', $group->id)->where('object_type', Group::class)->first();
+        $this->assertSame('Test comment', $event->comment);
+        $this->assertSame(EventLog::TYPE_SUSPENDED, $event->type);
+    }
+}
diff --git a/src/tests/Feature/Console/Group/UnsuspendTest.php b/src/tests/Feature/Console/Group/UnsuspendTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Console/Group/UnsuspendTest.php
@@ -0,0 +1,76 @@
+<?php
+
+namespace Tests\Feature\Console\Group;
+
+use App\EventLog;
+use App\Group;
+use Illuminate\Support\Facades\Queue;
+use Tests\TestCase;
+
+class UnsuspendTest extends TestCase
+{
+    /**
+     * {@inheritDoc}
+     */
+    public function setUp(): void
+    {
+        parent::setUp();
+
+        $this->deleteTestGroup('group-test@kolabnow.com');
+        EventLog::truncate();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function tearDown(): void
+    {
+        $this->deleteTestGroup('group-test@kolabnow.com');
+        EventLog::truncate();
+
+        parent::tearDown();
+    }
+
+    /**
+     * Test the command
+     */
+    public function testHandle(): void
+    {
+        Queue::fake();
+
+        // Non-existing user
+        $code = \Artisan::call("group:unsuspend unknown");
+        $output = trim(\Artisan::output());
+
+        $this->assertSame(1, $code);
+        $this->assertSame("Group not found.", $output);
+
+        $group = $this->getTestGroup('group-test@kolabnow.com');
+        $group->suspend();
+
+        // Test success (no --comment)
+        $code = \Artisan::call("group:unsuspend {$group->email}");
+        $output = trim(\Artisan::output());
+
+        $this->assertSame(0, $code);
+        $this->assertSame("", $output);
+        $this->assertFalse($group->fresh()->isSuspended());
+        $event = EventLog::where('object_id', $group->id)->where('object_type', Group::class)->first();
+        $this->assertSame(null, $event->comment);
+        $this->assertSame(EventLog::TYPE_UNSUSPENDED, $event->type);
+
+        $group->suspend();
+        EventLog::truncate();
+
+        // Test success (no --comment)
+        $code = \Artisan::call("group:unsuspend --comment=\"Test comment\" {$group->email}");
+        $output = trim(\Artisan::output());
+
+        $this->assertSame(0, $code);
+        $this->assertSame("", $output);
+        $this->assertFalse($group->fresh()->isSuspended());
+        $event = EventLog::where('object_id', $group->id)->where('object_type', Group::class)->first();
+        $this->assertSame('Test comment', $event->comment);
+        $this->assertSame(EventLog::TYPE_UNSUSPENDED, $event->type);
+    }
+}
diff --git a/src/tests/Feature/Controller/Admin/DomainsTest.php b/src/tests/Feature/Controller/Admin/DomainsTest.php
--- a/src/tests/Feature/Controller/Admin/DomainsTest.php
+++ b/src/tests/Feature/Controller/Admin/DomainsTest.php
@@ -4,6 +4,7 @@
 
 use App\Domain;
 use App\Entitlement;
+use App\EventLog;
 use App\Sku;
 use Illuminate\Support\Facades\Queue;
 use Tests\TestCase;
@@ -201,7 +202,7 @@
 
         $this->assertFalse($domain->fresh()->isSuspended());
 
-        // Test suspending the user
+        // Test suspending the domain
         $response = $this->actingAs($admin)->post("/api/v4/domains/{$domain->id}/suspend", []);
         $response->assertStatus(200);
 
@@ -210,7 +211,23 @@
         $this->assertSame('success', $json['status']);
         $this->assertSame("Domain suspended successfully.", $json['message']);
         $this->assertCount(2, $json);
+        $this->assertTrue($domain->fresh()->isSuspended());
+
+        $domain->unsuspend();
+        EventLog::truncate();
+
+        // Test suspending the domain with a comment
+        $response = $this->actingAs($admin)->post("/api/v4/domains/{$domain->id}/suspend", ['comment' => 'Test']);
+        $response->assertStatus(200);
+
+        $where = [
+            'object_id' => $domain->id,
+            'object_type' => Domain::class,
+            'type' => EventLog::TYPE_SUSPENDED,
+            'comment' => 'Test'
+        ];
 
+        $this->assertSame(1, EventLog::where($where)->count());
         $this->assertTrue($domain->fresh()->isSuspended());
     }
 
@@ -234,7 +251,7 @@
 
         $this->assertTrue($domain->fresh()->isSuspended());
 
-        // Test suspending the user
+        // Test suspending the domain
         $response = $this->actingAs($admin)->post("/api/v4/domains/{$domain->id}/unsuspend", []);
         $response->assertStatus(200);
 
@@ -243,7 +260,23 @@
         $this->assertSame('success', $json['status']);
         $this->assertSame("Domain unsuspended successfully.", $json['message']);
         $this->assertCount(2, $json);
+        $this->assertFalse($domain->fresh()->isSuspended());
+
+        $domain->suspend();
+        EventLog::truncate();
+
+        // Test unsuspending the domain with a comment
+        $response = $this->actingAs($admin)->post("/api/v4/domains/{$domain->id}/unsuspend", ['comment' => 'Test']);
+        $response->assertStatus(200);
+
+        $where = [
+            'object_id' => $domain->id,
+            'object_type' => Domain::class,
+            'type' => EventLog::TYPE_UNSUSPENDED,
+            'comment' => 'Test'
+        ];
 
+        $this->assertSame(1, EventLog::where($where)->count());
         $this->assertFalse($domain->fresh()->isSuspended());
     }
 }
diff --git a/src/tests/Feature/Controller/Admin/EventLogTest.php b/src/tests/Feature/Controller/Admin/EventLogTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Controller/Admin/EventLogTest.php
@@ -0,0 +1,97 @@
+<?php
+
+namespace Tests\Feature\Controller\Admin;
+
+use App\EventLog;
+use Tests\TestCase;
+
+class EventLogTest extends TestCase
+{
+    /**
+     * {@inheritDoc}
+     */
+    public function setUp(): void
+    {
+        parent::setUp();
+        self::useAdminUrl();
+
+        EventLog::query()->delete();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function tearDown(): void
+    {
+        EventLog::query()->delete();
+
+        parent::tearDown();
+    }
+
+    /**
+     * Test listing events for a user (GET /api/v4/eventlog/user/{user})
+     */
+    public function testUserLog(): void
+    {
+        $user = $this->getTestUser('john@kolab.org');
+        $admin = $this->getTestUser('jeroen@jeroen.jeroen');
+
+        // Non-admin user
+        $response = $this->actingAs($user)->get("api/v4/eventlog/user/{$user->id}");
+        $response->assertStatus(403);
+
+        // Admin user (unknown object type)
+        $response = $this->actingAs($admin)->get("api/v4/eventlog/eeee/{$user->id}");
+        $response->assertStatus(404);
+
+        // Admin user (empty list)
+        $response = $this->actingAs($admin)->get("api/v4/eventlog/user/{$user->id}");
+        $response->assertStatus(200);
+
+        $json = $response->json();
+
+        $this->assertSame(0, $json['count']);
+        $this->assertSame([], $json['list']);
+        $this->assertFalse($json['hasMore']);
+
+        // Non-empty list
+        $event1 = EventLog::createFor($user, EventLog::TYPE_SUSPENDED, "Event 1", ['test' => 'test1']);
+        $event1->created_at = now();
+        $event1->save();
+        $event2 = EventLog::createFor($user, EventLog::TYPE_UNSUSPENDED, "Event 2", ['test' => 'test2']);
+        $event2->created_at = (clone now())->subDay();
+        $event2->save();
+
+        $response = $this->actingAs($admin)->get("api/v4/eventlog/user/{$user->id}");
+        $response->assertStatus(200);
+
+        $json = $response->json();
+
+        $this->assertSame(2, $json['count']);
+        $this->assertCount(2, $json['list']);
+        $this->assertFalse($json['hasMore']);
+        $this->assertSame($event1->id, $json['list'][0]['id']);
+        $this->assertSame($event1->comment, $json['list'][0]['comment']);
+        $this->assertSame($event1->data, $json['list'][0]['data']);
+        $this->assertSame($admin->email, $json['list'][0]['user']);
+        $this->assertSame('Suspended', $json['list'][0]['event']);
+        $this->assertSame($event2->id, $json['list'][1]['id']);
+        $this->assertSame($event2->comment, $json['list'][1]['comment']);
+        $this->assertSame($event2->data, $json['list'][1]['data']);
+        $this->assertSame($admin->email, $json['list'][1]['user']);
+        $this->assertSame('Unsuspended', $json['list'][1]['event']);
+
+        // A user in another tenant
+        $user = $this->getTestUser('user@sample-tenant.dev-local');
+        $event3 = EventLog::createFor($user, EventLog::TYPE_SUSPENDED, "Event 3");
+
+        $response = $this->actingAs($admin)->get("api/v4/eventlog/user/{$user->id}");
+        $response->assertStatus(200);
+
+        $json = $response->json();
+
+        $this->assertSame(1, $json['count']);
+        $this->assertCount(1, $json['list']);
+        $this->assertSame($event3->id, $json['list'][0]['id']);
+    }
+}
diff --git a/src/tests/Feature/Controller/Admin/GroupsTest.php b/src/tests/Feature/Controller/Admin/GroupsTest.php
--- a/src/tests/Feature/Controller/Admin/GroupsTest.php
+++ b/src/tests/Feature/Controller/Admin/GroupsTest.php
@@ -2,6 +2,7 @@
 
 namespace Tests\Feature\Controller\Admin;
 
+use App\EventLog;
 use App\Group;
 use Illuminate\Support\Facades\Queue;
 use Tests\TestCase;
@@ -184,7 +185,23 @@
         $this->assertSame('success', $json['status']);
         $this->assertSame("Distribution list suspended successfully.", $json['message']);
         $this->assertCount(2, $json);
+        $this->assertTrue($group->fresh()->isSuspended());
+
+        $group->unsuspend();
+        EventLog::truncate();
+
+        // Test suspending the group with a comment
+        $response = $this->actingAs($admin)->post("/api/v4/groups/{$group->id}/suspend", ['comment' => 'Test']);
+        $response->assertStatus(200);
+
+        $where = [
+            'object_id' => $group->id,
+            'object_type' => Group::class,
+            'type' => EventLog::TYPE_SUSPENDED,
+            'comment' => 'Test'
+        ];
 
+        $this->assertSame(1, EventLog::where($where)->count());
         $this->assertTrue($group->fresh()->isSuspended());
     }
 
@@ -221,7 +238,23 @@
         $this->assertSame('success', $json['status']);
         $this->assertSame("Distribution list unsuspended successfully.", $json['message']);
         $this->assertCount(2, $json);
+        $this->assertFalse($group->fresh()->isSuspended());
+
+        $group->unsuspend();
+        EventLog::truncate();
+
+        // Test unsuspending the group with a comment
+        $response = $this->actingAs($admin)->post("/api/v4/groups/{$group->id}/unsuspend", ['comment' => 'Test']);
+        $response->assertStatus(200);
+
+        $where = [
+            'object_id' => $group->id,
+            'object_type' => Group::class,
+            'type' => EventLog::TYPE_UNSUSPENDED,
+            'comment' => 'Test'
+        ];
 
+        $this->assertSame(1, EventLog::where($where)->count());
         $this->assertFalse($group->fresh()->isSuspended());
     }
 }
diff --git a/src/tests/Feature/Controller/Admin/UsersTest.php b/src/tests/Feature/Controller/Admin/UsersTest.php
--- a/src/tests/Feature/Controller/Admin/UsersTest.php
+++ b/src/tests/Feature/Controller/Admin/UsersTest.php
@@ -3,7 +3,9 @@
 namespace Tests\Feature\Controller\Admin;
 
 use App\Auth\SecondFactor;
+use App\EventLog;
 use App\Sku;
+use App\User;
 use Illuminate\Support\Facades\Queue;
 use Tests\TestCase;
 
@@ -503,7 +505,23 @@
         $this->assertSame('success', $json['status']);
         $this->assertSame("User suspended successfully.", $json['message']);
         $this->assertCount(2, $json);
+        $this->assertTrue($user->fresh()->isSuspended());
+
+        $user->unsuspend();
+        EventLog::truncate();
+
+        // Test suspending the user with a comment
+        $response = $this->actingAs($admin)->post("/api/v4/users/{$user->id}/suspend", ['comment' => 'Test']);
+        $response->assertStatus(200);
+
+        $where = [
+            'object_id' => $user->id,
+            'object_type' => User::class,
+            'type' => EventLog::TYPE_SUSPENDED,
+            'comment' => 'Test'
+        ];
 
+        $this->assertSame(1, EventLog::where($where)->count());
         $this->assertTrue($user->fresh()->isSuspended());
     }
 
@@ -534,7 +552,23 @@
         $this->assertSame('success', $json['status']);
         $this->assertSame("User unsuspended successfully.", $json['message']);
         $this->assertCount(2, $json);
+        $this->assertFalse($user->fresh()->isSuspended());
+
+        $user->suspend();
+        EventLog::truncate();
+
+        // Test suspending the user with a comment
+        $response = $this->actingAs($admin)->post("/api/v4/users/{$user->id}/unsuspend", ['comment' => 'Test']);
+        $response->assertStatus(200);
+
+        $where = [
+            'object_id' => $user->id,
+            'object_type' => User::class,
+            'type' => EventLog::TYPE_UNSUSPENDED,
+            'comment' => 'Test'
+        ];
 
+        $this->assertSame(1, EventLog::where($where)->count());
         $this->assertFalse($user->fresh()->isSuspended());
     }
 
diff --git a/src/tests/Feature/Controller/Reseller/EventLogTest.php b/src/tests/Feature/Controller/Reseller/EventLogTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Controller/Reseller/EventLogTest.php
@@ -0,0 +1,106 @@
+<?php
+
+namespace Tests\Feature\Controller\Reseller;
+
+use App\EventLog;
+use Tests\TestCase;
+
+class EventLogTest extends TestCase
+{
+    /**
+     * {@inheritDoc}
+     */
+    public function setUp(): void
+    {
+        parent::setUp();
+        self::useResellerUrl();
+
+        EventLog::query()->delete();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function tearDown(): void
+    {
+        EventLog::query()->delete();
+
+        parent::tearDown();
+    }
+
+    /**
+     * Test listing events for a user (GET /api/v4/eventlog/user/{user})
+     */
+    public function testUserLog(): void
+    {
+        $user = $this->getTestUser('john@kolab.org');
+        $admin = $this->getTestUser('jeroen@jeroen.jeroen');
+        $reseller1 = $this->getTestUser('reseller@' . \config('app.domain'));
+        $reseller2 = $this->getTestUser('reseller@sample-tenant.dev-local');
+
+        // Non-admin user
+        $response = $this->actingAs($user)->get("api/v4/eventlog/user/{$user->id}");
+        $response->assertStatus(403);
+
+        // Admin user
+        $response = $this->actingAs($admin)->get("api/v4/eventlog/user/{$user->id}");
+        $response->assertStatus(403);
+
+        // Reseller user (unknown object type)
+        $response = $this->actingAs($reseller1)->get("api/v4/eventlog/eeee/{$user->id}");
+        $response->assertStatus(404);
+
+        // Reseller user (empty list)
+        $response = $this->actingAs($reseller1)->get("api/v4/eventlog/user/{$user->id}");
+        $response->assertStatus(200);
+
+        $json = $response->json();
+
+        $this->assertSame(0, $json['count']);
+        $this->assertSame([], $json['list']);
+        $this->assertFalse($json['hasMore']);
+
+        // Non-empty list
+        $event1 = EventLog::createFor($user, EventLog::TYPE_SUSPENDED, "Event 1", ['test' => 'test1']);
+        $event1->created_at = now();
+        $event1->save();
+        $event2 = EventLog::createFor($user, EventLog::TYPE_UNSUSPENDED, "Event 2", ['test' => 'test2']);
+        $event2->created_at = (clone now())->subDay();
+        $event2->save();
+
+        $response = $this->actingAs($reseller1)->get("api/v4/eventlog/user/{$user->id}");
+        $response->assertStatus(200);
+
+        $json = $response->json();
+
+        $this->assertSame(2, $json['count']);
+        $this->assertCount(2, $json['list']);
+        $this->assertFalse($json['hasMore']);
+        $this->assertSame($event1->id, $json['list'][0]['id']);
+        $this->assertSame($event1->comment, $json['list'][0]['comment']);
+        $this->assertSame($event1->data, $json['list'][0]['data']);
+        $this->assertSame($reseller1->email, $json['list'][0]['user']);
+        $this->assertSame('Suspended', $json['list'][0]['event']);
+        $this->assertSame($event2->id, $json['list'][1]['id']);
+        $this->assertSame($event2->comment, $json['list'][1]['comment']);
+        $this->assertSame($event2->data, $json['list'][1]['data']);
+        $this->assertSame($reseller1->email, $json['list'][1]['user']);
+        $this->assertSame('Unsuspended', $json['list'][1]['event']);
+
+        // A user in another tenant
+        $user = $this->getTestUser('user@sample-tenant.dev-local');
+        $event3 = EventLog::createFor($user, EventLog::TYPE_SUSPENDED, "Event 3");
+
+        $response = $this->actingAs($reseller1)->get("api/v4/eventlog/user/{$user->id}");
+        $response->assertStatus(404);
+
+        $response = $this->actingAs($reseller2)->get("api/v4/eventlog/user/{$user->id}");
+        $response->assertStatus(200);
+
+        $json = $response->json();
+
+        $this->assertSame(1, $json['count']);
+        $this->assertCount(1, $json['list']);
+        $this->assertSame($event3->id, $json['list'][0]['id']);
+    }
+}
diff --git a/src/tests/Feature/DomainTest.php b/src/tests/Feature/DomainTest.php
--- a/src/tests/Feature/DomainTest.php
+++ b/src/tests/Feature/DomainTest.php
@@ -4,6 +4,7 @@
 
 use App\Domain;
 use App\Entitlement;
+use App\EventLog;
 use App\Sku;
 use App\User;
 use App\Tenant;
@@ -232,6 +233,29 @@
     }
 
     /**
+     * Test eventlog on domain deletion
+     */
+    public function testDeleteAndEventLog(): void
+    {
+        Queue::fake();
+
+        $domain = $this->getTestDomain('gmail.com', [
+                'status' => Domain::STATUS_NEW,
+                'type' => Domain::TYPE_PUBLIC,
+        ]);
+
+        EventLog::createFor($domain, EventLog::TYPE_SUSPENDED, 'test');
+
+        $domain->delete();
+
+        $this->assertCount(1, EventLog::where('object_id', $domain->id)->where('object_type', Domain::class)->get());
+
+        $domain->forceDelete();
+
+        $this->assertCount(0, EventLog::where('object_id', $domain->id)->where('object_type', Domain::class)->get());
+    }
+
+    /**
      * Test isEmpty() method
      */
     public function testIsEmpty(): void
diff --git a/src/tests/Feature/GroupTest.php b/src/tests/Feature/GroupTest.php
--- a/src/tests/Feature/GroupTest.php
+++ b/src/tests/Feature/GroupTest.php
@@ -3,6 +3,7 @@
 namespace Tests\Feature;
 
 use App\Group;
+use App\EventLog;
 use Illuminate\Support\Facades\Queue;
 use Tests\TestCase;
 
@@ -133,6 +134,26 @@
     }
 
     /**
+     * Test eventlog on group deletion
+     */
+    public function testDeleteAndEventLog(): void
+    {
+        Queue::fake();
+
+        $group = $this->getTestGroup('group-test@kolabnow.com');
+
+        EventLog::createFor($group, EventLog::TYPE_SUSPENDED, 'test');
+
+        $group->delete();
+
+        $this->assertCount(1, EventLog::where('object_id', $group->id)->where('object_type', Group::class)->get());
+
+        $group->forceDelete();
+
+        $this->assertCount(0, EventLog::where('object_id', $group->id)->where('object_type', Group::class)->get());
+    }
+
+    /**
      * Tests for Group::emailExists()
      */
     public function testEmailExists(): void
diff --git a/src/tests/Feature/UserTest.php b/src/tests/Feature/UserTest.php
--- a/src/tests/Feature/UserTest.php
+++ b/src/tests/Feature/UserTest.php
@@ -3,6 +3,7 @@
 namespace Tests\Feature;
 
 use App\Domain;
+use App\EventLog;
 use App\Group;
 use App\Package;
 use App\PackageSku;
@@ -839,6 +840,26 @@
     }
 
     /**
+     * Test eventlog on user deletion
+     */
+    public function testDeleteAndEventLog(): void
+    {
+        Queue::fake();
+
+        $user = $this->getTestUser('user-test@' . \config('app.domain'));
+
+        EventLog::createFor($user, EventLog::TYPE_SUSPENDED, 'test');
+
+        $user->delete();
+
+        $this->assertCount(1, EventLog::where('object_id', $user->id)->where('object_type', User::class)->get());
+
+        $user->forceDelete();
+
+        $this->assertCount(0, EventLog::where('object_id', $user->id)->where('object_type', User::class)->get());
+    }
+
+    /**
      * Test user deletion vs. group membership
      */
     public function testDeleteAndGroups(): void
diff --git a/src/tests/Unit/EventLogTest.php b/src/tests/Unit/EventLogTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Unit/EventLogTest.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace Tests\Unit;
+
+use App\EventLog;
+use Tests\TestCase;
+
+class EventLogTest extends TestCase
+{
+    /**
+     * Test type mutator
+     */
+    public function testSetTypeAttribute(): void
+    {
+        $event = new EventLog();
+
+        $this->expectException(\Exception::class);
+        $event->type = -1;
+
+        $this->expectException(\Exception::class);
+        $event->type = 256;
+
+        $this->expectException(\Exception::class);
+        $event->type = 'abc'; // @phpstan-ignore-line
+
+        $event->type = 2;
+        $this->assertSame(20, $event->type);
+    }
+}