diff --git a/app/Facades/Activity.php b/app/Facades/Activity.php
index 3dfb49aa..86e2bd31 100644
--- a/app/Facades/Activity.php
+++ b/app/Facades/Activity.php
@@ -10,11 +10,10 @@ use Pterodactyl\Services\Activity\ActivityLogService;
  * @method static ActivityLogService anonymous()
  * @method static ActivityLogService event(string $action)
  * @method static ActivityLogService description(?string $description)
- * @method static ActivityLogService subject(Model $subject)
+ * @method static ActivityLogService subject(Model|Model[] $subject)
  * @method static ActivityLogService actor(Model $actor)
- * @method static ActivityLogService withProperties(\Illuminate\Support\Collection|array $properties)
  * @method static ActivityLogService withRequestMetadata()
- * @method static ActivityLogService property(string $key, mixed $value)
+ * @method static ActivityLogService property(string|array $key, mixed $value = null)
  * @method static \Pterodactyl\Models\ActivityLog log(string $description = null)
  * @method static ActivityLogService clone()
  * @method static mixed transaction(\Closure $callback)
diff --git a/app/Http/Controllers/Api/Client/Servers/BackupController.php b/app/Http/Controllers/Api/Client/Servers/BackupController.php
index 67b54cd2..bfd9b68b 100644
--- a/app/Http/Controllers/Api/Client/Servers/BackupController.php
+++ b/app/Http/Controllers/Api/Client/Servers/BackupController.php
@@ -5,8 +5,8 @@ namespace Pterodactyl\Http\Controllers\Api\Client\Servers;
 use Illuminate\Http\Request;
 use Pterodactyl\Models\Backup;
 use Pterodactyl\Models\Server;
-use Pterodactyl\Models\AuditLog;
 use Illuminate\Http\JsonResponse;
+use Pterodactyl\Facades\Activity;
 use Pterodactyl\Models\Permission;
 use Illuminate\Auth\Access\AuthorizationException;
 use Pterodactyl\Services\Backups\DeleteBackupService;
@@ -77,25 +77,23 @@ class BackupController extends ClientApiController
      */
     public function store(StoreBackupRequest $request, Server $server): array
     {
-        /** @var \Pterodactyl\Models\Backup $backup */
-        $backup = $server->audit(AuditLog::SERVER__BACKUP_STARTED, function (AuditLog $model, Server $server) use ($request) {
-            $action = $this->initiateBackupService
-                ->setIgnoredFiles(explode(PHP_EOL, $request->input('ignored') ?? ''));
+        $action = $this->initiateBackupService
+            ->setIgnoredFiles(explode(PHP_EOL, $request->input('ignored') ?? ''));
 
-            // Only set the lock status if the user even has permission to delete backups,
-            // otherwise ignore this status. This gets a little funky since it isn't clear
-            // how best to allow a user to create a backup that is locked without also preventing
-            // them from just filling up a server with backups that can never be deleted?
-            if ($request->user()->can(Permission::ACTION_BACKUP_DELETE, $server)) {
-                $action->setIsLocked((bool) $request->input('is_locked'));
-            }
+        // Only set the lock status if the user even has permission to delete backups,
+        // otherwise ignore this status. This gets a little funky since it isn't clear
+        // how best to allow a user to create a backup that is locked without also preventing
+        // them from just filling up a server with backups that can never be deleted?
+        if ($request->user()->can(Permission::ACTION_BACKUP_DELETE, $server)) {
+            $action->setIsLocked((bool) $request->input('is_locked'));
+        }
 
-            $backup = $action->handle($server, $request->input('name'));
+        $backup = $action->handle($server, $request->input('name'));
 
-            $model->metadata = ['backup_uuid' => $backup->uuid];
-
-            return $backup;
-        });
+        Activity::event('server:backup.start')
+            ->subject($backup)
+            ->property(['name' => $backup->name, 'locked' => (bool) $request->input('is_locked')])
+            ->log();
 
         return $this->fractal->item($backup)
             ->transformWith($this->getTransformer(BackupTransformer::class))
@@ -114,14 +112,11 @@ class BackupController extends ClientApiController
             throw new AuthorizationException();
         }
 
