diff --git a/djangoldp/activities/services.py b/djangoldp/activities/services.py
index c3379a5831ec50575a3212e16843dc0e2982c912..4b817d9fe383876abfdadc8a7f0e12ddae4ea2f7 100644
--- a/djangoldp/activities/services.py
+++ b/djangoldp/activities/services.py
@@ -7,6 +7,7 @@ from django.dispatch import receiver
 from rest_framework.utils import model_meta
 
 from djangoldp.models import Model, Follower
+from djangoldp.models import Activity as ActivityModel
 
 from .objects import *
 from .verbs import *
@@ -55,228 +56,184 @@ class ActivityPubService(object):
         return obj
 
     @classmethod
-    def _discover_inbox(cls, target_id):
-        url = urlparse(target_id)
-        return target_id.replace(url.path, "/") + "inbox/"
-
-    @classmethod
-    def send_add_activity(cls, actor, object, target):
-        '''
-        Sends an Add activity
-        :param actor: a valid Actor object, or a user instance
-        :param object: a valid ActivityStreams Object
-        :param target: an object representing the target collection
-        '''
-        # bounds checking
-        if isinstance(actor, get_user_model()):
-            actor = {
+    def get_actor_from_user_instance(cls, user):
+        '''Auxiliary function returns valid Actor object from parameterised user instance, None if parameter invalid'''
+        if isinstance(user, get_user_model()) and hasattr(user, 'urlid'):
+            return {
                 '@type': 'foaf:user',
-                '@id': actor.urlid
+                '@id': user.urlid
             }
+        return None
 
-        summary = str(object['@id']) + " was added to " + str(target['@id'])
+    @classmethod
+    def discover_inbox(cls, target_id):
+        url = urlparse(target_id)
+        return target_id.replace(url.path, "/") + "inbox/"
 
-        activity = {
+    @classmethod
+    def _build_activity(self, actor, obj, activity_type='Activity', **kwargs):
+        '''Auxiliary function returns an activity object with kwargs in the body'''
+        res = {
             "@context": [
                 "https://www.w3.org/ns/activitystreams",
                 settings.LDP_RDF_CONTEXT
             ],
-            "summary": summary,
-            "type": "Add",
+            "type": activity_type,
             "actor": actor,
-            "object": object,
-            "target": target
+            "object": obj
         }
 
-        logger.debug('[Sender] sending add activity ' + str(activity))
+        for kwarg in kwargs:
+            res.update({kwarg: kwargs[kwarg]})
 
-        inbox = ActivityPubService._discover_inbox(target['@id'])
+        return res
+
+    @classmethod
+    def send_add_activity(cls, actor, obj, target):
+        '''
+        Sends an Add activity
+        :param actor: a valid Actor object
+        :param obj: a valid ActivityStreams Object
+        :param target: an object representing the target collection
+        '''
+        summary = str(obj['@id']) + " was added to " + str(target['@id'])
+        activity = cls._build_activity(actor, obj, activity_type='Add', summary=summary, target=target)
 
         # send request
+        inbox = ActivityPubService.discover_inbox(target['@id'])
         t = threading.Thread(target=cls.do_post, args=[inbox, activity])
         t.start()
+        cls._save_sent_activity(activity)
 
     @classmethod
-    def send_remove_activity(cls, actor, object, origin):
+    def send_remove_activity(cls, actor, obj, origin):
         '''
         Sends a Remove activity
         :param actor: a valid Actor object, or a user instance
-        :param object: a valid ActivityStreams Object
+        :param obj: a valid ActivityStreams Object
         :param origin: the context the object has been removed from
         '''
-        # bounds checking
-        if isinstance(actor, get_user_model()):
-            actor = {
-                '@type': 'foaf:user',
-                '@id': actor.urlid
-            }
-
-        summary = str(object['@id']) + " was removed from " + str(origin['@id'])
-
-        activity = {
-            "@context": [
-                "https://www.w3.org/ns/activitystreams",
-                settings.LDP_RDF_CONTEXT
-            ],
-            "summary": summary,
-            "type": "Remove",
-            "actor": actor,
-            "object": object,
-            "origin": origin
-        }
-
-        logger.debug('[Sender] sending remove activity ' + str(activity))
-
-        inbox = ActivityPubService._discover_inbox(origin['@id'])
+        summary = str(obj['@id']) + " was removed from " + str(origin['@id'])
+        activity = cls._build_activity(actor, obj, activity_type='Remove', summary=summary, origin=origin)
 
         # send request
+        inbox = ActivityPubService.discover_inbox(origin['@id'])
         t = threading.Thread(target=cls.do_post, args=[inbox, activity])
         t.start()
+        cls._save_sent_activity(activity)
 
     @classmethod
-    def send_create_activity(cls, actor, object, inbox):
+    def send_create_activity(cls, actor, obj, inbox):
         '''
         Sends a Create activity
         :param actor: a valid Actor object, or a user instance
-        :param object: a valid ActivityStreams Object
+        :param obj: a valid ActivityStreams Object
         :param inbox: the inbox to send the activity to
         '''
-        # bounds checking
-        if isinstance(actor, get_user_model()):
-            actor = {
-                '@type': 'foaf:user',
-                '@id': actor.urlid
-            }
-
-        summary = str(object['@id']) + " was created"
-
-        activity = {
-            "@context": [
-                "https://www.w3.org/ns/activitystreams",
-                settings.LDP_RDF_CONTEXT
-            ],
-            "summary": summary,
-            "type": "Create",
-            "actor": actor,
-            "object": object
-        }
-
-        logger.debug('[Sender] sending create activity ' + str(activity))
+        summary = str(obj['@id']) + " was created"
+        activity = cls._build_activity(actor, obj, activity_type='Create', summary=summary)
 
         # send request
         t = threading.Thread(target=cls.do_post, args=[inbox, activity])
         t.start()
+        cls._save_sent_activity(activity)
 
     @classmethod
-    def send_update_activity(cls, actor, object, inbox):
+    def send_update_activity(cls, actor, obj, inbox):
         '''
         Sends an Update activity
         :param actor: a valid Actor object, or a user instance
-        :param object: a valid ActivityStreams Object
+        :param obj: a valid ActivityStreams Object
         :param inbox: the inbox to send the activity to
         '''
-        # bounds checking
-        if isinstance(actor, get_user_model()):
-            actor = {
-                '@type': 'foaf:user',
-                '@id': actor.urlid
-            }
-
-        summary = str(object['@id']) + " was updated"
-
-        activity = {
-            "@context": [
-                "https://www.w3.org/ns/activitystreams",
-                settings.LDP_RDF_CONTEXT
-            ],
-            "summary": summary,
-            "type": "Update",
-            "actor": actor,
-            "object": object
-        }
-
-        logger.debug('[Sender] sending update activity ' + str(activity))
+        summary = str(obj['@id']) + " was updated"
+        activity = cls._build_activity(actor, obj, activity_type='Update', summary=summary)
 
         # send request
         t = threading.Thread(target=cls.do_post, args=[inbox, activity])
         t.start()
+        cls._save_sent_activity(activity)
 
     @classmethod
-    def send_delete_activity(cls, actor, object, inbox):
+    def send_delete_activity(cls, actor, obj, inbox):
         '''
         Sends a Remove activity
         :param actor: a valid Actor object, or a user instance
-        :param object: a valid ActivityStreams Object
+        :param obj: a valid ActivityStreams Object
         :param inbox: the inbox to send the activity to
         '''
-        # bounds checking
-        if isinstance(actor, get_user_model()):
-            actor = {
-                '@type': 'foaf:user',
-                '@id': actor.urlid
-            }
-
-        summary = str(object['@id']) + " was deleted"
-
-        activity = {
-            "@context": [
-                "https://www.w3.org/ns/activitystreams",
-                settings.LDP_RDF_CONTEXT
-            ],
-            "summary": summary,
-            "type": "Delete",
-            "actor": actor,
-            "object": object
-        }
-
-        logger.debug('[Sender] sending delete activity ' + str(activity))
+        summary = str(obj['@id']) + " was deleted"
+        activity = cls._build_activity(actor, obj, activity_type='Delete', summary=summary)
 
         # send request
         t = threading.Thread(target=cls.do_post, args=[inbox, activity])
         t.start()
+        cls._save_sent_activity(activity)
+
+    @classmethod
+    def _save_sent_activity(cls, activity):
+        '''Auxiliary function saves a record of parameterised activity'''
+        payload = bytes(json.dumps(activity), "utf-8")
+        local_id = settings.SITE_URL + "outbox/"
+        obj = ActivityModel.objects.create(local_id=local_id, payload=payload)
+        obj.aid = Model.absolute_url(obj)
+        obj.save()
 
     @classmethod
     def do_post(cls, url, activity, auth=None):
-        '''makes a POST request to passed url'''
+        '''
+        makes a POST request to url, passing activity (json) content
+        :return: response, or None if the request was unsuccessful
+        '''
         headers = {'Content-Type': 'application/ld+json'}
         response = None
         try:
-            response = requests.post(url, data=json.dumps(activity), headers=headers)
-            logger.debug('[Sender] sent, receiver responded ' + response.text)
+            logger.debug('[Sender] sending Activity... ' + str(activity))
+            if not getattr(settings, 'DISABLE_OUTBOX', False):
+                response = requests.post(url, data=json.dumps(activity), headers=headers)
+                logger.debug('[Sender] sent, receiver responded ' + response.text)
         except:
             logger.error('Failed to deliver backlink to ' + str(url) +', was attempting ' + str(activity))
         return response
 
+    @classmethod
+    def get_related_externals(cls, sender, instance):
+        '''Auxiliary function returns a set of urlids of distant resources connected to paramertised instance'''
+        info = model_meta.get_field_info(sender)
 
-def _check_instance_for_backlinks(sender, instance):
-    '''Auxiliary function returns a set of backlink targets from paramertised instance'''
-    info = model_meta.get_field_info(sender)
+        # bounds checking
+        if not hasattr(instance, 'urlid') or Model.get_model_rdf_type(sender) is None:
+            return set()
 
-    # bounds checking
-    if not hasattr(instance, 'urlid') or Model.get_model_rdf_type(sender) is None:
-        return {}
+        # check each foreign key for a distant resource
+        targets = set()
+        for field_name, relation_info in info.relations.items():
+            if not relation_info.to_many:
+                value = getattr(instance, field_name, None)
+                logger.debug('[Sender] model has relation ' + str(value))
+                if value is not None and Model.is_external(value):
+                    target_type = Model.get_model_rdf_type(type(value))
 
-    # check each foreign key for a distant resource
-    targets = set()
-    for field_name, relation_info in info.relations.items():
-        if not relation_info.to_many:
-            value = getattr(instance, field_name, None)
-            logger.debug('[Sender] model has relation ' + str(value))
-            if value is not None and Model.is_external(value):
-                target_type = Model.get_model_rdf_type(type(value))
+                    if target_type is None:
+                        continue
 
-                if target_type is None:
-                    continue
+                    targets.add(value.urlid)
 
-                targets.add(ActivityPubService._discover_inbox(value.urlid))
+        return targets
 
-    # append Followers as targets
-    followers = Follower.objects.filter(object=instance.urlid)
-    for follower in followers:
-        targets.add(follower.inbox)
+    @classmethod
+    def get_target_inboxes(cls, urlids):
+        '''Auxiliary function returns a set of inboxes, from a set of target object urlids'''
+        inboxes = set()
+        for urlid in urlids:
+            inboxes.add(ActivityPubService.discover_inbox(urlid))
+        return inboxes
 
-    logger.debug('[Sender] built set of targets: ' + str(targets))
-    return targets
+    @classmethod
+    def get_follower_inboxes(cls, object_urlid):
+        '''Auxiliary function returns a set of inboxes, from the followers of parameterised object urlid'''
+        inboxes = set(Follower.objects.filter(object=object_urlid).values_list('inbox', flat=True))
+        return inboxes
 
 
 @receiver([post_save])
@@ -284,8 +241,9 @@ def check_save_for_backlinks(sender, instance, created, **kwargs):
     if getattr(settings, 'SEND_BACKLINKS', True) and getattr(instance, 'allow_create_backlink', False) \
             and not Model.is_external(instance) \
             and getattr(instance, 'username', None) != 'hubl-workaround-493':
-        logger.debug("[Sender] Received created non-backlink instance " + str(instance) + "(" + str(sender) + ")")
-        targets = _check_instance_for_backlinks(sender, instance)
+        external_urlids = ActivityPubService.get_related_externals(sender, instance)
+        inboxes = ActivityPubService.get_follower_inboxes(instance.urlid)
+        targets = set().union(ActivityPubService.get_target_inboxes(external_urlids), inboxes)
 
         if len(targets) > 0:
             obj = ActivityPubService.build_object_tree(instance)
@@ -297,21 +255,24 @@ def check_save_for_backlinks(sender, instance, created, **kwargs):
             if created:
                 for target in targets:
                     ActivityPubService.send_create_activity(actor, obj, target)
-                    Follower.objects.create(object=obj['@id'], inbox=target)
             # Update Activity
             else:
                 for target in targets:
                     ActivityPubService.send_update_activity(actor, obj, target)
-                    if not Follower.objects.filter(object=obj['@id'], inbox=target).exists():
-                        Follower.objects.create(object=obj['@id'], inbox=target)
+
+            # create Followers to update external resources of changes in future
+            existing_followers = Follower.objects.filter(object=obj['@id']).values_list('follower', flat=True)
+            for urlid in external_urlids:
+                if urlid not in existing_followers:
+                    Follower.objects.create(object=obj['@id'], inbox=ActivityPubService.discover_inbox(urlid),
+                                            follower=urlid, is_backlink=True)
 
 
 @receiver([post_delete])
 def check_delete_for_backlinks(sender, instance, **kwargs):
     if getattr(settings, 'SEND_BACKLINKS', True) and getattr(instance, 'allow_create_backlink', False) \
             and getattr(instance, 'username', None) != 'hubl-workaround-493':
-        logger.debug("[Sender] Received deleted non-backlink instance " + str(instance) + "(" + str(sender) + ")")
-        targets = _check_instance_for_backlinks(sender, instance)
+        targets = ActivityPubService.get_follower_inboxes(instance.urlid)
 
         if len(targets) > 0:
             for target in targets:
@@ -384,6 +345,10 @@ def check_m2m_for_backlinks(sender, instance, action, *args, **kwargs):
                         "type": "Service",
                         "name": "Backlinks Service"
                     }, obj, target)
