diff --git a/app/Config/Events.php b/app/Config/Events.php
index 17713837901732e16a4dc5fdaa190dcc06801a19..da21d27df30206e1daedd7a8a954b8d915b448cd 100644
--- a/app/Config/Events.php
+++ b/app/Config/Events.php
@@ -81,6 +81,10 @@ Events::on('on_note_add', function ($note) {
             ->where('id', $note->episode_id)
             ->increment('notes_total');
     }
+
+    // Removing all of the podcast pages is a bit overkill, but works perfectly
+    // same for other events below
+    cache()->deleteMatching("page_podcast#{$note->actor->podcast->id}*");
 });
 
 Events::on('on_note_remove', function ($note) {
@@ -97,6 +101,9 @@ Events::on('on_note_remove', function ($note) {
             ->where('id', $note->episode_id)
             ->decrement('favourites_total', $note->favourites_count);
     }
+
+    cache()->deleteMatching("page_podcast#{$note->actor->podcast->id}*");
+    cache()->deleteMatching("page_note#{$note->id}*");
 });
 
 Events::on('on_note_reblog', function ($actor, $note) {
@@ -109,10 +116,18 @@ Events::on('on_note_reblog', function ($actor, $note) {
             ->where('id', $episodeId)
             ->increment('notes_total');
     }
+
+    cache()->deleteMatching("page_podcast#{$note->actor->podcast->id}*");
+    cache()->deleteMatching("page_note#{$note->id}*");
+
+    if ($actor->is_podcast) {
+        cache()->deleteMatching("page_podcast#{$actor->podcast->id}*");
+    }
 });
 
 Events::on('on_note_undo_reblog', function ($reblogNote) {
-    if ($episodeId = $reblogNote->reblog_of_note->episode_id) {
+    $note = $reblogNote->reblog_of_note;
+    if ($episodeId = $note->episode_id) {
         model('EpisodeModel')
             ->where('id', $episodeId)
             ->decrement('reblogs_total');
@@ -121,6 +136,29 @@ Events::on('on_note_undo_reblog', function ($reblogNote) {
             ->where('id', $episodeId)
             ->decrement('notes_total');
     }
+
+    cache()->deleteMatching("page_podcast#{$note->actor->podcast->id}*");
+    cache()->deleteMatching("page_note#{$note->id}*");
+
+    if ($reblogNote->actor->is_podcast) {
+        cache()->deleteMatching(
+            "page_podcast#{$reblogNote->actor->podcast->id}*",
+        );
+    }
+});
+
+Events::on('on_note_reply', function ($reply) {
+    $note = $reply->reply_to_note;
+
+    cache()->deleteMatching("page_podcast#{$note->actor->podcast->id}*");
+    cache()->deleteMatching("page_note#{$note->id}*");
+});
+
+Events::on('on_reply_remove', function ($reply) {
+    $note = $reply->reply_to_note;
+
+    cache()->deleteMatching("page_podcast#{$note->actor->podcast->id}*");
+    cache()->deleteMatching("page_note#{$note->id}*");
 });
 
 Events::on('on_note_favourite', function ($actor, $note) {
@@ -129,6 +167,13 @@ Events::on('on_note_favourite', function ($actor, $note) {
             ->where('id', $note->episode_id)
             ->increment('favourites_total');
     }
+
+    cache()->deleteMatching("page_podcast#{$actor->podcast->id}*");
+    cache()->deleteMatching("page_note#{$note->id}*");
+
+    if ($note->in_reply_to_id) {
+        cache()->deleteMatching("page_note#{$note->in_reply_to_id}*");
+    }
 });
 
 Events::on('on_note_undo_favourite', function ($actor, $note) {
@@ -137,4 +182,31 @@ Events::on('on_note_undo_favourite', function ($actor, $note) {
             ->where('id', $note->episode_id)
             ->decrement('favourites_total');
     }
+
+    cache()->deleteMatching("page_podcast#{$actor->podcast->id}*");
+    cache()->deleteMatching("page_note#{$note->id}*");
+
+    if ($note->in_reply_to_id) {
+        cache()->deleteMatching("page_note#{$note->in_reply_to_id}*");
+    }
+});
+
+Events::on('on_block_actor', function ($actorId) {
+    cache()->deleteMatching('page_podcast*');
+    cache()->deleteMatching('page_note*');
+});
+
+Events::on('on_unblock_actor', function ($actorId) {
+    cache()->deleteMatching('page_podcast*');
+    cache()->deleteMatching('page_note*');
+});
+
+Events::on('on_block_domain', function ($domainName) {
+    cache()->deleteMatching('page_podcast*');
+    cache()->deleteMatching('page_note*');
+});
+
+Events::on('on_unblock_domain', function ($domainName) {
+    cache()->deleteMatching('page_podcast*');
+    cache()->deleteMatching('page_note*');
 });
diff --git a/app/Controllers/Actor.php b/app/Controllers/Actor.php
index 6c91eda9c9d7af36b1a97a50f88f512cfa632959..34951ad28220b556f14595a313b0127320910abc 100644
--- a/app/Controllers/Actor.php
+++ b/app/Controllers/Actor.php
@@ -8,15 +8,32 @@
 
 namespace App\Controllers;
 
+use Analytics\AnalyticsTrait;
+
 class Actor extends \ActivityPub\Controllers\ActorController
 {
+    use AnalyticsTrait;
+
     public function follow()
     {
-        helper(['form', 'components', 'svg']);
-        $data = [
-            'actor' => $this->actor,
-        ];
+        // Prevent analytics hit when authenticated
+        if (!can_user_interact()) {
+            $this->registerPodcastWebpageHit($this->actor->podcast->id);
+        }
+
+        $cacheName = "page_podcast@{$this->actor->username}_follow";
+        if (!($cachedView = cache($cacheName))) {
+            helper(['form', 'components', 'svg']);
+            $data = [
+                'actor' => $this->actor,
+            ];
+
+            return view('podcast/follow', $data, [
+                'cache' => DECADE,
+                'cache_name' => $cacheName,
+            ]);
+        }
 
-        return view('podcast/follow', $data);
+        return $cachedView;
     }
 }
diff --git a/app/Controllers/BaseController.php b/app/Controllers/BaseController.php
index 126b71009de4f164437f45e62a8820ae6769f5da..df816124ae41e61afe113ed34c9fbf280ada5319 100644
--- a/app/Controllers/BaseController.php
+++ b/app/Controllers/BaseController.php
@@ -26,7 +26,7 @@ class BaseController extends Controller
      *
      * @var array
      */
-    protected $helpers = ['auth', 'analytics', 'svg', 'components', 'misc'];
+    protected $helpers = ['auth', 'svg', 'components', 'misc'];
 
     /**
      * Constructor.
@@ -47,15 +47,5 @@ class BaseController extends Controller
         // Preload any models, libraries, etc, here.
         //--------------------------------------------------------------------
         // E.g.: $this->session = \Config\Services::session();
-
-        set_user_session_deny_list_ip();
-        set_user_session_browser();
-        set_user_session_referer();
-        set_user_session_entry_page();
-    }
-
-    protected static function triggerWebpageHit($podcastId)
-    {
-        webpage_hit($podcastId);
     }
 }
diff --git a/app/Controllers/Episode.php b/app/Controllers/Episode.php
index 8448e7565fa3044fc12698911b4be38fc7afe5b4..f0533e764b02a20c2de13ed12b286eb2ee817c69 100644
--- a/app/Controllers/Episode.php
+++ b/app/Controllers/Episode.php
@@ -8,12 +8,15 @@
 
 namespace App\Controllers;
 
+use Analytics\AnalyticsTrait;
 use App\Models\EpisodeModel;
 use App\Models\PodcastModel;
 use SimpleXMLElement;
 
 class Episode extends BaseController
 {
+    use AnalyticsTrait;
+
     /**
      * @var \App\Entities\Podcast
      */
@@ -44,10 +47,15 @@ class Episode extends BaseController
 
     public function index()
     {
-        self::triggerWebpageHit($this->podcast->id);
+        // Prevent analytics hit when authenticated
+        if (!can_user_interact()) {
+            $this->registerPodcastWebpageHit($this->episode->podcast_id);
+        }
 
         $locale = service('request')->getLocale();
-        $cacheName = "page_podcast#{$this->podcast->id}_episode{$this->episode->id}_{$locale}";
+        $cacheName =
+            "page_podcast#{$this->podcast->id}_episode#{$this->episode->id}_{$locale}" .
+            (can_user_interact() ? '_authenticated' : '');
 
         if (!($cachedView = cache($cacheName))) {
             helper('persons');
@@ -69,13 +77,7 @@ class Episode extends BaseController
 
             if (can_user_interact()) {
                 helper('form');
-                // The page cache is set to a decade so it is deleted manually upon podcast update
-                return view('podcast/episode_authenticated', $data, [
-                    'cache' => $secondsToNextUnpublishedEpisode
-                        ? $secondsToNextUnpublishedEpisode
-                        : DECADE,
-                    'cache_name' => $cacheName . '_authenticated',
-                ]);
+                return view('podcast/episode_authenticated', $data);
             } else {
                 // The page cache is set to a decade so it is deleted manually upon podcast update
                 return view('podcast/episode', $data, [
@@ -94,7 +96,10 @@ class Episode extends BaseController
     {
         header('Content-Security-Policy: frame-ancestors https://* http://*');
 
-        self::triggerWebpageHit($this->episode->podcast_id);
+        // Prevent analytics hit when authenticated
+        if (!can_user_interact()) {
+            $this->registerPodcastWebpageHit($this->episode->podcast_id);
+        }
 
         $session = \Config\Services::session();
         $session->start();
@@ -107,7 +112,7 @@ class Episode extends BaseController
 
         $locale = service('request')->getLocale();
 
-        $cacheName = "page_podcast#{$this->podcast->id}_episode{$this->episode->id}_embeddable_player_{$theme}_{$locale}";
+        $cacheName = "page_podcast#{$this->podcast->id}_episode#{$this->episode->id}_embeddable_player_{$theme}_{$locale}";
 
         if (!($cachedView = cache($cacheName))) {
             $theme = EpisodeModel::$themes[$theme];
diff --git a/app/Controllers/Note.php b/app/Controllers/Note.php
index c685a690e0c9b90e4ec06123f88d5e6990e3ae89..509c78746a02ac1f7a71318cb34fc93dfa0ae0da 100644
--- a/app/Controllers/Note.php
+++ b/app/Controllers/Note.php
@@ -8,6 +8,7 @@
 
 namespace App\Controllers;
 
+use Analytics\AnalyticsTrait;
 use App\Models\EpisodeModel;
 use App\Models\PodcastModel;
 use CodeIgniter\HTTP\URI;
@@ -15,6 +16,8 @@ use CodeIgniter\I18n\Time;
 
 class Note extends \ActivityPub\Controllers\NoteController
 {
+    use AnalyticsTrait;
+
     /**
      * @var \App\Entities\Podcast
      */
@@ -47,24 +50,46 @@ class Note extends \ActivityPub\Controllers\NoteController
 
     public function index()
     {
-        helper('persons');
-        $persons = [];
-        construct_person_array($this->podcast->persons, $persons);
-
-        $data = [
-            'podcast' => $this->podcast,
-            'actor' => $this->actor,
-            'note' => $this->note,
-            'persons' => $persons,
-        ];
+        // Prevent analytics hit when authenticated
+        if (!can_user_interact()) {
+            $this->registerPodcastWebpageHit($this->podcast->id);
+        }
 
-        // if user is logged in then send to the authenticated activity view
-        if (can_user_interact()) {
-            helper('form');
-            return view('podcast/note_authenticated', $data);
-        } else {
-            return view('podcast/note', $data);
+        $cacheName = implode(
+            '_',
+            array_filter([
+                'page',
+                "note#{$this->note->id}",
+                service('request')->getLocale(),
+                can_user_interact() ? '_authenticated' : null,
+            ]),
+        );
+
+        if (!($cachedView = cache($cacheName))) {
+            helper('persons');
+            $persons = [];
+            construct_person_array($this->podcast->persons, $persons);
+
+            $data = [
+                'podcast' => $this->podcast,
+                'actor' => $this->actor,
+                'note' => $this->note,
+                'persons' => $persons,
+            ];
+
+            // if user is logged in then send to the authenticated activity view
+            if (can_user_interact()) {
+                helper('form');
+                return view('podcast/note_authenticated', $data);
+            } else {
+                return view('podcast/note', $data, [
+                    'cache' => DECADE,
+                    'cache_name' => $cacheName,
+                ]);
+            }
         }
+
+        return $cachedView;
     }
 
     public function attemptCreate()
@@ -198,15 +223,37 @@ class Note extends \ActivityPub\Controllers\NoteController
 
     public function remoteAction($action)
     {
-        $data = [
-            'podcast' => $this->podcast,
-            'actor' => $this->actor,
-            'note' => $this->note,
-            'action' => $action,
-        ];
+        // Prevent analytics hit when authenticated
+        if (!can_user_interact()) {
+            $this->registerPodcastWebpageHit($this->podcast->id);
+        }
+
+        $cacheName = implode(
+            '_',
+            array_filter([
+                'page',
+                "note#{$this->note->id}",
+                "remote_{$action}",
+                service('request')->getLocale(),
+            ]),
+        );
 
-        helper('form');
+        if (!($cachedView = cache($cacheName))) {
+            $data = [
+                'podcast' => $this->podcast,
+                'actor' => $this->actor,
+                'note' => $this->note,
+                'action' => $action,
+            ];
+
+            helper('form');
+
+            return view('podcast/note_remote_action', $data, [
+                'cache' => DECADE,
+                'cache_name' => $cacheName,
+            ]);
+        }
 
-        return view('podcast/note_remote_action', $data);
+        return $cachedView;
     }
 }
diff --git a/app/Controllers/Page.php b/app/Controllers/Page.php
index 8f97b5de99a58a35bd6d57d02d10437a6c538b2c..73c452dfe9d18131c2d58f2d098ea5c77775147c 100644
--- a/app/Controllers/Page.php
+++ b/app/Controllers/Page.php
@@ -56,7 +56,7 @@ class Page extends BaseController
         $locale = service('request')->getLocale();
         $allPodcasts = (new PodcastModel())->findAll();
 
-        $cacheName = "paĝe_credits_{$locale}";
+        $cacheName = "page_credits_{$locale}";
         if (!($found = cache($cacheName))) {
             $page = new \App\Entities\Page([
                 'title' => lang('Person.credits', [], $locale),
diff --git a/app/Controllers/Podcast.php b/app/Controllers/Podcast.php
index 2b351e73d24992b5ea0516d145870a36031e2845..c9e735805c656fb1777704af3092fde3f5127ef9 100644
--- a/app/Controllers/Podcast.php
+++ b/app/Controllers/Podcast.php
@@ -8,12 +8,15 @@
 
 namespace App\Controllers;
 
+use Analytics\AnalyticsTrait;
 use App\Models\EpisodeModel;
 use App\Models\PodcastModel;
 use App\Models\NoteModel;
 
 class Podcast extends BaseController
 {
+    use AnalyticsTrait;
+
     /**
      * @var \App\Entities\Podcast|null
      */
@@ -37,32 +40,56 @@ class Podcast extends BaseController
 
     public function activity()
     {
-        self::triggerWebpageHit($this->podcast->id);
-
-        helper('persons');
-        $persons = [];
-        construct_person_array($this->podcast->persons, $persons);
-
-        $data = [
-            'podcast' => $this->podcast,
-            'notes' => (new NoteModel())->getActorNotes(
-                $this->podcast->actor_id,
-            ),
-            'persons' => $persons,
-        ];
-
-        // if user is logged in then send to the authenticated activity view
-        if (can_user_interact()) {
-            helper('form');
-            return view('podcast/activity_authenticated', $data);
-        } else {
-            return view('podcast/activity', $data);
+        // Prevent analytics hit when authenticated
+        if (!can_user_interact()) {
+            $this->registerPodcastWebpageHit($this->podcast->id);
+        }
+
+        $cacheName = implode(
+            '_',
+            array_filter([
+                'page',
+                "podcast#{$this->podcast->id}",
+                'activity',
+                service('request')->getLocale(),
+                can_user_interact() ? '_authenticated' : null,
+            ]),
+        );
+
+        if (!($cachedView = cache($cacheName))) {
+            helper('persons');
+            $persons = [];
+            construct_person_array($this->podcast->persons, $persons);
+
+            $data = [
+                'podcast' => $this->podcast,
+                'notes' => (new NoteModel())->getActorPublishedNotes(
+                    $this->podcast->actor_id,
+                ),
+                'persons' => $persons,
+            ];
+
+            // if user is logged in then send to the authenticated activity view
+            if (can_user_interact()) {
+                helper('form');
+                return view('podcast/activity_authenticated', $data);
+            } else {
+                return view('podcast/activity', $data, [
+                    'cache' => DECADE,
+                    'cache_name' => $cacheName,
+                ]);
+            }
         }
+
+        return $cachedView;
     }
 
     public function episodes()
     {
-        self::triggerWebpageHit($this->podcast->id);
+        // Prevent analytics hit when authenticated
+        if (!can_user_interact()) {
+            $this->registerPodcastWebpageHit($this->podcast->id);
+        }
 
         $yearQuery = $this->request->getGet('year');
         $seasonQuery = $this->request->getGet('season');
@@ -85,14 +112,15 @@ class Podcast extends BaseController
             array_filter([
                 'page',
                 "podcast#{$this->podcast->id}",
+                'episodes',
                 $yearQuery ? 'year' . $yearQuery : null,
                 $seasonQuery ? 'season' . $seasonQuery : null,
                 service('request')->getLocale(),
-                can_user_interact() ? '_interact' : '',
+                can_user_interact() ? '_authenticated' : null,
             ]),
         );
 
-        if (!($found = cache($cacheName))) {
+        if (!($cachedView = cache($cacheName))) {
             // Build navigation array
             $podcastModel = new PodcastModel();
             $years = $podcastModel->getYears($this->podcast->id);
@@ -171,14 +199,9 @@ class Podcast extends BaseController
 
             // if user is logged in then send to the authenticated episodes view
             if (can_user_interact()) {
-                $found = view('podcast/episodes_authenticated', $data, [
-                    'cache' => $secondsToNextUnpublishedEpisode
-                        ? $secondsToNextUnpublishedEpisode
-                        : DECADE,
-                    'cache_name' => $cacheName,
-                ]);
+                return view('podcast/episodes_authenticated', $data);
             } else {
-                $found = view('podcast/episodes', $data, [
+                return view('podcast/episodes', $data, [
                     'cache' => $secondsToNextUnpublishedEpisode
                         ? $secondsToNextUnpublishedEpisode
                         : DECADE,
@@ -187,6 +210,6 @@ class Podcast extends BaseController
             }
         }
 
-        return $found;
+        return $cachedView;
     }
 }
diff --git a/app/Database/Migrations/2020-05-30-101500_add_podcasts.php b/app/Database/Migrations/2020-05-30-101500_add_podcasts.php
index 0eddb82f0528b0db5613e8a6640db0aae9a95105..d052c278afd1dd364f11a46614646aa08696930e 100644
--- a/app/Database/Migrations/2020-05-30-101500_add_podcasts.php
+++ b/app/Database/Migrations/2020-05-30-101500_add_podcasts.php
@@ -187,7 +187,9 @@ class AddPodcasts extends Migration
         ]);
 
         $this->forge->addPrimaryKey('id');
+        // TODO: remove name in favor of username from actor
         $this->forge->addUniqueKey('name');
+        $this->forge->addUniqueKey('actor_id');
         $this->forge->addForeignKey(
             'actor_id',
             'activitypub_actors',
diff --git a/app/Entities/Actor.php b/app/Entities/Actor.php
new file mode 100644
index 0000000000000000000000000000000000000000..78515a706fa0f126bcf858bb72a431f09c34c75c
--- /dev/null
+++ b/app/Entities/Actor.php
@@ -0,0 +1,46 @@
+<?php
+
+/**
+ * @copyright  2020 Podlibre
+ * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
+ * @link       https://castopod.org/
+ */
+
+namespace App\Entities;
+
+use App\Models\PodcastModel;
+
+class Actor extends \ActivityPub\Entities\Actor
+{
+    /**
+     * @var App\Entities\Podcast|null
+     */
+    protected $podcast;
+
+    /**
+     * @var boolean
+     */
+    protected $is_podcast;
+
+    public function getIsPodcast()
+    {
+        return !empty($this->podcast);
+    }
+
+    public function getPodcast()
+    {
+        if (empty($this->id)) {
+            throw new \RuntimeException(
+                'Actor must be created before getting associated podcast.',
+            );
+        }
+
+        if (empty($this->podcast)) {
+            $this->podcast = (new PodcastModel())->getPodcastByActorId(
+                $this->id,
+            );
+        }
+
+        return $this->podcast;
+    }
+}
diff --git a/app/Entities/Category.php b/app/Entities/Category.php
index e233f5a5e07687fc5da742a8921c388f8b3df08c..d58e143b480a5c9d2d8d2aa0bb766d8db764f9a0 100644
--- a/app/Entities/Category.php
+++ b/app/Entities/Category.php
@@ -31,7 +31,7 @@ class Category extends Entity
         $parentId = $this->attributes['parent_id'];
 
         return $parentId != 0
-            ? (new CategoryModel())->findParent($parentId)
+            ? (new CategoryModel())->getCategoryById($parentId)
             : null;
     }
 }
diff --git a/app/Entities/Podcast.php b/app/Entities/Podcast.php
index 8bce314e5a685ba12d9ca8b7527a7027a4093563..db7af33717e52155fb41ef43f7ea3eeae9c5ed74 100644
--- a/app/Entities/Podcast.php
+++ b/app/Entities/Podcast.php
@@ -143,7 +143,7 @@ class Podcast extends Entity
         }
 
         if (empty($this->actor)) {
-            $this->actor = (new ActorModel())->getActorById($this->actor_id);
+            $this->actor = model('ActorModel')->getActorById($this->actor_id);
         }
 
         return $this->actor;
@@ -254,7 +254,9 @@ class Podcast extends Entity
         }
 
         if (empty($this->category)) {
-            $this->category = (new CategoryModel())->find($this->category_id);
+            $this->category = (new CategoryModel())->getCategoryById(
+                $this->category_id,
+            );
         }
 
         return $this->category;
diff --git a/app/Helpers/auth_helper.php b/app/Helpers/auth_helper.php
index d1d095baaf0a96bb042c707c43461b72e80a9732..4408c0aa7cb4dffa85cc3ad3b230861f6b26f172 100644
--- a/app/Helpers/auth_helper.php
+++ b/app/Helpers/auth_helper.php
@@ -6,7 +6,6 @@
  * @link       https://castopod.org/
  */
 
-use ActivityPub\Models\ActorModel;
 use CodeIgniter\Database\Exceptions\DataException;
 use Config\Services;
 
@@ -68,7 +67,7 @@ if (!function_exists('interact_as_actor')) {
 
         $session = session();
         if ($session->has('interact_as_actor_id')) {
-            return (new ActorModel())->getActorById(
+            return model('ActorModel')->getActorById(
                 $session->get('interact_as_actor_id'),
             );
         }
diff --git a/app/Helpers/media_helper.php b/app/Helpers/media_helper.php
index d82720a07296c5e7f7d5b5c4250c3756d7852933..a7e2eb6926a53329d1d870c00d0b78aad12ee0c5 100644
--- a/app/Helpers/media_helper.php
+++ b/app/Helpers/media_helper.php
@@ -7,7 +7,6 @@
  */
 
 use CodeIgniter\Files\File;
-use CodeIgniter\HTTP\Exceptions\HTTPException;
 use CodeIgniter\HTTP\ResponseInterface;
 
 /**
@@ -42,8 +41,6 @@ function save_media($file, $folder, $mediaName)
  */
 function download_file($fileUrl)
 {
-    var_dump($fileUrl);
-
     $client = \Config\Services::curlrequest();
 
     $response = $client->get($fileUrl, [
diff --git a/app/Libraries/ActivityPub/Config/ActivityPub.php b/app/Libraries/ActivityPub/Config/ActivityPub.php
index 0feb9f162f9e1a8eae2311c9f56e28d0c87c58f1..e96460d47d783d192e50490eb1a9d59ca93150c9 100644
--- a/app/Libraries/ActivityPub/Config/ActivityPub.php
+++ b/app/Libraries/ActivityPub/Config/ActivityPub.php
@@ -30,4 +30,11 @@ class ActivityPub extends BaseConfig
 
     public $defaultCoverImagePath = 'assets/images/cover-default.jpg';
     public $defaultCoverImageMimetype = 'image/jpeg';
+
+    /**
+     * --------------------------------------------------------------------
+     * Cache options
+     * --------------------------------------------------------------------
+     */
+    public $cachePrefix = 'ap_';
 }
diff --git a/app/Libraries/ActivityPub/Entities/Note.php b/app/Libraries/ActivityPub/Entities/Note.php
index 9e1736d0c24b307a96b70feea744c74b47b1de3d..af278d4b913708ecacb35021258e757000491bd4 100644
--- a/app/Libraries/ActivityPub/Entities/Note.php
+++ b/app/Libraries/ActivityPub/Entities/Note.php
@@ -44,11 +44,21 @@ class Note extends UuidEntity
      */
     protected $preview_card;
 
+    /**
+     * @var boolean
+     */
+    protected $has_preview_card;
+
     /**
      * @var \ActivityPub\Entities\Note[]
      */
     protected $replies;
 
+    /**
+     * @var boolean
+     */
+    protected $has_replies;
+
     /**
      * @var \ActivityPub\Entities\Note[]
      */
@@ -106,6 +116,18 @@ class Note extends UuidEntity
         return $this->preview_card;
     }
 
+    public function getHasPreviewCard()
+    {
+        return !empty($this->getPreviewCard()) ? true : false;
+    }
+
+    public function getIsReply()
+    {
+        $this->is_reply = $this->in_reply_to_id !== null;
+
+        return $this->is_reply;
+    }
+
     public function getReplies()
     {
         if (empty($this->id)) {
@@ -121,11 +143,9 @@ class Note extends UuidEntity
         return $this->replies;
     }
 
-    public function getIsReply()
+    public function getHasReplies()
     {
-        $this->is_reply = $this->in_reply_to_id !== null;
-
-        return $this->is_reply;
+        return !empty($this->getReplies()) ? true : false;
     }
 
     public function getReplyToNote()
@@ -152,11 +172,7 @@ class Note extends UuidEntity
         }
 
         if (empty($this->reblogs)) {
-            $this->reblogs = model('NoteModel')->getNoteReblogs(
-                service('uuid')
-                    ->fromString($this->id)
-                    ->getBytes(),
-            );
+            $this->reblogs = model('NoteModel')->getNoteReblogs($this->id);
         }
 
         return $this->reblogs;
diff --git a/app/Libraries/ActivityPub/Models/ActivityModel.php b/app/Libraries/ActivityPub/Models/ActivityModel.php
index 33c7f45e41811b217d8b2ad57714e4840b1b7087..0d7a82ad5dde6a2d30bedb9a1dd830ec089a5f84 100644
--- a/app/Libraries/ActivityPub/Models/ActivityModel.php
+++ b/app/Libraries/ActivityPub/Models/ActivityModel.php
@@ -8,6 +8,8 @@
 
 namespace ActivityPub\Models;
 
+use Michalsn\Uuid\UuidModel;
+
 class ActivityModel extends UuidModel
 {
     protected $table = 'activitypub_activities';
@@ -34,7 +36,15 @@ class ActivityModel extends UuidModel
 
     public function getActivityById($activityId)
     {
-        return $this->find($activityId);
+        $cacheName =
+            config('ActivityPub')->cachePrefix . "activity#{$activityId}";
+        if (!($found = cache($cacheName))) {
+            $found = $this->find($activityId);
+
+            cache()->save($cacheName, $found, DECADE);
+        }
+
+        return $found;
     }
 
     /**
diff --git a/app/Libraries/ActivityPub/Models/ActorModel.php b/app/Libraries/ActivityPub/Models/ActorModel.php
index deed724ef544ee327c133d310ac2e11f01ae7ff3..a82ee94e739dbfdcfc9d75ae6f04c1176b7c3d26 100644
--- a/app/Libraries/ActivityPub/Models/ActorModel.php
+++ b/app/Libraries/ActivityPub/Models/ActorModel.php
@@ -8,6 +8,7 @@
 
 namespace ActivityPub\Models;
 
+use CodeIgniter\Events\Events;
 use CodeIgniter\Model;
 
 class ActorModel extends Model
@@ -42,7 +43,14 @@ class ActorModel extends Model
 
     public function getActorById($id)
     {
-        return $this->find($id);
+        $cacheName = config('ActivityPub')->cachePrefix . "actor#{$id}";
+        if (!($found = cache($cacheName))) {
+            $found = $this->find($id);
+
+            cache()->save($cacheName, $found, DECADE);
+        }
+
+        return $found;
     }
 
     /**
@@ -76,31 +84,51 @@ class ActorModel extends Model
 
     public function getActorByUri($actorUri)
     {
-        return $this->where('uri', $actorUri)->first();
+        $hashedActorUri = md5($actorUri);
+        $cacheName =
+            config('ActivityPub')->cachePrefix . "actor@{$hashedActorUri}";
+        if (!($found = cache($cacheName))) {
+            $found = $this->where('uri', $actorUri)->first();
+
+            cache()->save($cacheName, $found, DECADE);
+        }
+
+        return $found;
     }
 
     public function getFollowers($actorId)
     {
-        return $this->join(
-            'activitypub_follows',
-            'activitypub_follows.actor_id = id',
-            'inner',
-        )
-            ->where('activitypub_follows.target_actor_id', $actorId)
-            ->findAll();
+        $cacheName =
+            config('ActivityPub')->cachePrefix . "actor#{$actorId}_followers";
+        if (!($found = cache($cacheName))) {
+            $found = $this->join(
+                'activitypub_follows',
+                'activitypub_follows.actor_id = id',
+                'inner',
+            )
+                ->where('activitypub_follows.target_actor_id', $actorId)
+                ->findAll();
+
+            cache()->save($cacheName, $found, DECADE);
+        }
+
+        return $found;
     }
 
     /**
-     * Check if an actor is blocked using its uri
+     * Check if an existing actor is blocked using its uri.
+     * Returns FALSE if the actor doesn't exist
      *
      * @param mixed $actorUri
      * @return boolean
      */
     public function isActorBlocked($actorUri)
     {
-        return $this->where(['uri' => $actorUri, 'is_blocked' => true])->first()
-            ? true
-            : false;
+        if ($actor = $this->getActorByUri($actorUri)) {
+            return $actor->is_blocked;
+        }
+
+        return false;
     }
 
     /**
@@ -110,16 +138,35 @@ class ActorModel extends Model
      */
     public function getBlockedActors()
     {
-        return $this->where('is_blocked', 1)->findAll();
+        $cacheName = config('ActivityPub')->cachePrefix . 'blocked_actors';
+        if (!($found = cache($cacheName))) {
+            $found = $this->where('is_blocked', 1)->findAll();
+
+            cache()->save($cacheName, $found, DECADE);
+        }
+
+        return $found;
     }
 
     public function blockActor($actorId)
     {
+        $prefix = config('ActivityPub')->cachePrefix;
+        cache()->delete($prefix . 'blocked_actors');
+        cache()->deleteMatching($prefix . '*replies');
+
+        Events::trigger('on_block_actor', $actorId);
+
         $this->update($actorId, ['is_blocked' => 1]);
     }
 
     public function unblockActor($actorId)
     {
+        $prefix = config('ActivityPub')->cachePrefix;
+        cache()->delete($prefix . 'blocked_actors');
+        cache()->deleteMatching($prefix . '*replies');
+
+        Events::trigger('on_unblock_actor', $actorId);
+
         $this->update($actorId, ['is_blocked' => 0]);
     }
 }
diff --git a/app/Libraries/ActivityPub/Models/BlockedDomainModel.php b/app/Libraries/ActivityPub/Models/BlockedDomainModel.php
index 482ef77c49860ee6706f054e87005020dd382ebc..d87e1157499e76f0f184354ffd8b333b45b1982e 100644
--- a/app/Libraries/ActivityPub/Models/BlockedDomainModel.php
+++ b/app/Libraries/ActivityPub/Models/BlockedDomainModel.php
@@ -8,6 +8,7 @@
 
 namespace ActivityPub\Models;
 
+use CodeIgniter\Events\Events;
 use CodeIgniter\Model;
 
 class BlockedDomainModel extends Model
@@ -30,20 +31,41 @@ class BlockedDomainModel extends Model
      */
     public function getBlockedDomains()
     {
-        return $this->findAll();
+        $cacheName = config('ActivityPub')->cachePrefix . 'blocked_domains';
+        if (!($found = cache($cacheName))) {
+            $found = $this->findAll();
+
+            cache()->save($cacheName, $found, DECADE);
+        }
+        return $found;
     }
 
     public function isDomainBlocked($domain)
     {
-        if ($this->find($domain)) {
-            return true;
+        $hashedDomain = md5($domain);
+        $cacheName =
+            config('ActivityPub')->cachePrefix .
+            "domain#{$hashedDomain}_isBlocked";
+        if (!($found = cache($cacheName))) {
+            $found = $this->find($domain) ? true : false;
+
+            cache()->save($cacheName, $found, DECADE);
         }
 
-        return false;
+        return $found;
     }
 
     public function blockDomain($name)
     {
+        $hashedDomain = md5($name);
+        $prefix = config('ActivityPub')->cachePrefix;
+        cache()->delete($prefix . "domain#{$hashedDomain}_isBlocked");
+        cache()->delete($prefix . 'blocked_domains');
+
+        cache()->deleteMatching($prefix . '*replies');
+
+        Events::trigger('on_block_domain', $name);
+
         $this->db->transStart();
 
         // set all actors from the domain as blocked
@@ -63,6 +85,15 @@ class BlockedDomainModel extends Model
 
     public function unblockDomain($name)
     {
+        $hashedDomain = md5($name);
+        $prefix = config('ActivityPub')->cachePrefix;
+        cache()->delete($prefix . "domain#{$hashedDomain}_isBlocked");
+        cache()->delete($prefix . 'blocked_domains');
+
+        cache()->deleteMatching($prefix . '*replies');
+
+        Events::trigger('on_unblock_domain', $name);
+
         $this->db->transStart();
         // unblock all actors from the domain
         model('ActorModel')
diff --git a/app/Libraries/ActivityPub/Models/FavouriteModel.php b/app/Libraries/ActivityPub/Models/FavouriteModel.php
index 8a5d781e48912b0ca3aba4714e101aa776cfe4ae..4aacdc9a2dbef7cb33cdc2b8ea749110b2262119 100644
--- a/app/Libraries/ActivityPub/Models/FavouriteModel.php
+++ b/app/Libraries/ActivityPub/Models/FavouriteModel.php
@@ -11,6 +11,7 @@ namespace ActivityPub\Models;
 use ActivityPub\Activities\LikeActivity;
 use ActivityPub\Activities\UndoActivity;
 use CodeIgniter\Events\Events;
+use Michalsn\Uuid\UuidModel;
 
 class FavouriteModel extends UuidModel
 {
@@ -49,6 +50,19 @@ class FavouriteModel extends UuidModel
             )
             ->increment('favourites_count');
 
+        $prefix = config('ActivityPub')->cachePrefix;
+        $hashedNoteUri = md5($note->uri);
+        cache()->delete($prefix . "note#{$note->id}");
+        cache()->delete($prefix . "note@{$hashedNoteUri}");
+        cache()->delete($prefix . "actor#{$actor->id}_published_notes");
+
+        if ($note->in_reply_to_id) {
+            cache()->delete($prefix . "note#{$note->in_reply_to_id}_replies");
+            cache()->delete(
+                $prefix . "note#{$note->in_reply_to_id}_replies_withBlocked",
+            );
+        }
+
         Events::trigger('on_note_favourite', $actor, $note);
 
         if ($registerActivity) {
@@ -91,6 +105,19 @@ class FavouriteModel extends UuidModel
             )
             ->decrement('favourites_count');
 
+        $prefix = config('ActivityPub')->cachePrefix;
+        $hashedNoteUri = md5($note->uri);
+        cache()->delete($prefix . "note#{$note->id}");
+        cache()->delete($prefix . "note@{$hashedNoteUri}");
+        cache()->delete($prefix . "actor#{$actor->id}_published_notes");
+
+        if ($note->in_reply_to_id) {
+            cache()->delete($prefix . "note#{$note->in_reply_to_id}_replies");
+            cache()->delete(
+                $prefix . "note#{$note->in_reply_to_id}_replies_withBlocked",
+            );
+        }
+
         $this->table('activitypub_favourites')
             ->where([
                 'actor_id' => $actor->id,
diff --git a/app/Libraries/ActivityPub/Models/FollowModel.php b/app/Libraries/ActivityPub/Models/FollowModel.php
index 89831855d4730a85e8ed8e413d8aef4743d2ff37..ced37815edbb220919af544bdd149585eacab506 100644
--- a/app/Libraries/ActivityPub/Models/FollowModel.php
+++ b/app/Libraries/ActivityPub/Models/FollowModel.php
@@ -27,9 +27,8 @@ class FollowModel extends Model
     protected $updatedField = null;
 
     /**
-     *
-     * @param \ActivityPub\Entities\Actor $actor
-     * @param \ActivityPub\Entities\Actor $targetActor
+     * @param \ActivityPub\Entities\Actor $actor Actor that is following
+     * @param \ActivityPub\Entities\Actor $targetActor Actor that is being followed
      * @param bool $registerActivity
      * @return void
      * @throws DatabaseException
@@ -49,6 +48,14 @@ class FollowModel extends Model
                 ->where('id', $targetActor->id)
                 ->increment('followers_count');
 
+            cache()->delete(
+                config('ActivityPub')->cachePrefix . "actor#{$targetActor->id}",
+            );
+            cache()->delete(
+                config('ActivityPub')->cachePrefix .
+                    "actor#{$targetActor->id}_followers",
+            );
+
             if ($registerActivity) {
                 $followActivity = new FollowActivity();
 
@@ -85,8 +92,8 @@ class FollowModel extends Model
     }
 
     /**
-     * @param \ActivityPub\Entities\Actor $actor
-     * @param \ActivityPub\Entities\Actor $targetActor
+     * @param \ActivityPub\Entities\Actor $actor Actor that is unfollowing
+     * @param \ActivityPub\Entities\Actor $targetActor Actor that is being unfollowed
      * @return void
      * @throws InvalidArgumentException
      * @throws DatabaseException
@@ -108,6 +115,14 @@ class FollowModel extends Model
             ->where('id', $targetActor->id)
             ->decrement('followers_count');
 
+        cache()->delete(
+            config('ActivityPub')->cachePrefix . "actor#{$targetActor->id}",
+        );
+        cache()->delete(
+            config('ActivityPub')->cachePrefix .
+                "actor#{$targetActor->id}_followers",
+        );
+
         if ($registerActivity) {
             $undoActivity = new UndoActivity();
             // get follow activity from database
diff --git a/app/Libraries/ActivityPub/Models/NoteModel.php b/app/Libraries/ActivityPub/Models/NoteModel.php
index f0914542d6f06df81104206c21d7277dcbccfbd2..ed1600f3d1cdd99d90a5921b69244e75eed3920d 100644
--- a/app/Libraries/ActivityPub/Models/NoteModel.php
+++ b/app/Libraries/ActivityPub/Models/NoteModel.php
@@ -17,6 +17,7 @@ use ActivityPub\Objects\TombstoneObject;
 use CodeIgniter\Events\Events;
 use CodeIgniter\HTTP\URI;
 use CodeIgniter\I18n\Time;
+use Michalsn\Uuid\UuidModel;
 
 class NoteModel extends UuidModel
 {
@@ -54,12 +55,28 @@ class NoteModel extends UuidModel
 
     public function getNoteById($noteId)
     {
-        return $this->find($noteId);
+        $cacheName = config('ActivityPub')->cachePrefix . "note#{$noteId}";
+        if (!($found = cache($cacheName))) {
+            $found = $this->find($noteId);
+
+            cache()->save($cacheName, $found, DECADE);
+        }
+
+        return $found;
     }
 
     public function getNoteByUri($noteUri)
     {
-        return $this->where('uri', $noteUri)->first();
+        $hashedNoteUri = md5($noteUri);
+        $cacheName =
+            config('ActivityPub')->cachePrefix . "note@{$hashedNoteUri}";
+        if (!($found = cache($cacheName))) {
+            $found = $this->where('uri', $noteUri)->first();
+
+            cache()->save($cacheName, $found, DECADE);
+        }
+
+        return $found;
     }
 
     /**
@@ -67,15 +84,24 @@ class NoteModel extends UuidModel
      *
      * @return \ActivityPub\Entities\Note[]
      */
-    public function getActorNotes($actorId)
+    public function getActorPublishedNotes($actorId)
     {
-        return $this->where([
-            'actor_id' => $actorId,
-            'in_reply_to_id' => null,
-        ])
-            ->where('`published_at` <= NOW()', null, false)
-            ->orderBy('published_at', 'DESC')
-            ->findAll();
+        $cacheName =
+            config('ActivityPub')->cachePrefix .
+            "actor#{$actorId}_published_notes";
+        if (!($found = cache($cacheName))) {
+            $found = $this->where([
+                'actor_id' => $actorId,
+                'in_reply_to_id' => null,
+            ])
+                ->where('`published_at` <= NOW()', null, false)
+                ->orderBy('published_at', 'DESC')
+                ->findAll();
+
+            cache()->save($cacheName, $found, DECADE);
+        }
+
+        return $found;
     }
 
     /**
@@ -88,26 +114,34 @@ class NoteModel extends UuidModel
      */
     public function getNoteReplies($noteId, $withBlocked = false)
     {
-        if (!$withBlocked) {
-            $this->select('activitypub_notes.*')
-                ->join(
-                    'activitypub_actors',
-                    'activitypub_actors.id = activitypub_notes.actor_id',
-                    'inner',
-                )
-                ->where('activitypub_actors.is_blocked', 0);
-        }
+        $cacheName =
+            config('ActivityPub')->cachePrefix .
+            "note#{$noteId}_replies" .
+            ($withBlocked ? '_withBlocked' : '');
+
+        if (!($found = cache($cacheName))) {
+            if (!$withBlocked) {
+                $this->select('activitypub_notes.*')
+                    ->join(
+                        'activitypub_actors',
+                        'activitypub_actors.id = activitypub_notes.actor_id',
+                        'inner',
+                    )
+                    ->where('activitypub_actors.is_blocked', 0);
+            }
 
-        $this->where(
-            'in_reply_to_id',
-            service('uuid')
-                ->fromString($noteId)
-                ->getBytes(),
-        )
-            ->where('`published_at` <= NOW()', null, false)
-            ->orderBy('published_at', 'ASC');
+            $this->where(
+                'in_reply_to_id',
+                $this->uuid->fromString($noteId)->getBytes(),
+            )
+                ->where('`published_at` <= NOW()', null, false)
+                ->orderBy('published_at', 'ASC');
+            $found = $this->findAll();
 
-        return $this->findAll();
+            cache()->save($cacheName, $found, DECADE);
+        }
+
+        return $found;
     }
 
     /**
@@ -115,16 +149,28 @@ class NoteModel extends UuidModel
      */
     public function getNoteReblogs($noteId)
     {
-        return $this->where('reblog_of_id', $noteId)
-            ->where('`published_at` <= NOW()', null, false)
-            ->orderBy('published_at', 'ASC')
-            ->findAll();
+        $cacheName =
+            config('ActivityPub')->cachePrefix . "note#{$noteId}_reblogs";
+
+        if (!($found = cache($cacheName))) {
+            $found = $this->where(
+                'reblog_of_id',
+                $this->uuid->fromString($noteId)->getBytes(),
+            )
+                ->where('`published_at` <= NOW()', null, false)
+                ->orderBy('published_at', 'ASC')
+                ->findAll();
+
+            cache()->save($cacheName, $found, DECADE);
+        }
+
+        return $found;
     }
 
     public function addPreviewCard($noteId, $previewCardId)
     {
         return $this->db->table('activitypub_notes_preview_cards')->insert([
-            'note_id' => $noteId,
+            'note_id' => $this->uuid->fromString($noteId)->getBytes(),
             'preview_card_id' => $previewCardId,
         ]);
     }
@@ -169,10 +215,6 @@ class NoteModel extends UuidModel
                     // problem when linking note to preview card
                     return false;
                 }
-
-                $this->db->transComplete();
-
-                return $newNoteId;
             }
         }
 
@@ -180,17 +222,19 @@ class NoteModel extends UuidModel
             ->where('id', $note->actor_id)
             ->increment('notes_count');
 
+        $cachePrefix = config('ActivityPub')->cachePrefix;
+        cache()->delete($cachePrefix . "actor#{$note->actor_id}");
+        cache()->delete(
+            $cachePrefix . "actor#{$note->actor_id}_published_notes",
+        );
+
         Events::trigger('on_note_add', $note);
 
         if ($registerActivity) {
-            $noteUuid = service('uuid')
-                ->fromBytes($newNoteId)
-                ->toString();
-
             // set note id and uri to construct NoteObject
-            $note->id = $noteUuid;
+            $note->id = $newNoteId;
             $note->uri = base_url(
-                route_to('note', $note->actor->username, $noteUuid),
+                route_to('note', $note->actor->username, $newNoteId),
             );
 
             $createActivity = new CreateActivity();
@@ -203,7 +247,7 @@ class NoteModel extends UuidModel
                 'Create',
                 $note->actor_id,
                 null,
-                $noteUuid,
+                $newNoteId,
                 $createActivity->toJSON(),
                 $note->published_at,
                 'queued',
@@ -234,7 +278,7 @@ class NoteModel extends UuidModel
         $scheduledActivity = model('ActivityModel')
             ->where([
                 'type' => 'Create',
-                'note_id' => service('uuid')
+                'note_id' => $this->uuid
                     ->fromString($updatedNote->id)
                     ->getBytes(),
             ])
@@ -253,6 +297,12 @@ class NoteModel extends UuidModel
         // update note
         $updateResult = $this->update($updatedNote->id, $updatedNote);
 
+        // Clear note cache
+        $prefix = config('ActivityPub')->cachePrefix;
+        $hashedNoteUri = md5($updatedNote->uri);
+        cache()->delete($prefix . "note#{$updatedNote->id}");
+        cache()->delete($prefix . "note@{$hashedNoteUri}");
+
         $this->db->transComplete();
 
         return $updateResult;
@@ -268,32 +318,61 @@ class NoteModel extends UuidModel
     {
         $this->db->transStart();
 
+        $cachePrefix = config('ActivityPub')->cachePrefix;
+
         model('ActorModel')
             ->where('id', $note->actor_id)
             ->decrement('notes_count');
+        cache()->delete($cachePrefix . "actor#{$note->actor_id}");
+        cache()->delete(
+            $cachePrefix . "actor#{$note->actor_id}_published_notes",
+        );
 
         if ($note->in_reply_to_id) {
             // Note to remove is a reply
             model('NoteModel')
                 ->where(
                     'id',
-                    service('uuid')
-                        ->fromString($note->in_reply_to_id)
-                        ->getBytes(),
+                    $this->uuid->fromString($note->in_reply_to_id)->getBytes(),
                 )
                 ->decrement('replies_count');
+
+            $replyToNote = $note->reply_to_note;
+            cache()->delete($cachePrefix . "note#{$replyToNote->id}");
+            cache()->delete($cachePrefix . "note@{$replyToNote->uri}");
+            cache()->delete($cachePrefix . "note#{$replyToNote->id}_replies");
+            cache()->delete(
+                $cachePrefix . "note#{$replyToNote->id}_replies_withBlocked",
+            );
+
+            Events::trigger('on_reply_remove', $note);
         }
 
-        // remove all reblogs
+        // remove all note reblogs
         foreach ($note->reblogs as $reblog) {
             $this->removeNote($reblog);
         }
 
-        // remove all replies
+        // remove all note replies
         foreach ($note->replies as $reply) {
             $this->removeNote($reply);
         }
 
+        if ($note->preview_card) {
+            // check that preview card in no longer used elsewhere before deleting it
+            if (
+                $this->db
+                    ->table('activitypub_notes_preview_cards')
+                    ->where('preview_card_id', $note->preview_card->id)
+                    ->countAll() <= 1
+            ) {
+                model('PreviewCardModel')->deletePreviewCard(
+                    $note->preview_card->id,
+                    $note->preview_card->url,
+                );
+            }
+        }
+
         Events::trigger('on_note_remove', $note);
 
         if ($registerActivity) {
@@ -326,6 +405,15 @@ class NoteModel extends UuidModel
             ]);
         }
 
+        // clear note + replies / reblogs + actor  and its published notes
+        $hashedNoteUri = md5($note->uri);
+        cache()->delete($cachePrefix . "note#{$note->id}");
+        cache()->delete($cachePrefix . "note@{$hashedNoteUri}");
+        cache()->delete($cachePrefix . "note#{$note->id}_replies");
+        cache()->delete($cachePrefix . "note#{$note->id}_replies_withBlocked");
+        cache()->delete($cachePrefix . "note#{$note->id}_reblogs");
+        cache()->delete($cachePrefix . "note#{$note->id}_preview_card");
+
         $result = model('NoteModel', false)->delete($note->id);
 
         $this->db->transComplete();
@@ -349,12 +437,19 @@ class NoteModel extends UuidModel
         model('NoteModel')
             ->where(
                 'id',
-                service('uuid')
-                    ->fromString($reply->in_reply_to_id)
-                    ->getBytes(),
+                $this->uuid->fromString($reply->in_reply_to_id)->getBytes(),
             )
             ->increment('replies_count');
 
+        $prefix = config('ActivityPub')->cachePrefix;
+        $hashedNoteUri = md5($reply->reply_to_note->uri);
+        cache()->delete($prefix . "note#{$reply->in_reply_to_id}");
+        cache()->delete($prefix . "note@{$hashedNoteUri}");
+        cache()->delete($prefix . "note#{$reply->in_reply_to_id}_replies");
+        cache()->delete(
+            $prefix . "note#{$reply->in_reply_to_id}_replies_withBlocked",
+        );
+
         Events::trigger('on_note_reply', $reply);
 
         $this->db->transComplete();
@@ -385,15 +480,19 @@ class NoteModel extends UuidModel
             ->where('id', $actor->id)
             ->increment('notes_count');
 
+        $prefix = config('ActivityPub')->cachePrefix;
+        cache()->delete($prefix . "actor#{$note->actor_id}");
+        cache()->delete($prefix . "actor#{$note->actor_id}_published_notes");
+
         model('NoteModel')
-            ->where(
-                'id',
-                service('uuid')
-                    ->fromString($note->id)
-                    ->getBytes(),
-            )
+            ->where('id', $this->uuid->fromString($note->id)->getBytes())
             ->increment('reblogs_count');
 
+        $hashedNoteUri = md5($note->uri);
+        cache()->delete($prefix . "note#{$note->id}");
+        cache()->delete($prefix . "note@{$hashedNoteUri}");
+        cache()->delete($prefix . "note#{$note->id}_reblogs");
+
         Events::trigger('on_note_reblog', $actor, $note);
 
         if ($registerActivity) {
@@ -438,15 +537,26 @@ class NoteModel extends UuidModel
             ->where('id', $reblogNote->actor_id)
             ->decrement('notes_count');
 
+        $cachePrefix = config('ActivityPub')->cachePrefix;
+        cache()->delete($cachePrefix . "actor#{$reblogNote->actor_id}");
+        cache()->delete(
+            $cachePrefix . "actor#{$reblogNote->actor_id}_published_notes",
+        );
+
         model('NoteModel')
             ->where(
                 'id',
-                service('uuid')
-                    ->fromString($reblogNote->reblog_of_id)
-                    ->getBytes(),
+                $this->uuid->fromString($reblogNote->reblog_of_id)->getBytes(),
             )
             ->decrement('reblogs_count');
 
+        $hashedReblogNoteUri = md5($reblogNote->uri);
+        $hashedNoteUri = md5($reblogNote->reblog_of_note->uri);
+        cache()->delete($cachePrefix . "note#{$reblogNote->id}");
+        cache()->delete($cachePrefix . "note@{$hashedReblogNoteUri}");
+        cache()->delete($cachePrefix . "note#{$reblogNote->reblog_of_id}");
+        cache()->delete($cachePrefix . "note@{$hashedNoteUri}");
+
         Events::trigger('on_note_undo_reblog', $reblogNote);
 
         if ($registerActivity) {
@@ -456,7 +566,7 @@ class NoteModel extends UuidModel
                 ->where([
                     'type' => 'Announce',
                     'actor_id' => $reblogNote->actor_id,
-                    'note_id' => service('uuid')
+                    'note_id' => $this->uuid
                         ->fromString($reblogNote->reblog_of_id)
                         ->getBytes(),
                 ])
@@ -516,7 +626,7 @@ class NoteModel extends UuidModel
         if (
             !($reblogNote = $this->where([
                 'actor_id' => $actor->id,
-                'reblog_of_id' => service('uuid')
+                'reblog_of_id' => $this->uuid
                     ->fromString($note->id)
                     ->getBytes(),
             ])->first())
@@ -529,9 +639,8 @@ class NoteModel extends UuidModel
 
     protected function setNoteId($data)
     {
-        $uuid4 = service('uuid')->uuid4();
-        $data['id'] = $uuid4->toString();
-        $data['data']['id'] = $uuid4->getBytes();
+        $uuid4 = $this->uuid->{$this->uuidVersion}();
+        $data['data']['id'] = $uuid4->toString();
 
         if (!isset($data['data']['uri'])) {
             $actor = model('ActorModel')->getActorById(
diff --git a/app/Libraries/ActivityPub/Models/PreviewCardModel.php b/app/Libraries/ActivityPub/Models/PreviewCardModel.php
index 874fe1151841c29864b2bf5fcac7a9539ee75727..f7dc02aea6e8a54976048a94fd3add2d0b0acd22 100644
--- a/app/Libraries/ActivityPub/Models/PreviewCardModel.php
+++ b/app/Libraries/ActivityPub/Models/PreviewCardModel.php
@@ -35,22 +35,50 @@ class PreviewCardModel extends Model
 
     public function getPreviewCardFromUrl($url)
     {
-        return $this->where('url', $url)->first();
+        $hashedPreviewCardUrl = md5($url);
+        $cacheName =
+            config('ActivityPub')->cachePrefix .
+            "preview_card@{$hashedPreviewCardUrl}";
+        if (!($found = cache($cacheName))) {
+            $found = $this->where('url', $url)->first();
+            cache()->save($cacheName, $found, DECADE);
+        }
+
+        return $found;
     }
 
     public function getNotePreviewCard($noteId)
     {
-        return $this->join(
-            'activitypub_notes_preview_cards',
-            'activitypub_notes_preview_cards.preview_card_id = id',
-            'inner',
-        )
-            ->where(
-                'note_id',
-                service('uuid')
-                    ->fromString($noteId)
-                    ->getBytes(),
+        $cacheName =
+            config('ActivityPub')->cachePrefix . "note#{$noteId}_preview_card";
+        if (!($found = cache($cacheName))) {
+            $found = $this->join(
+                'activitypub_notes_preview_cards',
+                'activitypub_notes_preview_cards.preview_card_id = id',
+                'inner',
             )
-            ->first();
+                ->where(
+                    'note_id',
+                    service('uuid')
+                        ->fromString($noteId)
+                        ->getBytes(),
+                )
+                ->first();
+
+            cache()->save($cacheName, $found, DECADE);
+        }
+
+        return $found;
+    }
+
+    public function deletePreviewCard($id, $url)
+    {
+        $hashedPreviewCardUrl = md5($url);
+        cache()->delete(
+            config('ActivityPub')->cachePrefix .
+                "preview_card@{$hashedPreviewCardUrl}",
+        );
+
+        return $this->delete($id);
     }
 }
diff --git a/app/Libraries/ActivityPub/Models/UuidModel.php b/app/Libraries/ActivityPub/Models/UuidModel.php
deleted file mode 100644
index 2029a846f76ca68a33127819e55616c2b90a51f8..0000000000000000000000000000000000000000
--- a/app/Libraries/ActivityPub/Models/UuidModel.php
+++ /dev/null
@@ -1,206 +0,0 @@
-<?php
-
-/**
- * @copyright  2021 Podlibre
- * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
- * @link       https://castopod.org/
- */
-
-namespace ActivityPub\Models;
-
-use CodeIgniter\Database\Exceptions\DataException;
-use stdClass;
-
-class UuidModel extends \Michalsn\Uuid\UuidModel
-{
-    /**
-     * This insert overwrite is added as a means to FIX some bugs
-     * from the extended Uuid package. See: https://github.com/michalsn/codeigniter4-uuid/issues/2
-     *
-     * Inserts data into the current table. If an object is provided,
-     * it will attempt to convert it to an array.
-     *
-     * @param array|object $data
-     * @param boolean      $returnID Whether insert ID should be returned or not.
-     *
-     * @return BaseResult|integer|string|false
-     * @throws \ReflectionException
-     */
-    public function insert($data = null, bool $returnID = true)
-    {
-        $escape = null;
-
-        $this->insertID = 0;
-
-        if (empty($data)) {
-            $data = $this->tempData['data'] ?? null;
-            $escape = $this->tempData['escape'] ?? null;
-            $this->tempData = [];
-        }
-
-        if (empty($data)) {
-            throw DataException::forEmptyDataset('insert');
-        }
-
-        // If $data is using a custom class with public or protected
-        // properties representing the table elements, we need to grab
-        // them as an array.
-        if (is_object($data) && !$data instanceof stdClass) {
-            $data = static::classToArray(
-                $data,
-                $this->primaryKey,
-                $this->dateFormat,
-                false,
-            );
-        }
-
-        // If it's still a stdClass, go ahead and convert to
-        // an array so doProtectFields and other model methods
-        // don't have to do special checks.
-        if (is_object($data)) {
-            $data = (array) $data;
-        }
-
-        if (empty($data)) {
-            throw DataException::forEmptyDataset('insert');
-        }
-
-        // Validate data before saving.
-        if ($this->skipValidation === false) {
-            if ($this->cleanRules()->validate($data) === false) {
-                return false;
-            }
-        }
-
-        // Must be called first so we don't
-        // strip out created_at values.
-        $data = $this->doProtectFields($data);
-
-        // Set created_at and updated_at with same time
-        $date = $this->setDate();
-
-        if (
-            $this->useTimestamps &&
-            !empty($this->createdField) &&
-            !array_key_exists($this->createdField, $data)
-        ) {
-            $data[$this->createdField] = $date;
-        }
-
-        if (
-            $this->useTimestamps &&
-            !empty($this->updatedField) &&
-            !array_key_exists($this->updatedField, $data)
-        ) {
-            $data[$this->updatedField] = $date;
-        }
-
-        $eventData = ['data' => $data];
-        if ($this->tempAllowCallbacks) {
-            $eventData = $this->trigger('beforeInsert', $eventData);
-        }
-
-        // Require non empty primaryKey when
-        // not using auto-increment feature
-        if (
-            !$this->useAutoIncrement &&
-            empty($eventData['data'][$this->primaryKey])
-        ) {
-            throw DataException::forEmptyPrimaryKey('insert');
-        }
-
-        if (!empty($this->uuidFields)) {
-            foreach ($this->uuidFields as $field) {
-                if ($field === $this->primaryKey) {
-                    $this->uuidTempData[
-                        $field
-                    ] = $this->uuid->{$this->uuidVersion}();
-
-                    if ($this->uuidUseBytes === true) {
-                        $this->builder()->set(
-                            $field,
-                            $this->uuidTempData[$field]->getBytes(),
-                        );
-                    } else {
-                        $this->builder()->set(
-                            $field,
-                            $this->uuidTempData[$field]->toString(),
-                        );
-                    }
-                } else {
-                    if (
-                        $this->uuidUseBytes === true &&
-                        !empty($eventData['data'][$field])
-                    ) {
-                        $this->uuidTempData[$field] = $this->uuid->fromString(
-                            $eventData['data'][$field],
-                        );
-                        $this->builder()->set(
-                            $field,
-                            $this->uuidTempData[$field]->getBytes(),
-                        );
-                        unset($eventData['data'][$field]);
-                    }
-                }
-            }
-        }
-
-        // Must use the set() method to ensure objects get converted to arrays
-        $result = $this->builder()
-            ->set($eventData['data'], '', $escape)
-            ->insert();
-
-        // If insertion succeeded then save the insert ID
-        if ($result) {
-            if (
-                !$this->useAutoIncrement ||
-                isset($eventData['data'][$this->primaryKey])
-            ) {
-                $this->insertID = $eventData['data'][$this->primaryKey];
-            } else {
-                if (in_array($this->primaryKey, $this->uuidFields)) {
-                    $this->insertID = $this->uuidTempData[
-                        $this->primaryKey
-                    ]->toString();
-                } else {
-                    $this->insertID = $this->db->insertID();
-                }
-            }
-        }
-
-        // Cleanup data before event trigger
-        if (!empty($this->uuidFields) && $this->uuidUseBytes === true) {
-            foreach ($this->uuidFields as $field) {
-                if (
-                    $field === $this->primaryKey ||
-                    empty($this->uuidTempData[$field])
-                ) {
-                    continue;
-                }
-
-                $eventData['data'][$field] = $this->uuidTempData[
-                    $field
-                ]->toString();
-            }
-        }
-
-        $eventData = [
-            'id' => $this->insertID,
-            'data' => $eventData['data'],
-            'result' => $result,
-        ];
-        if ($this->tempAllowCallbacks) {
-            // Trigger afterInsert events with the inserted data and new ID
-            $this->trigger('afterInsert', $eventData);
-        }
-        $this->tempAllowCallbacks = $this->allowCallbacks;
-
-        // If insertion failed, get out of here
-        if (!$result) {
-            return $result;
-        }
-
-        // otherwise return the insertID, if requested.
-        return $returnID ? $this->insertID : $result;
-    }
-}
diff --git a/app/Libraries/Analytics/AnalyticsTrait.php b/app/Libraries/Analytics/AnalyticsTrait.php
new file mode 100644
index 0000000000000000000000000000000000000000..2b0a69e70ccb5f271882db66903af816c539700f
--- /dev/null
+++ b/app/Libraries/Analytics/AnalyticsTrait.php
@@ -0,0 +1,51 @@
+<?php
+
+/**
+ * @copyright  2021 Podlibre
+ * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
+ * @link       https://castopod.org/
+ */
+
+namespace Analytics;
+
+trait AnalyticsTrait
+{
+    /**
+     *
+     * @param integer $podcastId
+     * @return void
+     */
+    protected function registerPodcastWebpageHit($podcastId)
+    {
+        helper('analytics');
+
+        set_user_session_deny_list_ip();
+        set_user_session_browser();
+        set_user_session_referer();
+        set_user_session_entry_page();
+
+        $session = \Config\Services::session();
+        $session->start();
+
+        if (!$session->get('denyListIp')) {
+            $db = \Config\Database::connect();
+
+            $referer = $session->get('referer');
+            $domain = empty(parse_url($referer, PHP_URL_HOST))
+                ? '- Direct -'
+                : parse_url($referer, PHP_URL_HOST);
+            parse_str(parse_url($referer, PHP_URL_QUERY), $queries);
+            $keywords = empty($queries['q']) ? null : $queries['q'];
+
+            $procedureName = $db->prefixTable('analytics_website');
+            $db->query("call $procedureName(?,?,?,?,?,?)", [
+                $podcastId,
+                $session->get('browser'),
+                $session->get('entryPage'),
+                $referer,
+                $domain,
+                $keywords,
+            ]);
+        }
+    }
+}
diff --git a/app/Libraries/Analytics/Helpers/analytics_helper.php b/app/Libraries/Analytics/Helpers/analytics_helper.php
index 227a7ef6bffcb3084d59a1a82a77ed638e019fca..a9fc7c5791a43f9e2bd204823aa783cc47d34435 100644
--- a/app/Libraries/Analytics/Helpers/analytics_helper.php
+++ b/app/Libraries/Analytics/Helpers/analytics_helper.php
@@ -258,40 +258,6 @@ if (!function_exists('set_user_session_entry_page')) {
     }
 }
 
-if (!function_exists('webpage_hit')) {
-    /**
-     *
-     * @param integer $podcastId
-     * @return void
-     */
-    function webpage_hit($podcastId)
-    {
-        $session = \Config\Services::session();
-        $session->start();
-
-        if (!$session->get('denyListIp')) {
-            $db = \Config\Database::connect();
-
-            $referer = $session->get('referer');
-            $domain = empty(parse_url($referer, PHP_URL_HOST))
-                ? '- Direct -'
-                : parse_url($referer, PHP_URL_HOST);
-            parse_str(parse_url($referer, PHP_URL_QUERY), $queries);
-            $keywords = empty($queries['q']) ? null : $queries['q'];
-
-            $procedureName = $db->prefixTable('analytics_website');
-            $db->query("call $procedureName(?,?,?,?,?,?)", [
-                $podcastId,
-                $session->get('browser'),
-                $session->get('entryPage'),
-                $referer,
-                $domain,
-                $keywords,
-            ]);
-        }
-    }
-}
-
 if (!function_exists('podcast_hit')) {
     /**
      * Counting podcast episode downloads for analytic purposes
diff --git a/app/Models/ActorModel.php b/app/Models/ActorModel.php
new file mode 100644
index 0000000000000000000000000000000000000000..694c7db6d09834ea08e6f63928e93cce2a7a80c6
--- /dev/null
+++ b/app/Models/ActorModel.php
@@ -0,0 +1,14 @@
+<?php
+
+/**
+ * @copyright  2021 Podlibre
+ * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
+ * @link       https://castopod.org/
+ */
+
+namespace App\Models;
+
+class ActorModel extends \ActivityPub\Models\ActorModel
+{
+    protected $returnType = \App\Entities\Actor::class;
+}
diff --git a/app/Models/CategoryModel.php b/app/Models/CategoryModel.php
index 6fc150c4707e85721eee14ec471eea4f39b41371..1116116b238640ab0c1dd164cafc029a018524e6 100644
--- a/app/Models/CategoryModel.php
+++ b/app/Models/CategoryModel.php
@@ -27,9 +27,9 @@ class CategoryModel extends Model
 
     protected $useTimestamps = false;
 
-    public function findParent($parentId)
+    public function getCategoryById($id)
     {
-        return $this->find($parentId);
+        return $this->find($id);
     }
 
     public function getCategoryOptions()
diff --git a/app/Models/EpisodeModel.php b/app/Models/EpisodeModel.php
index 9360348286373be1e7b18d2fc4672df2330f3db7..8d36325bb0259b2416a401abd2fe9fa2a6e11a75 100644
--- a/app/Models/EpisodeModel.php
+++ b/app/Models/EpisodeModel.php
@@ -139,6 +139,7 @@ class EpisodeModel extends Model
 
     public function getEpisodeById($episodeId)
     {
+        // TODO: episode id should be a composite key. The cache should include podcast_id.
         $cacheName = "podcast_episode#{$episodeId}";
         if (!($found = cache($cacheName))) {
             $builder = $this->where([
@@ -153,19 +154,16 @@ class EpisodeModel extends Model
         return $found;
     }
 
-    public function getPublishedEpisodeById($episodeId, $podcastId = null)
+    public function getPublishedEpisodeById($podcastId, $episodeId)
     {
-        $cacheName = "podcast_episode#{$episodeId}_published";
+        $cacheName = "podcast#{$podcastId}_episode#{$episodeId}_published";
         if (!($found = cache($cacheName))) {
-            $builder = $this->where([
+            $found = $this->where([
                 'id' => $episodeId,
-            ])->where('`published_at` <= NOW()', null, false);
-
-            if ($podcastId) {
-                $builder->where('podcast_id', $podcastId);
-            }
-
-            $found = $builder->first();
+            ])
+                ->where('podcast_id', $podcastId)
+                ->where('`published_at` <= NOW()', null, false)
+                ->first();
 
             cache()->save($cacheName, $found, DECADE);
         }
@@ -287,11 +285,17 @@ class EpisodeModel extends Model
         // delete model requests cache
         cache()->delete("podcast#{$episode->podcast_id}_episodes");
 
-        cache()->deleteMatching("podcast_episode#{$episode->id}*");
+        cache()->delete("podcast_episode#{$episode->id}");
+        cache()->deleteMatching(
+            "podcast#{$episode->podcast_id}_episode#{$episode->id}*",
+        );
         cache()->delete(
             "podcast#{$episode->podcast_id}_episode@{$episode->slug}",
         );
 
+        cache()->deleteMatching(
+            "page_podcast#{$episode->podcast_id}_activity*",
+        );
         cache()->deleteMatching(
             "page_podcast#{$episode->podcast_id}_episode#{$episode->id}_*",
         );
@@ -300,12 +304,12 @@ class EpisodeModel extends Model
         if ($episode->season_number) {
             cache()->deleteMatching("podcast#{$episode->podcast_id}_season*");
             cache()->deleteMatching(
-                "page_podcast#{$episode->podcast_id}_season*",
+                "page_podcast#{$episode->podcast_id}_episodes_season*",
             );
         } else {
             cache()->deleteMatching("podcast#{$episode->podcast_id}_year*");
             cache()->deleteMatching(
-                "page_podcast#{$episode->podcast_id}_year*",
+                "page_podcast#{$episode->podcast_id}_episodes_year*",
             );
         }
 
diff --git a/app/Models/EpisodePersonModel.php b/app/Models/EpisodePersonModel.php
index 8df81e10e5f90ca01dc69328f66df11572404e6c..5e96171b6ef2a0556a9625a291f82b5f79ced4ad 100644
--- a/app/Models/EpisodePersonModel.php
+++ b/app/Models/EpisodePersonModel.php
@@ -38,9 +38,9 @@ class EpisodePersonModel extends Model
     protected $afterInsert = ['clearCache'];
     protected $beforeDelete = ['clearCache'];
 
-    public function getEpisodePersons($episodeId)
+    public function getEpisodePersons($podcastId, $episodeId)
     {
-        $cacheName = "podcast_episode#{$episodeId}_persons";
+        $cacheName = "podcast#{$podcastId}_episode#{$episodeId}_persons";
         if (!($found = cache($cacheName))) {
             $found = $this->select('episodes_persons.*')
                 ->where('episode_id', $episodeId)
@@ -124,7 +124,6 @@ class EpisodePersonModel extends Model
             $episodeId = $person->episode_id;
         }
 
-        cache()->delete("podcast_episode#{$episodeId}_persons");
         (new EpisodeModel())->clearCache(['id' => $episodeId]);
 
         return $data;
diff --git a/app/Models/NoteModel.php b/app/Models/NoteModel.php
index 5a95a16ecd79d0513a1db8426d56034461ca8039..981e1591ba5997328179f94e63ea6225da483473 100644
--- a/app/Models/NoteModel.php
+++ b/app/Models/NoteModel.php
@@ -1,7 +1,7 @@
 <?php
 
 /**
- * @copyright  2020 Podlibre
+ * @copyright  2021 Podlibre
  * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
  * @link       https://castopod.org/
  */
diff --git a/app/Models/PodcastModel.php b/app/Models/PodcastModel.php
index 4724385fbeb55bcbf10333629bc4f03dbed6d9f7..a332f763c59b9ddfb7311a57b3b13368e3f1b43a 100644
--- a/app/Models/PodcastModel.php
+++ b/app/Models/PodcastModel.php
@@ -8,7 +8,6 @@
 
 namespace App\Models;
 
-use ActivityPub\Models\ActorModel;
 use CodeIgniter\HTTP\URI;
 use CodeIgniter\Model;
 use phpseclib\Crypt\RSA;
@@ -74,7 +73,7 @@ class PodcastModel extends Model
     protected $validationMessages = [];
 
     protected $beforeInsert = ['createPodcastActor'];
-    protected $afterInsert = ['setAvatarImageUrl'];
+    protected $afterInsert = ['setActorAvatar'];
     protected $afterUpdate = ['updatePodcastActor'];
 
     // clear cache before update if by any chance, the podcast name changes, so will the podcast link
@@ -104,6 +103,18 @@ class PodcastModel extends Model
         return $found;
     }
 
+    public function getPodcastByActorId($actorId)
+    {
+        $cacheName = "podcast_actor#{$actorId}";
+        if (!($found = cache($cacheName))) {
+            $found = $this->where('actor_id', $actorId)->first();
+
+            cache()->save($cacheName, $found, DECADE);
+        }
+
+        return $found;
+    }
+
     /**
      *  Gets all the podcasts a given user is contributing to
      *
@@ -300,25 +311,6 @@ class PodcastModel extends Model
         return $defaultQuery;
     }
 
-    public function clearCache(array $data)
-    {
-        $podcast = (new PodcastModel())->getPodcastById(
-            is_array($data['id']) ? $data['id'][0] : $data['id'],
-        );
-
-        // delete cache all podcast pages
-        cache()->deleteMatching("page_podcast#{$podcast->id}_*");
-
-        // delete model requests cache, includes feed / query / episode lists, etc.
-        cache()->deleteMatching("podcast#{$podcast->id}*");
-        cache()->delete("podcast@{$podcast->name}");
-
-        // clear cache for every credit page
-        cache()->deleteMatching('page_credits_*');
-
-        return $data;
-    }
-
     /**
      * Creates an actor linked to the podcast
      * (Triggered before insert)
@@ -359,16 +351,18 @@ class PodcastModel extends Model
         return $data;
     }
 
-    protected function setAvatarImageUrl($data)
+    protected function setActorAvatar($data)
     {
         $podcast = (new PodcastModel())->getPodcastById(
             is_array($data['id']) ? $data['id'][0] : $data['id'],
         );
 
-        $podcast->actor->avatar_image_url = $podcast->image->thumbnail_url;
-        $podcast->actor->avatar_image_mimetype = $podcast->image_mimetype;
+        $podcastActor = (new ActorModel())->find($podcast->actor_id);
 
-        (new ActorModel())->update($podcast->actor->id, $podcast->actor);
+        $podcastActor->avatar_image_url = $podcast->image->thumbnail_url;
+        $podcastActor->avatar_image_mimetype = $podcast->image_mimetype;
+
+        (new ActorModel())->update($podcast->actor_id, $podcastActor);
 
         return $data;
     }
@@ -380,7 +374,7 @@ class PodcastModel extends Model
         );
 
         $actorModel = new ActorModel();
-        $actor = $actorModel->find($podcast->actor_id);
+        $actor = $actorModel->getActorById($podcast->actor_id);
 
         // update values
         $actor->display_name = $podcast->title;
@@ -394,4 +388,28 @@ class PodcastModel extends Model
 
         return $data;
     }
+
+    public function clearCache(array $data)
+    {
+        $podcast = (new PodcastModel())->getPodcastById(
+            is_array($data['id']) ? $data['id'][0] : $data['id'],
+        );
+
+        // delete cache all podcast pages
+        cache()->deleteMatching("page_podcast#{$podcast->id}*");
+
+        // delete all cache for podcast actor
+        cache()->deleteMatching(
+            config('ActivityPub')->cachePrefix . "actor#{$podcast->actor_id}*",
+        );
+
+        // delete model requests cache, includes feed / query / episode lists, etc.
+        cache()->deleteMatching("podcast#{$podcast->id}*");
+        cache()->delete("podcast@{$podcast->name}");
+
+        // clear cache for every credit page
+        cache()->deleteMatching('page_credits_*');
+
+        return $data;
+    }
 }
diff --git a/app/Models/SoundbiteModel.php b/app/Models/SoundbiteModel.php
index 102ab7bfafc60f99a4aafdc82eca22aa8b21de66..9f7bcbc6c178bd1203c5c3cc5779201306808f6b 100644
--- a/app/Models/SoundbiteModel.php
+++ b/app/Models/SoundbiteModel.php
@@ -56,7 +56,7 @@ class SoundbiteModel extends Model
      */
     public function getEpisodeSoundbites(int $podcastId, int $episodeId): array
     {
-        $cacheName = "podcast_episode#{$episodeId}_soundbites";
+        $cacheName = "podcast#{$podcastId}_episode#{$episodeId}_soundbites";
         if (!($found = cache($cacheName))) {
             $found = $this->where([
                 'episode_id' => $episodeId,
@@ -77,7 +77,9 @@ class SoundbiteModel extends Model
                 : $data['id']['episode_id'],
         );
 
-        cache()->delete("podcast_episode#{$episode->id}_soundbites");
+        cache()->delete(
+            "podcast#{$episode->podcast_id}_episode#{$episode->id}_soundbites",
+        );
 
         // delete cache for rss feed
         cache()->deleteMatching("podcast#{$episode->podcast_id}_feed*");
diff --git a/app/Views/podcast/_partials/note.php b/app/Views/podcast/_partials/note.php
index d94f7eb390898573c7b4ffed2bafba07f8851620..c14fbef75af024734310fad10dccc555d7bc1670 100644
--- a/app/Views/podcast/_partials/note.php
+++ b/app/Views/podcast/_partials/note.php
@@ -31,7 +31,7 @@
         <?= view('podcast/_partials/episode_card', [
             'episode' => $note->episode,
         ]) ?>
-    <?php elseif ($note->preview_card_id): ?>
+    <?php elseif ($note->has_preview_card): ?>
         <?= view('podcast/_partials/preview_card', [
             'preview_card' => $note->preview_card,
         ]) ?>
diff --git a/app/Views/podcast/_partials/note_authenticated.php b/app/Views/podcast/_partials/note_authenticated.php
index 01930eb2450341d0f17568a125d65122b5dce2f5..baeb8794a418388329bab666838c9c2369a16f72 100644
--- a/app/Views/podcast/_partials/note_authenticated.php
+++ b/app/Views/podcast/_partials/note_authenticated.php
@@ -31,7 +31,7 @@
         <?= view('podcast/_partials/episode_card', [
             'episode' => $note->episode,
         ]) ?>
-    <?php elseif ($note->preview_card_id): ?>
+    <?php elseif ($note->has_preview_card): ?>
         <?= view('podcast/_partials/preview_card', [
             'preview_card' => $note->preview_card,
         ]) ?>
diff --git a/app/Views/podcast/_partials/note_with_replies.php b/app/Views/podcast/_partials/note_with_replies.php
index fa9a769dbdf4bfe44e7234c9783b6c9bcd23f0ee..c33ed4cb625c8120ceeaa374675fa926917c0992 100644
--- a/app/Views/podcast/_partials/note_with_replies.php
+++ b/app/Views/podcast/_partials/note_with_replies.php
@@ -15,7 +15,9 @@
 </div>
 
 
-<?php foreach ($note->replies as $reply): ?>
-    <?= view('podcast/_partials/reply', ['reply' => $reply]) ?>
-<?php endforeach; ?>
+<?php if ($note->has_replies): ?>
+    <?php foreach ($note->replies as $reply): ?>
+        <?= view('podcast/_partials/reply', ['reply' => $reply]) ?>
+    <?php endforeach; ?>
+<?php endif; ?>
 </div>
diff --git a/app/Views/podcast/_partials/note_with_replies_authenticated.php b/app/Views/podcast/_partials/note_with_replies_authenticated.php
index 429d99f3dbd0e651e5827e55ea9bbe71526afbb8..95e7fe725851dfd501dba1e00463a428b1cbcdd0 100644
--- a/app/Views/podcast/_partials/note_with_replies_authenticated.php
+++ b/app/Views/podcast/_partials/note_with_replies_authenticated.php
@@ -39,7 +39,11 @@
 </div>
 <?= form_close() ?>
 
-<?php foreach ($note->replies as $reply): ?>
-    <?= view('podcast/_partials/reply_authenticated', ['reply' => $reply]) ?>
-<?php endforeach; ?>
+<?php if ($note->has_replies): ?>
+    <?php foreach ($note->replies as $reply): ?>
+        <?= view('podcast/_partials/reply_authenticated', [
+            'reply' => $reply,
+        ]) ?>
+    <?php endforeach; ?>
+<?php endif; ?>
 </div>
diff --git a/app/Views/podcast/_partials/reblog.php b/app/Views/podcast/_partials/reblog.php
index fa014f1e91159ddb825910f2064256d4b519aa09..461c76ca6491c86379b0963409af15f35c4474d1 100644
--- a/app/Views/podcast/_partials/reblog.php
+++ b/app/Views/podcast/_partials/reblog.php
@@ -38,7 +38,7 @@
         <?= view('podcast/_partials/episode_card', [
             'episode' => $note->episode,
         ]) ?>
-    <?php elseif ($note->preview_card_id): ?>
+    <?php elseif ($note->has_preview_card): ?>
         <?= view('podcast/_partials/preview_card', [
             'preview_card' => $note->preview_card,
         ]) ?>
diff --git a/app/Views/podcast/_partials/reblog_authenticated.php b/app/Views/podcast/_partials/reblog_authenticated.php
index 43ecf4601254fc16a71b96bb101a8f0c0f56a4ca..e9251e256a8c839fe75c5824a70669a2e09b7901 100644
--- a/app/Views/podcast/_partials/reblog_authenticated.php
+++ b/app/Views/podcast/_partials/reblog_authenticated.php
@@ -38,7 +38,7 @@
         <?= view('podcast/_partials/episode_card', [
             'episode' => $note->episode,
         ]) ?>
-    <?php elseif ($note->preview_card_id): ?>
+    <?php elseif ($note->has_preview_card): ?>
         <?= view('podcast/_partials/preview_card', [
             'preview_card' => $note->preview_card,
         ]) ?>
diff --git a/app/Views/podcast/_partials/reply.php b/app/Views/podcast/_partials/reply.php
index c687a1e9c516914f85b7b131bcfca7044c330963..2a9018edf1f5b65902813aed63ddb3ee17cbceb6 100644
--- a/app/Views/podcast/_partials/reply.php
+++ b/app/Views/podcast/_partials/reply.php
@@ -19,7 +19,7 @@
             ><?= lang('Common.mediumDate', [$reply->published_at]) ?></time>
         </header>
         <p class="mb-2 note-content"><?= $reply->message_html ?></p>
-        <?php if ($reply->preview_card_id): ?>
+        <?php if ($reply->has_preview_card): ?>
             <?= view('podcast/_partials/preview_card', [
                 'preview_card' => $reply->preview_card,
             ]) ?>
diff --git a/app/Views/podcast/_partials/reply_authenticated.php b/app/Views/podcast/_partials/reply_authenticated.php
index e7dfebcfaa870556dcc59d0d9fc9db36b290f52d..f618dc0be683501ee873f1a4d98f123f8306d1b0 100644
--- a/app/Views/podcast/_partials/reply_authenticated.php
+++ b/app/Views/podcast/_partials/reply_authenticated.php
@@ -19,7 +19,7 @@
             ><?= lang('Common.mediumDate', [$reply->created_at]) ?></time>
         </header>
         <p class="mb-2 note-content"><?= $reply->message_html ?></p>
-        <?php if ($reply->preview_card_id): ?>
+        <?php if ($reply->has_preview_card): ?>
             <?= view('podcast/_partials/preview_card', [
                 'preview_card' => $reply->preview_card,
             ]) ?>
diff --git a/composer.json b/composer.json
index d219a958df9f9e6a1d245882eb6ee1f7f43ea546..447f16c778346d40360fb7587b48eb58d01ad5bd 100644
--- a/composer.json
+++ b/composer.json
@@ -12,14 +12,14 @@
     "geoip2/geoip2": "^v2.11.0",
     "myth/auth": "dev-develop",
     "codeigniter4/codeigniter4": "dev-develop",
-    "league/commonmark": "^1.5.7",
+    "league/commonmark": "^1.6.0",
     "vlucas/phpdotenv": "^v5.3.0",
     "league/html-to-markdown": "^4.10",
     "opawg/user-agents-php": "^v1.0",
     "podlibre/ipcat": "^v1.0",
     "podlibre/podcast-namespace": "^v1.0.6",
     "phpseclib/phpseclib": "~2.0.30",
-    "michalsn/codeigniter4-uuid": "^1.0@beta",
+    "michalsn/codeigniter4-uuid": "^v1.0.0",
     "essence/essence": "^3.5.4"
   },
   "require-dev": {
diff --git a/composer.lock b/composer.lock
index ed5ded54577419940ebb5329a13a85e72d18b6f9..89b57d985e42bd4c25449a4e03f28ac9c980fb42 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "b5d726bdc7252c80c0fd5a6f53de1948",
+    "content-hash": "f370b196462e2ca2ff3e2df9627f2ba4",
     "packages": [
         {
             "name": "brick/math",
@@ -68,12 +68,12 @@
             "source": {
                 "type": "git",
                 "url": "https://github.com/codeigniter4/CodeIgniter4.git",
-                "reference": "dfbc85af9ef408a6654cce6a462c8fdde3ee2446"
+                "reference": "8b2e7c29043977fac378c37690cc951a715c2bd5"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/codeigniter4/CodeIgniter4/zipball/dfbc85af9ef408a6654cce6a462c8fdde3ee2446",
-                "reference": "dfbc85af9ef408a6654cce6a462c8fdde3ee2446",
+                "url": "https://api.github.com/repos/codeigniter4/CodeIgniter4/zipball/8b2e7c29043977fac378c37690cc951a715c2bd5",
+                "reference": "8b2e7c29043977fac378c37690cc951a715c2bd5",
                 "shasum": ""
             },
             "require": {
@@ -91,10 +91,10 @@
                 "fakerphp/faker": "^1.9",
                 "mikey179/vfsstream": "^1.6",
                 "nexusphp/tachycardia": "^1.0",
-                "phpstan/phpstan": "0.12.84",
+                "phpstan/phpstan": "0.12.85",
                 "phpunit/phpunit": "^9.1",
                 "predis/predis": "^1.1",
-                "rector/rector": "0.10.6",
+                "rector/rector": "0.10.17",
                 "squizlabs/php_codesniffer": "^3.3"
             },
             "suggest": {
@@ -139,7 +139,7 @@
                 "slack": "https://codeigniterchat.slack.com",
                 "issues": "https://github.com/codeigniter4/CodeIgniter4/issues"
             },
-            "time": "2021-04-20T08:40:30+00:00"
+            "time": "2021-05-03T08:32:21+00:00"
         },
         {
             "name": "composer/ca-bundle",
@@ -823,16 +823,16 @@
         },
         {
             "name": "league/commonmark",
-            "version": "1.5.8",
+            "version": "1.6.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/thephpleague/commonmark.git",
-                "reference": "08fa59b8e4e34ea8a773d55139ae9ac0e0aecbaf"
+                "reference": "19a9673b833cc37770439097b381d86cd125bfe8"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/08fa59b8e4e34ea8a773d55139ae9ac0e0aecbaf",
-                "reference": "08fa59b8e4e34ea8a773d55139ae9ac0e0aecbaf",
+                "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/19a9673b833cc37770439097b381d86cd125bfe8",
+                "reference": "19a9673b833cc37770439097b381d86cd125bfe8",
                 "shasum": ""
             },
             "require": {
@@ -920,7 +920,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-03-28T18:51:39+00:00"
+            "time": "2021-05-01T19:00:49+00:00"
         },
         {
             "name": "league/html-to-markdown",
@@ -1125,16 +1125,16 @@
         },
         {
             "name": "michalsn/codeigniter4-uuid",
-            "version": "v1.0.0-beta3",
+            "version": "v1.0.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/michalsn/codeigniter4-uuid.git",
-                "reference": "568aba8f315199b6cc87e76b8441cd03a2bba5b4"
+                "reference": "c8bbd961401015307bc72f6f6aa93509ffac1d5f"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/michalsn/codeigniter4-uuid/zipball/568aba8f315199b6cc87e76b8441cd03a2bba5b4",
-                "reference": "568aba8f315199b6cc87e76b8441cd03a2bba5b4",
+                "url": "https://api.github.com/repos/michalsn/codeigniter4-uuid/zipball/c8bbd961401015307bc72f6f6aa93509ffac1d5f",
+                "reference": "c8bbd961401015307bc72f6f6aa93509ffac1d5f",
                 "shasum": ""
             },
             "require": {
@@ -1172,9 +1172,9 @@
             ],
             "support": {
                 "issues": "https://github.com/michalsn/codeigniter4-uuid/issues",
-                "source": "https://github.com/michalsn/codeigniter4-uuid/tree/v1.0.0-beta3"
+                "source": "https://github.com/michalsn/codeigniter4-uuid/tree/v1.0.1"
             },
-            "time": "2021-04-02T11:08:18+00:00"
+            "time": "2021-05-03T12:47:44+00:00"
         },
         {
             "name": "myth/auth",
@@ -1182,12 +1182,12 @@
             "source": {
                 "type": "git",
                 "url": "https://github.com/lonnieezell/myth-auth.git",
-                "reference": "eff9805d7f1d27326f14875b53ff4b3d2a6b72ee"
+                "reference": "2b42da1884745eec22ac10f7941a4f9350576a86"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/lonnieezell/myth-auth/zipball/eff9805d7f1d27326f14875b53ff4b3d2a6b72ee",
-                "reference": "eff9805d7f1d27326f14875b53ff4b3d2a6b72ee",
+                "url": "https://api.github.com/repos/lonnieezell/myth-auth/zipball/2b42da1884745eec22ac10f7941a4f9350576a86",
+                "reference": "2b42da1884745eec22ac10f7941a4f9350576a86",
                 "shasum": ""
             },
             "require": {
@@ -1248,7 +1248,7 @@
                     "type": "patreon"
                 }
             ],
-            "time": "2021-04-12T22:34:12+00:00"
+            "time": "2021-05-02T05:32:03+00:00"
         },
         {
             "name": "opawg/user-agents-php",
@@ -1584,16 +1584,16 @@
         },
         {
             "name": "psr/log",
-            "version": "1.1.3",
+            "version": "1.1.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/php-fig/log.git",
-                "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc"
+                "reference": "d49695b909c3b7628b6289db5479a1c204601f11"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc",
-                "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc",
+                "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11",
+                "reference": "d49695b909c3b7628b6289db5479a1c204601f11",
                 "shasum": ""
             },
             "require": {
@@ -1617,7 +1617,7 @@
             "authors": [
                 {
                     "name": "PHP-FIG",
-                    "homepage": "http://www.php-fig.org/"
+                    "homepage": "https://www.php-fig.org/"
                 }
             ],
             "description": "Common interface for logging libraries",
@@ -1628,9 +1628,9 @@
                 "psr-3"
             ],
             "support": {
-                "source": "https://github.com/php-fig/log/tree/1.1.3"
+                "source": "https://github.com/php-fig/log/tree/1.1.4"
             },
-            "time": "2020-03-23T09:12:05+00:00"
+            "time": "2021-05-03T11:20:27+00:00"
         },
         {
             "name": "ramsey/collection",
@@ -4314,8 +4314,7 @@
     "stability-flags": {
         "james-heinrich/getid3": 20,
         "myth/auth": 20,
-        "codeigniter4/codeigniter4": 20,
-        "michalsn/codeigniter4-uuid": 10
+        "codeigniter4/codeigniter4": 20
     },
     "prefer-stable": true,
     "prefer-lowest": false,