-        $action = $backup->is_locked ? AuditLog::SERVER__BACKUP_UNLOCKED : AuditLog::SERVER__BACKUP_LOCKED;
-        $server->audit($action, function (AuditLog $audit) use ($backup) {
-            $audit->metadata = ['backup_uuid' => $backup->uuid];
+        $action = $backup->is_locked ? 'server:backup.unlock' : 'server:backup.lock';
 
-            $backup->update(['is_locked' => !$backup->is_locked]);
-        });
+        $backup->update(['is_locked' => !$backup->is_locked]);
 
-        $backup->refresh();
+        Activity::event($action)->subject($backup)->property('name', $backup->name)->log();
 
         return $this->fractal->item($backup)
             ->transformWith($this->getTransformer(BackupTransformer::class))
@@ -156,11 +151,12 @@ class BackupController extends ClientApiController
             throw new AuthorizationException();
         }
 
-        $server->audit(AuditLog::SERVER__BACKUP_DELETED, function (AuditLog $audit) use ($backup) {
-            $audit->metadata = ['backup_uuid' => $backup->uuid];
+        $this->deleteBackupService->handle($backup);
 
-            $this->deleteBackupService->handle($backup);
-        });
+        Activity::event('server:backup.delete')
+            ->subject($backup)
+            ->property(['name' => $backup->name, 'failed' => !$backup->is_successful])
+            ->log();
 
         return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
     }
@@ -184,9 +180,8 @@ class BackupController extends ClientApiController
         }
 
         $url = $this->downloadLinkService->handle($backup, $request->user());