+                    inbox = ActivityPubService.discover_inbox(target['@id'])
+                    if not Follower.objects.filter(object=obj['@id'], follower=target['@id']).exists():
+                        Follower.objects.create(object=obj['@id'], inbox=inbox, follower=target['@id'],
+                                                is_backlink=True)
 
             elif action == "post_remove" or action == "pre_clear":
                 for target in targets:
@@ -391,3 +356,6 @@ def check_m2m_for_backlinks(sender, instance, action, *args, **kwargs):
                         "type": "Service",
                         "name": "Backlinks Service"
                     }, obj, target)
+                    for follower in Follower.objects.filter(object=obj['@id'], follower=target['@id'],
+                                                            is_backlink=True):
+                        follower.delete()
diff --git a/djangoldp/migrations/0013_auto_20200624_1709.py b/djangoldp/migrations/0013_auto_20200624_1709.py
new file mode 100644
index 0000000000000000000000000000000000000000..2fd89ca6a560682408f5042d52b0619e0f9e099c
--- /dev/null
+++ b/djangoldp/migrations/0013_auto_20200624_1709.py
@@ -0,0 +1,30 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.29 on 2020-06-24 17:09
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('djangoldp', '0012_auto_20200617_1817'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='follower',
+            name='follower',
+            field=models.URLField(blank=True, help_text='(optional) the resource/actor following the object'),
+        ),
+        migrations.AlterField(
+            model_name='follower',
+            name='inbox',
+            field=models.URLField(help_text='the inbox recipient of updates'),
+        ),
+        migrations.AlterField(
+            model_name='follower',
+            name='object',
+            field=models.URLField(help_text='the object being followed'),
+        ),
+    ]
diff --git a/djangoldp/models.py b/djangoldp/models.py
index fb99c63801464bad0aa0723bd4b1668b79e74864..d970ac35402eb1c409d171690a6d60a734c36af4 100644
--- a/djangoldp/models.py
+++ b/djangoldp/models.py
@@ -273,8 +273,9 @@ class Activity(Model):
 
 class Follower(Model):
     '''Models a subscription on a model. When the model is saved, an Update activity will be sent to the inbox'''