-        $server->audit(AuditLog::SERVER__BACKUP_DOWNLOADED, function (AuditLog $audit) use ($backup) {
-            $audit->metadata = ['backup_uuid' => $backup->uuid];
-        });
+
+        Activity::event('server:backup.download')->subject($backup)->property('name', $backup->name)->log();
 
         return new JsonResponse([
             'object' => 'signed_url',
@@ -221,9 +216,11 @@ class BackupController extends ClientApiController
             throw new BadRequestHttpException('This backup cannot be restored at this time: not completed or failed.');
         }
 
-        $server->audit(AuditLog::SERVER__BACKUP_RESTORE_STARTED, function (AuditLog $audit, Server $server) use ($backup, $request) {
-            $audit->metadata = ['backup_uuid' => $backup->uuid];
+        $log = Activity::event('server:backup.restore')
+            ->subject($backup)
+            ->property(['name' => $backup->name, 'truncate' => $request->input('truncate')]);
 
+        $log->transaction(function () use ($backup, $server, $request) {
             // If the backup is for an S3 file we need to generate a unique Download link for
             // it that will allow Wings to actually access the file.
             if ($backup->disk === Backup::ADAPTER_AWS_S3) {
diff --git a/app/Http/Controllers/Api/Remote/Backups/BackupStatusController.php b/app/Http/Controllers/Api/Remote/Backups/BackupStatusController.php
index f4e7bd6e..17f62329 100644
--- a/app/Http/Controllers/Api/Remote/Backups/BackupStatusController.php
+++ b/app/Http/Controllers/Api/Remote/Backups/BackupStatusController.php
@@ -5,9 +5,8 @@ namespace Pterodactyl\Http\Controllers\Api\Remote\Backups;
 use Carbon\CarbonImmutable;
 use Illuminate\Http\Request;
 use Pterodactyl\Models\Backup;
-use Pterodactyl\Models\Server;
-use Pterodactyl\Models\AuditLog;
 use Illuminate\Http\JsonResponse;
+use Pterodactyl\Facades\Activity;
 use League\Flysystem\AwsS3v3\AwsS3Adapter;
 use Pterodactyl\Exceptions\DisplayException;
 use Pterodactyl\Http\Controllers\Controller;
@@ -46,15 +45,12 @@ class BackupStatusController extends Controller
             throw new BadRequestHttpException('Cannot update the status of a backup that is already marked as completed.');
         }
 
-        $action = $request->input('successful')
-            ? AuditLog::SERVER__BACKUP_COMPELTED
-            : AuditLog::SERVER__BACKUP_FAILED;
-
-        $model->server->audit($action, function (AuditLog $audit) use ($model, $request) {
-            $audit->is_system = true;
-            $audit->metadata = ['backup_uuid' => $model->uuid];
+        $action = $request->boolean('successful') ? 'server:backup.complete' : 'server:backup.failed';
+        $log = Activity::event($action)->subject($model, $model->server)->property('name', $model->name);
 
+        $log->transaction(function () use ($model, $request) {
             $successful = $request->boolean('successful');
+
             $model->fill([
                 'is_successful' => $successful,
                 // Change the lock state to unlocked if this was a failed backup so that it can be
@@ -93,17 +89,13 @@ class BackupStatusController extends Controller
     {
         /** @var \Pterodactyl\Models\Backup $model */
         $model = Backup::query()->where('uuid', $backup)->firstOrFail();
-        $action = $request->get('successful')
-            ? AuditLog::SERVER__BACKUP_RESTORE_COMPLETED
-            : AuditLog::SERVER__BACKUP_RESTORE_FAILED;
 
-        // Just create a new audit entry for this event and update the server state
-        // so that power actions, file management, and backups can resume as normal.
-        $model->server->audit($action, function (AuditLog $audit, Server $server) use ($backup) {
-            $audit->is_system = true;
-            $audit->metadata = ['backup_uuid' => $backup];
-            $server->update(['status' => null]);
-        });
+        $model->server->update(['status' => null]);
+
+        Activity::event($request->boolean('successful') ? 'server:backup.restore-complete' : 'server.backup.restore-failed')
+            ->subject($model, $model->server)
+            ->property('name', $model->name)
+            ->log();
 
         return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
     }
diff --git a/app/Http/Controllers/Api/Remote/Servers/ServerDetailsController.php b/app/Http/Controllers/Api/Remote/Servers/ServerDetailsController.php
index 6b49fafa..0e93c60c 100644
--- a/app/Http/Controllers/Api/Remote/Servers/ServerDetailsController.php
+++ b/app/Http/Controllers/Api/Remote/Servers/ServerDetailsController.php
@@ -4,8 +4,10 @@ namespace Pterodactyl\Http\Controllers\Api\Remote\Servers;
 
 use Illuminate\Http\Request;
 use Pterodactyl\Models\Server;
+use Pterodactyl\Models\Backup;
 use Pterodactyl\Models\AuditLog;
 use Illuminate\Http\JsonResponse;
+use Pterodactyl\Facades\Activity;
 use Illuminate\Database\Query\Builder;
 use Illuminate\Database\Query\JoinClause;
 use Pterodactyl\Http\Controllers\Controller;
@@ -107,7 +109,6 @@ class ServerDetailsController extends Controller
         //
         // For each of those servers we'll track a new audit log entry to mark them as
         // failed and then update them all to be in a valid state.
-        /** @var \Pterodactyl\Models\Server[] $servers */
         $servers = Server::query()
             ->select('servers.*')
             ->selectRaw('JSON_UNQUOTE(JSON_EXTRACT(started.metadata, "$.backup_uuid")) as backup_uuid')
@@ -130,14 +131,17 @@ class ServerDetailsController extends Controller
             ->where('servers.status', Server::STATUS_RESTORING_BACKUP)
             ->get();
 
+        $backups = Backup::query()->whereIn('uuid', $servers->pluck('backup_uuid'))->get();
+
+        /** @var \Pterodactyl\Models\Server $server */
         foreach ($servers as $server) {
-            // Just create a new audit entry for this event and update the server state
-            // so that power actions, file management, and backups can resume as normal.
-            $server->audit(AuditLog::SERVER__BACKUP_RESTORE_FAILED, function (AuditLog $audit, Server $server) {
-                $audit->is_system = true;
-                $audit->metadata = ['backup_uuid' => $server->getAttribute('backup_uuid')];
-                $server->update(['status' => null]);
-            });
+            $server->update(['status' => null]);
+
+            if ($backup = $backups->where('uuid', $server->getAttribute('backup_uuid'))->first()) {
+                // Just create a new audit entry for this event and update the server state
+                // so that power actions, file management, and backups can resume as normal.
+                Activity::event('server:backup.restore-failed')->subject($server, $backup)->log();
+            }
         }
 
         // Update any server marked as installing or restoring as being in a normal state
diff --git a/app/Models/ActivityLog.php b/app/Models/ActivityLog.php
index 47ef911c..c8be7249 100644
--- a/app/Models/ActivityLog.php
+++ b/app/Models/ActivityLog.php
@@ -16,16 +16,13 @@ use Illuminate\Database\Eloquent\Model as IlluminateModel;
  * @property string|null $description
  * @property string|null $actor_type
  * @property int|null $actor_id
- * @property string|null $subject_type
- * @property int|null $subject_id
  * @property \Illuminate\Support\Collection $properties
  * @property string $timestamp
  * @property IlluminateModel|\Eloquent $actor
  * @property IlluminateModel|\Eloquent $subject
  *
- * @method static Builder|ActivityLog forAction(string $action)
+ * @method static Builder|ActivityLog forEvent(string $event)
  * @method static Builder|ActivityLog forActor(\Illuminate\Database\Eloquent\Model $actor)
- * @method static Builder|ActivityLog forSubject(\Illuminate\Database\Eloquent\Model $subject)
  * @method static Builder|ActivityLog newModelQuery()
  * @method static Builder|ActivityLog newQuery()
  * @method static Builder|ActivityLog query()
@@ -37,8 +34,6 @@ use Illuminate\Database\Eloquent\Model as IlluminateModel;
  * @method static Builder|ActivityLog whereId($value)
  * @method static Builder|ActivityLog whereIp($value)
  * @method static Builder|ActivityLog whereProperties($value)
- * @method static Builder|ActivityLog whereSubjectId($value)
- * @method static Builder|ActivityLog whereSubjectType($value)
  * @method static Builder|ActivityLog whereTimestamp($value)
  * @mixin \Eloquent
  */
@@ -68,14 +63,9 @@ class ActivityLog extends Model
         return $this->morphTo();
     }
 
-    public function subject(): MorphTo
+    public function scopeForEvent(Builder $builder, string $action): Builder
     {
-        return $this->morphTo();
-    }
-
-    public function scopeForAction(Builder $builder, string $action): Builder
-    {
-        return $builder->where('action', $action);
+        return $builder->where('event', $action);
     }
 
     /**
@@ -85,12 +75,4 @@ class ActivityLog extends Model
     {
         return $builder->whereMorphedTo('actor', $actor);
     }
-
-    /**
-     * Scopes a query to only return results where the subject is the given model.
-     */
-    public function scopeForSubject(Builder $builder, IlluminateModel $subject): Builder
-    {
-        return $builder->whereMorphedTo('subject', $subject);
-    }
 }
diff --git a/app/Models/ActivityLogSubject.php b/app/Models/ActivityLogSubject.php
new file mode 100644
index 00000000..47264dbd
--- /dev/null
+++ b/app/Models/ActivityLogSubject.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace Pterodactyl\Models;
+
+use Illuminate\Database\Eloquent\Relations\Pivot;
+
+/**
+ * \Pterodactyl\Models\ActivityLogSubject.
+ *
+ * @property int $id
+ * @property int $activity_log_id
+ * @property int $subject_id
+ * @property string $subject_type
+ * @property \Pterodactyl\Models\ActivityLog|null $activityLog
+ * @property \Illuminate\Database\Eloquent\Model|\Eloquent $subject
+ *
+ * @method static \Illuminate\Database\Eloquent\Builder|ActivityLogSubject newModelQuery()
+ * @method static \Illuminate\Database\Eloquent\Builder|ActivityLogSubject newQuery()
+ * @method static \Illuminate\Database\Eloquent\Builder|ActivityLogSubject query()
+ * @mixin \Eloquent
+ */
+class ActivityLogSubject extends Pivot
+{
+    public $incrementing = true;
+    public $timestamps = false;
+
+    protected $table = 'activity_log_subjects';
+
+    protected $guarded = ['id'];
+
+    public function activityLog()
+    {
+        return $this->belongsTo(ActivityLog::class);
+    }
+
+    public function subject()
+    {
+        return $this->morphTo();
+    }
+}
diff --git a/app/Models/Server.php b/app/Models/Server.php
index b7131583..7fe28674 100644
--- a/app/Models/Server.php
+++ b/app/Models/Server.php
@@ -2,10 +2,10 @@
 
 namespace Pterodactyl\Models;
 
-use Closure;
 use Illuminate\Notifications\Notifiable;
 use Illuminate\Database\Query\JoinClause;
 use Znck\Eloquent\Traits\BelongsToThrough;
+use Illuminate\Database\Eloquent\Relations\MorphToMany;
 use Pterodactyl\Exceptions\Http\Server\ServerStateConflictException;
 
 /**
@@ -41,8 +41,6 @@ use Pterodactyl\Exceptions\Http\Server\ServerStateConflictException;
  * @property \Pterodactyl\Models\Allocation|null $allocation
  * @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\Allocation[] $allocations
  * @property int|null $allocations_count
- * @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\AuditLog[] $audits
- * @property int|null $audits_count
  * @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\Backup[] $backups
  * @property int|null $backups_count
  * @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\Database[] $databases
@@ -373,48 +371,11 @@ class Server extends Model
     }
 
     /**
-     * Returns a fresh AuditLog model for the server. This model is not saved to the
-     * database when created, so it is up to the caller to correctly store it as needed.
-     *
-     * @return \Pterodactyl\Models\AuditLog
+     * Returns all of the activity log entries where the server is the subject.
      */
-    public function newAuditEvent(string $action, array $metadata = []): AuditLog
+    public function activity(): MorphToMany
     {
-        return AuditLog::instance($action, $metadata)->fill([
-            'server_id' => $this->id,
-        ]);
-    }
-
-    /**
-     * Stores a new audit event for a server by using a transaction. If the transaction
-     * fails for any reason everything executed within will be rolled back. The callback
-     * passed in will receive the AuditLog model before it is saved and the second argument
-     * will be the current server instance. The callback should modify the audit entry as
-     * needed before finishing, any changes will be persisted.
-     *
-     * The response from the callback is returned to the caller.
-     *
-     * @return mixed
-     *
-     * @throws \Throwable
-     */
-    public function audit(string $action, Closure $callback)
-    {
-        return $this->getConnection()->transaction(function () use ($action, $callback) {
-            $model = $this->newAuditEvent($action);
-            $response = $callback($model, $this);
-            $model->save();
-
-            return $response;
-        });
-    }
-
-    /**
-     * @return \Illuminate\Database\Eloquent\Relations\HasMany
-     */
-    public function audits()
-    {
-        return $this->hasMany(AuditLog::class);
+        return $this->morphToMany(ActivityLog::class, 'subject', 'activity_log_subjects');
     }
 
     /**
diff --git a/app/Models/User.php b/app/Models/User.php
index c8fe6421..0570bbf3 100644
--- a/app/Models/User.php
+++ b/app/Models/User.php
@@ -14,6 +14,7 @@ use Illuminate\Auth\Passwords\CanResetPassword;
 use Pterodactyl\Traits\Helpers\AvailableLanguages;
 use Illuminate\Database\Eloquent\Relations\HasMany;
 use Illuminate\Foundation\Auth\Access\Authorizable;
+use Illuminate\Database\Eloquent\Relations\MorphToMany;
 use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
 use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
 use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
@@ -273,6 +274,15 @@ class User extends Model implements
         return $this->hasMany(UserSSHKey::class);
     }
 
+    /**
+     * Returns all of the activity logs where this user is the subject — not to
+     * be confused by activity logs where this user is the _actor_.
+     */
+    public function activity(): MorphToMany
+    {
+        return $this->morphToMany(ActivityLog::class, 'subject', 'activity_log_subjects');
+    }
+
     /**
      * Returns all of the servers that a user can access by way of being the owner of the
      * server, or because they are assigned as a subuser for that server.
diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php
index 51fa6e7f..a55aa297 100644
--- a/app/Providers/AppServiceProvider.php
+++ b/app/Providers/AppServiceProvider.php
@@ -5,11 +5,15 @@ namespace Pterodactyl\Providers;
 use View;
 use Cache;
 use Illuminate\Support\Str;
+use Pterodactyl\Models\User;
+use Pterodactyl\Models\Server;
+use Pterodactyl\Models\Backup;
 use Illuminate\Support\Facades\URL;
 use Illuminate\Pagination\Paginator;
 use Illuminate\Support\Facades\Schema;
 use Illuminate\Support\ServiceProvider;
 use Pterodactyl\Extensions\Themes\Theme;
+use Illuminate\Database\Eloquent\Relations\Relation;
 
 class AppServiceProvider extends ServiceProvider
 {
@@ -33,6 +37,12 @@ class AppServiceProvider extends ServiceProvider
         if (Str::startsWith(config('app.url') ?? '', 'https://')) {
             URL::forceScheme('https');
         }
+
+        Relation::enforceMorphMap([
+            'backup' => Backup::class,
+            'server' => Server::class,
+            'user' => User::class,
+        ]);
     }
 
     /**
diff --git a/app/Services/Activity/ActivityLogService.php b/app/Services/Activity/ActivityLogService.php
index 8ad5af53..7c8227d2 100644
--- a/app/Services/Activity/ActivityLogService.php
+++ b/app/Services/Activity/ActivityLogService.php
@@ -2,20 +2,28 @@
 
 namespace Pterodactyl\Services\Activity;
 
+use Illuminate\Support\Arr;
+use Webmozart\Assert\Assert;
 use Illuminate\Support\Collection;
 use Pterodactyl\Models\ActivityLog;
 use Illuminate\Contracts\Auth\Factory;
 use Illuminate\Database\Eloquent\Model;
 use Illuminate\Support\Facades\Request;
+use Pterodactyl\Models\ActivityLogSubject;
 use Illuminate\Database\ConnectionInterface;
 
 class ActivityLogService
 {
     protected ?ActivityLog $activity = null;
 
+    protected array $subjects = [];
+
     protected Factory $manager;
+
     protected ConnectionInterface $connection;
+
     protected AcitvityLogBatchService $batch;
+
     protected ActivityLogTargetableService $targetable;
 
     public function __construct(
@@ -65,10 +73,22 @@ class ActivityLogService
 
     /**
      * Sets the subject model instance.
+     *
+     * @param \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Model[] $subjects
      */
-    public function subject(Model $subject): self
+    public function subject(...$subjects): self
     {
-        $this->getActivity()->subject()->associate($subject);
+        foreach (Arr::wrap($subjects) as $subject) {
+            foreach ($this->subjects as $entry) {
+                // If this subject is already tracked in our array of subjects just skip over
+                // it and move on to the next one in the list.
+                if ($entry->is($subject)) {
+                    continue 2;
+                }
+            }
+
+            $this->subjects[] = $subject;
+        }
 
         return $this;
     }
@@ -83,26 +103,18 @@ class ActivityLogService
         return $this;
     }
 
-    /**
-     * Sets the custom properties for the activity log instance.
-     *
-     * @param \Illuminate\Support\Collection|array $properties
-     */
-    public function withProperties($properties): self
-    {
-        $this->getActivity()->properties = Collection::make($properties);
-
-        return $this;
-    }
-
     /**
      * Sets a custom property on the activty log instance.
      *
+     * @param string|array $key
      * @param mixed $value
      */
-    public function property(string $key, $value): self
+    public function property($key, $value = null): self
     {
-        $this->getActivity()->properties = $this->getActivity()->properties->put($key, $value);
+        $properties = $this->getActivity()->properties;
+        $this->activity->properties = is_array($key)
+            ? $properties->merge($key)
+            : $properties->put($key, $value);
 
         return $this;
     }
@@ -112,10 +124,10 @@ class ActivityLogService
      */
     public function withRequestMetadata(): self
     {
-        $this->property('ip', Request::getClientIp());
-        $this->property('useragent', Request::userAgent());
-
-        return $this;
+        return $this->property([
+            'ip' => Request::getClientIp(),
+            'useragent' => Request::userAgent(),
+        ]);
     }
 
     /**
@@ -130,11 +142,7 @@ class ActivityLogService
             $activity->description = $description;
         }
 
-        $activity->save();
-
-        $this->activity = null;
-
-        return $activity;
+        return $this->save();
     }
 
     /**
@@ -155,17 +163,12 @@ class ActivityLogService
      *
      * @throws \Throwable
      */
-    public function transaction(\Closure $callback, string $description = null)
+    public function transaction(\Closure $callback)
     {
-        if (!is_null($description)) {
-            $this->description($description);
-        }
-
         return $this->connection->transaction(function () use ($callback) {
             $response = $callback($activity = $this->getActivity());
 
-            $activity->save();
-            $this->activity = null;
+            $this->save($activity);
 
             return $response;
         });
@@ -200,4 +203,38 @@ class ActivityLogService
 
         return $this->activity;
     }
+
+    /**
+     * Saves the activity log instance and attaches all of the subject models.
+     *
+     * @throws \Throwable
+     */
+    protected function save(ActivityLog $activity = null): ActivityLog
+    {
+        $activity = $activity ?? $this->activity;
+
+        Assert::notNull($activity);
+
+        $response = $this->connection->transaction(function () use ($activity) {
+            $activity->save();
+
+            $subjects = Collection::make($this->subjects)
+                ->map(fn (Model $subject) => [
+                    'activity_log_id' => $this->activity->id,
+                    'subject_id' => $subject->getKey(),
+                    'subject_type' => $subject->getMorphClass(),
+                ])
+                ->values()
+                ->toArray();
+
+            ActivityLogSubject::insert($subjects);
+
+            return $activity;
+        });
+
+        $this->activity = null;
+        $this->subjects = [];
+
+        return $response;
+    }
 }
diff --git a/database/migrations/2022_05_28_135717_create_activity_logs_table.php b/database/migrations/2022_05_28_135717_create_activity_logs_table.php
index 0624a5a6..448439dc 100644
--- a/database/migrations/2022_05_28_135717_create_activity_logs_table.php
+++ b/database/migrations/2022_05_28_135717_create_activity_logs_table.php
@@ -20,7 +20,6 @@ class CreateActivityLogsTable extends Migration
             $table->string('ip');
             $table->text('description')->nullable();
             $table->nullableNumericMorphs('actor');
-            $table->nullableNumericMorphs('subject');
             $table->json('properties');
             $table->timestamp('timestamp')->useCurrent()->onUpdate(null);
         });
diff --git a/database/migrations/2022_05_29_140349_create_activity_log_actors_table.php b/database/migrations/2022_05_29_140349_create_activity_log_actors_table.php
new file mode 100644
index 00000000..8be57bc1
--- /dev/null
+++ b/database/migrations/2022_05_29_140349_create_activity_log_actors_table.php
@@ -0,0 +1,32 @@
+<?php
+
+use Illuminate\Support\Facades\Schema;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Database\Migrations\Migration;
+
+class CreateActivityLogActorsTable extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::create('activity_log_subjects', function (Blueprint $table) {
+            $table->id();
+            $table->foreignId('activity_log_id')->references('id')->on('activity_logs');
+            $table->numericMorphs('subject');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::dropIfExists('activity_log_subject');
+    }
+}