-    object = models.URLField()
-    inbox = models.URLField()
+    object = models.URLField(help_text='the object being followed')
+    inbox = models.URLField(help_text='the inbox recipient of updates')
+    follower = models.URLField(help_text='(optional) the resource/actor following the object', blank=True)
 
     def __str__(self):
         return 'Inbox ' + str(self.inbox) + ' on ' + str(self.object)
diff --git a/djangoldp/tests/runner.py b/djangoldp/tests/runner.py
index 66794179358e79c6d062775c6cabcc64623226f8..af89c6b874dac8a703ed4fcb1ea802455b4c0e23 100644
--- a/djangoldp/tests/runner.py
+++ b/djangoldp/tests/runner.py
@@ -24,6 +24,7 @@ failures = test_runner.run_tests([
     'djangoldp.tests.tests_sources',
     'djangoldp.tests.tests_pagination',
     'djangoldp.tests.tests_inbox',
+    'djangoldp.tests.tests_backlinks_service',
     #'djangoldp.tests.tests_temp'
 ])
 if failures:
diff --git a/djangoldp/tests/tests_backlinks_service.py b/djangoldp/tests/tests_backlinks_service.py
new file mode 100644
index 0000000000000000000000000000000000000000..05dcde6a0c816689fed2aeb61c084f749399522f
--- /dev/null
+++ b/djangoldp/tests/tests_backlinks_service.py
@@ -0,0 +1,91 @@
+import json
+import uuid
+from django.contrib.auth import get_user_model
+from django.db import IntegrityError
+from django.test import override_settings
+from rest_framework.test import APIClient, APITestCase
+from djangoldp.tests.models import Circle, CircleMember, Project, UserProfile, DateModel, DateChild
+from djangoldp.models import Activity, Follower
+
+
+class TestsBacklinksService(APITestCase):
+
+    def setUp(self):
+        self.client = APIClient(enforce_csrf_checks=True)
+        self.local_user = get_user_model().objects.create_user(username='john', email='jlennon@beatles.com',
+                                                               password='glass onion')
+
+    def _get_random_external_user(self):
+        '''Auxiliary function creates a user with random external urlid and returns it'''
+        username = str(uuid.uuid4())
+        email = username + '@test.com'
+        urlid = 'https://distant.com/users/' + username
+        return get_user_model().objects.create_user(username=username, email=email, password='test', urlid=urlid)
+
+    @override_settings(SEND_BACKLINKS=True, DISABLE_OUTBOX=True)
+    def test_local_object_with_distant_foreign_key(self):
+        # a local Circle with a distant owner
+        local_circle = Circle.objects.create(description='Test')
+        external_user = self._get_random_external_user()
+        local_circle.owner = external_user
+        local_circle.save()
+
+        # assert that a activity was sent
+        self.assertEqual(Activity.objects.all().count(), 1)
+
+        # reset to a local user, another (update) activity should be sent
+        local_circle.owner = self.local_user
+        local_circle.save()
+        self.assertEqual(Activity.objects.all().count(), 2)
+
+        # external user should no longer be following the object. A further update should not send an activity
+        # TODO: https://git.startinblox.com/djangoldp-packages/djangoldp/issues/257
+        '''another_user = get_user_model().objects.create_user(username='test', email='test@test.com',
+                                                            password='glass onion')
+        local_circle.owner = another_user
+        local_circle.save()
+        self.assertEqual(Activity.objects.all().count(), 2)'''
+
+        # re-add the external user as owner
+        local_circle.owner = external_user
+        local_circle.save()
+
+        # delete parent
+        local_circle.delete()
+        self.assertEqual(Activity.objects.all().count(), 4)
+
+    @override_settings(SEND_BACKLINKS=True, DISABLE_OUTBOX=True)
+    def test_local_object_with_external_m2m_join_leave(self):
+        # a local project with three distant users
+        project = Project.objects.create(description='Test')
+        external_a = self._get_random_external_user()
+        external_b = self._get_random_external_user()
+        external_c = self._get_random_external_user()
+        project.team.add(external_a)
+        project.team.add(external_b)
+        project.team.add(external_c)
+        self.assertEqual(Activity.objects.all().count(), 3)
+
+        # remove one individual
+        project.team.remove(external_a)
+        self.assertEqual(Activity.objects.all().count(), 4)
+
+        # clear the rest
+        project.team.clear()
+        self.assertEqual(Activity.objects.all().count(), 6)
+        prior_count = Activity.objects.all().count()
+
+        # once removed I should not be following the object anymore
+        project.delete()
+        self.assertEqual(Activity.objects.all().count(), prior_count)
+
+    @override_settings(SEND_BACKLINKS=True, DISABLE_OUTBOX=True)
+    def test_local_object_with_external_m2m_delete_parent(self):
+        # a local project with three distant users
+        project = Project.objects.create(description='Test')
+        external_a = self._get_random_external_user()
+        project.team.add(external_a)
+        prior_count = Activity.objects.all().count()
+
+        project.delete()
+        self.assertEqual(Activity.objects.all().count(), prior_count + 1)
diff --git a/djangoldp/tests/tests_inbox.py b/djangoldp/tests/tests_inbox.py
index 532fb676b37388fb80a5c7ece13c48a2f13c1088..b11f7ddbe78a5146ba6c2c21bf275da53361b5f8 100644
--- a/djangoldp/tests/tests_inbox.py
+++ b/djangoldp/tests/tests_inbox.py
@@ -10,9 +10,8 @@ class TestsInbox(APITestCase):
 
     def setUp(self):
         self.client = APIClient(enforce_csrf_checks=True)
-        self.user = get_user_model().objects.create(username='john', email='jlennon@beatles.com',
-                                                    password='glass onion')
-        self.profile = UserProfile.objects.create(user=self.user)
+        self.user = get_user_model().objects.create_user(username='john', email='jlennon@beatles.com',
+                                                         password='glass onion')
 
     def _get_activity_request_template(self, type, obj, target=None, origin=None):
         res = {