diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 6a7343bd30c6f680fa04bae4f8769bd3bdbb14eb..6f2f75d972385e5fedb73344878fad907e843ae4 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -2,9 +2,22 @@
 image: python:3.6
 
 stages:
+  - test
   - release
 
 include:
   - project: 'infra/platform'
     ref: master
     file: '/templates/python.ci.yml'
+
+
+test:
+  stage: test
+  script:
+    - pip install .[dev]
+    - python -m unittest djangoldp_community.tests.runner
+  except:
+    - master
+    - tags
+  tags:
+    - test
\ No newline at end of file
diff --git a/djangoldp_community/migrations/0003_auto_20210218_1145.py b/djangoldp_community/migrations/0003_auto_20210218_1145.py
new file mode 100644
index 0000000000000000000000000000000000000000..7817aa6ef098ec7771c84e1dc5782da8c291f48e
--- /dev/null
+++ b/djangoldp_community/migrations/0003_auto_20210218_1145.py
@@ -0,0 +1,38 @@
+# Generated by Django 2.2.18 on 2021-02-18 10:45
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('djangoldp_community', '0002_auto_20210217_1205'),
+    ]
+
+    operations = [
+        migrations.AlterModelOptions(
+            name='community',
+            options={'default_permissions': ['add', 'change', 'delete', 'view', 'control'], 'ordering': ['slug'], 'verbose_name': 'community', 'verbose_name_plural': 'communities'},
+        ),
+        migrations.AlterModelOptions(
+            name='communitycircle',
+            options={'default_permissions': ['add', 'change', 'delete', 'view', 'control'], 'verbose_name': 'community circle', 'verbose_name_plural': 'community circles'},
+        ),
+        migrations.AlterModelOptions(
+            name='communityjoboffer',
+            options={'default_permissions': ['add', 'change', 'delete', 'view', 'control'], 'verbose_name': 'community job offer', 'verbose_name_plural': 'community job offers'},
+        ),
+        migrations.AlterModelOptions(
+            name='communitymember',
+            options={'default_permissions': ['add', 'change', 'delete', 'view', 'control'], 'verbose_name': 'community member', 'verbose_name_plural': 'community members'},
+        ),
+        migrations.AlterModelOptions(
+            name='communityproject',
+            options={'default_permissions': ['add', 'change', 'delete', 'view', 'control'], 'verbose_name': 'community project', 'verbose_name_plural': 'community projects'},
+        ),
+        migrations.AlterField(
+            model_name='community',
+            name='name',
+            field=models.CharField(blank=True, help_text="Changing a community's name is highly discouraged", max_length=255),
+        ),
+    ]
diff --git a/djangoldp_community/models.py b/djangoldp_community/models.py
index c7e95fa3f7be4f7dfbe603378b80d19cc1f1d8a1..ca0e4ecb446e0f7075bfb9d47ea21a64fe4a88c3 100644
--- a/djangoldp_community/models.py
+++ b/djangoldp_community/models.py
@@ -8,7 +8,9 @@ from djangoldp.models import Model
 from django.utils.text import slugify
 from django.utils.translation import ugettext_lazy as _
 
-from djangoldp_community.permissions import CommunityPermissions
+from djangoldp_community.permissions import CommunityPermissions, CommunityCirclePermissions, \
+    CommunityProjectPermissions
+from djangoldp_community.views import CommunityMembersViewset
 
 from djangoldp_circle.models import Circle
 from djangoldp_project.models import Project
@@ -27,8 +29,9 @@ class Community(Model):
         verbose_name = _('community')
         verbose_name_plural = _("communities")
         permission_classes = [CommunityPermissions]
-        anonymous_perms = []
-        authenticated_perms = []
+        anonymous_perms = ['view']
+        authenticated_perms = ['inherit', 'add']
+        superuser_perms = ['view']
         lookup_field = 'slug'
         container_path = "/communities/"
         ordering = ['slug']
@@ -43,9 +46,11 @@ class CommunityMember(Model):
     class Meta(Model.Meta):
         verbose_name = _('community member')
         verbose_name_plural = _("community members")
+        view_set = CommunityMembersViewset
         permission_classes = [CommunityPermissions]
-        anonymous_perms = []
-        authenticated_perms = []
+        anonymous_perms = ['view', 'add']
+        authenticated_perms = ['inherit']
+        superuser_perms = ['view', 'add']
         container_path = "community-members/"
         serializer_fields = ['@id', 'community', 'user', 'is_admin']
         rdf_type = "as:items"
@@ -57,9 +62,10 @@ class CommunityCircle(Model):
     class Meta(Model.Meta):
         verbose_name = _('community circle')
         verbose_name_plural = _("community circles")
-        permission_classes = [CommunityPermissions]
+        permission_classes = [CommunityPermissions, CommunityCirclePermissions]
         anonymous_perms = []
         authenticated_perms = []
+        superuser_perms = []
         container_path = "community-circles/"
         serializer_fields = ['@id', 'community', 'circle']
         rdf_type = "as:items"
@@ -71,9 +77,10 @@ class CommunityProject(Model):
     class Meta(Model.Meta):
         verbose_name = _('community project')
         verbose_name_plural = _("community projects")
-        permission_classes = [CommunityPermissions]
+        permission_classes = [CommunityPermissions, CommunityProjectPermissions]
         anonymous_perms = []
         authenticated_perms = []
+        superuser_perms = []
         container_path = "community-projects/"
         serializer_fields = ['@id', 'community', 'project']
         rdf_type = "as:items"
@@ -88,6 +95,7 @@ class CommunityJobOffer(Model):
         permission_classes = [CommunityPermissions]
         anonymous_perms = []
         authenticated_perms = []
+        superuser_perms = []
         container_path = "community-joboffers/"
         serializer_fields = ['@id', 'community', 'joboffer']
         rdf_type = "as:items"
diff --git a/djangoldp_community/permissions.py b/djangoldp_community/permissions.py
index 95cff43c7823cdc34181eb2706367f5890819e63..075d6e7052eea4436904aae1d3aafba315353a2b 100644
--- a/djangoldp_community/permissions.py
+++ b/djangoldp_community/permissions.py
@@ -1,97 +1,65 @@
 from djangoldp.permissions import LDPPermissions
-from django.db.models.base import ModelBase
+from djangoldp.utils import is_authenticated_user
+from djangoldp_project.permissions import ProjectPermissions
+from djangoldp_circle.permissions import CirclePermissions
 
 
 class CommunityPermissions(LDPPermissions):
 
     filter_backends = []
 
-    def user_permissions(self, user, model, obj, community=None):
-        # Get guardians permissions, over everything else
-        perms = set(super().user_permissions(user, model, obj))
+    def _get_community_from_obj(self, obj):
+        from djangoldp_community.models import Community
+        if isinstance(obj, Community):
+            return obj
+
+        if not hasattr(obj, 'community') or not isinstance(obj.community, Community):
+            raise KeyError('Object ' + str(obj) + ' must have a ForeignKey "community" to model Community to use CommunityPermissions')
 
-        # Communities affiliations list are public
-        perms = perms.union({'view'})
+        return obj.community
 
+    def get_object_permissions(self, request, view, obj):
         from djangoldp_community.models import Community, CommunityMember
-        if community and isinstance(community, Community):
 
-            if model == CommunityMember and community.allow_self_registration:
-                perms = perms.union({'add'})
+        perms = set(super().get_object_permissions(request, view, obj))
 
-            # Get the user membership, also manage the anonymous user
-            try:
-                membership = community.members.get(user=user)
-            except:
-                membership = None
+        community = self._get_community_from_obj(obj)
 
-            if membership:
-                # Any member can add a job offer, circle or project to the community
-                if not model == Community and not model == CommunityMember:
-                    perms = perms.union({'add'})
+        if is_authenticated_user(request.user) and community.members.filter(user=request.user).exists():
+            # Any member can add a job offer, circle or project to the community
+            if not isinstance(obj, Community) and not isinstance(obj, CommunityMember):
+                perms = perms.union({'add'})
 
-                if membership.is_admin:
-                    # Admins can add members
-                    perms = perms.union({'add'})
+            member = community.members.get(user=request.user)
+            if member.is_admin:
+                # Admins can add members
+                perms = perms.union({'add', 'change'})
 
-                    if not isinstance(obj, Community) and not obj.is_admin:
+                if isinstance(obj, CommunityMember):
+                    if (obj.user == request.user and community.members.filter(is_admin=True).count() > 1) \
+                            or not obj.is_admin:
                         # Admins can't delete community or other admins, but have super-powers on everything else
+                        # I can't delete myself if I am the last member
                         perms = perms.union({'delete'})
+                elif not isinstance(obj, Community):
+                    perms = perms.union({'delete'})
+            elif isinstance(obj, CommunityMember) and obj.user == request.user:
+                perms = perms.union({'delete'})
 
-        return list(perms)
-
-
-    def get_model_or_obj(self, request, view, origin=None):
-    
-        from djangoldp.models import Model
-        if self.is_a_container(request._request.path):
-            try:
-                # Container have a parent, follow its perms
-                obj = Model.resolve_parent(request.path)
-                model = view.parent_model
-            except:
-                # Container have no parent
-                obj = None
-                model = view.model
-        else:
-            # Is an object
-            obj = Model.resolve_id(request._request.path)
-            model = view.model
-
-        if origin:
-            # Always follow origins
-            if isinstance(origin, ModelBase):
-                # Have an origin, is a model
-                model = origin
-            if not isinstance(origin, ModelBase):
-                # Have an origin, is an object
-                obj = origin
-
-        from djangoldp_community.models import Community
-        # Get the related community, from origin or from the object itself
-        if isinstance(origin, Community):
-            community = origin
-        else:
-            community = getattr(obj, 'community', obj)
-        
-        return community, model, obj
+        return perms
 
 
-    def filter_user_perms(self, context, origin, permissions):
-        # Only used on Model.get_permissions to translate permissions to LDP
-        community, model, obj = self.get_model_or_obj(context['request'], context['view'], origin)
-        return [perm for perm in permissions if perm in self.user_permissions(context['request'].user, model, obj, community)]
+class CommunityCirclePermissions(CirclePermissions):
+    filter_backends = []
 
+    def get_object_permissions(self, request, view, obj):
+        obj = obj.circle
+        return set(super().get_object_permissions(request, view, obj))
 
-    def has_permission(self, request, view):
-        # get permissions required
-        community, model, obj = self.get_model_or_obj(request, view)
-        perms = self.get_permissions(request.method, model)
-        user_perms = self.user_permissions(request.user, model, obj, community)
 
-        # compare them with the permissions I have
-        for perm in perms:
-            if not perm.split('.')[-1].split('_')[0] in user_perms:
-                return False
+class CommunityProjectPermissions(ProjectPermissions):
+    filter_backends = []
 
-        return True
+    def get_object_permissions(self, request, view, obj):
+        obj = obj.project
+        return set(super().get_object_permissions(request, view, obj))
diff --git a/djangoldp_community/tests/__init__.py b/djangoldp_community/tests/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/djangoldp_community/tests/models.py b/djangoldp_community/tests/models.py
new file mode 100644
index 0000000000000000000000000000000000000000..7935695475e7d30f8e68e003f0b675493c6c9f4c
--- /dev/null
+++ b/djangoldp_community/tests/models.py
@@ -0,0 +1,13 @@
+from django.contrib.auth.models import AbstractUser
+
+from djangoldp.models import Model
+
+
+# we have a custom user Model for running in the test envrionment only
+class User(AbstractUser, Model):
+
+    class Meta(AbstractUser.Meta, Model.Meta):
+        serializer_fields = ['@id', 'username', 'first_name', 'last_name', 'email']
+        anonymous_perms = ['view', 'add']
+        authenticated_perms = ['inherit', 'change']
+        owner_perms = ['inherit']
diff --git a/djangoldp_community/tests/runner.py b/djangoldp_community/tests/runner.py
new file mode 100644
index 0000000000000000000000000000000000000000..00d80354e89eff186140fdf3fb4a57c80b93bffa
--- /dev/null
+++ b/djangoldp_community/tests/runner.py
@@ -0,0 +1,47 @@
+import sys
+import yaml
+
+import django
+from django.conf import settings as django_settings
+from djangoldp.conf.ldpsettings import LDPSettings
+from djangoldp.tests.settings_default import yaml_config
+
+# this is where we configure the server settings that we will run our tests with
+config = {
+    # add the packages to the reference list
+    'ldppackages': ['modeltranslation', 'djangoldp_circle', 'djangoldp_project', 'djangoldp_conversation', 'djangoldp_community',
+                    'djangoldp_skill', 'djangoldp_joboffer', 'djangoldp_community.tests'],
+
+    # required values for server
+    'server': {
+        'AUTH_USER_MODEL': 'tests.User',
+        'REST_FRAMEWORK': {
+            'DEFAULT_PAGINATION_CLASS': 'djangoldp.pagination.LDPPagination',
+            'PAGE_SIZE': 5
+        },
+        # map the config of the core settings (avoid asserts to fail)
+        'SITE_URL': 'http://happy-dev.fr',
+        'BASE_URL': 'http://happy-dev.fr',
+        'SEND_BACKLINKS': False,
+        'JABBER_DEFAULT_HOST': None,
+        'PERMISSIONS_CACHE': False,
+        'ANONYMOUS_USER_NAME': None,
+        'SERIALIZER_CACHE': False
+    }
+}
+ldpsettings = LDPSettings(config)
+ldpsettings.config = yaml.safe_load(yaml_config)
+
+django_settings.configure(ldpsettings)
+
+django.setup()
+from django.test.runner import DiscoverRunner
+
+test_runner = DiscoverRunner(verbosity=1)
+
+# this is where we link our test classes to the runner
+failures = test_runner.run_tests([
+    'djangoldp_community.tests.tests_permissions',
+])
+if failures:
+    sys.exit(failures)
diff --git a/djangoldp_community/tests/tests_permissions.py b/djangoldp_community/tests/tests_permissions.py
new file mode 100644
index 0000000000000000000000000000000000000000..1fe158f1dd0b2c6fd0f33bef8671f18cda162f34
--- /dev/null
+++ b/djangoldp_community/tests/tests_permissions.py
@@ -0,0 +1,249 @@
+import uuid
+import json
+from datetime import datetime, timedelta
+
+from djangoldp.serializers import LDListMixin, LDPSerializer
+from rest_framework.test import APITestCase, APIClient
+
+from djangoldp_community.models import Community, CommunityMember, CommunityCircle, CommunityProject, CommunityJobOffer
+from djangoldp_community.tests.models import User
+
+
+class PermissionsTestCase(APITestCase):
+    # Django runs setUp automatically before every test
+    def setUp(self):
+        # we set up a client, that allows us
+        self.client = APIClient()
+        LDListMixin.to_representation_cache.reset()
+        LDPSerializer.to_representation_cache.reset()
+
+    # we have custom set up functions for things that we don't want to run before *every* test, e.g. often we want to
+    # set up an authenticated user, but sometimes we want to run a test with an anonymous user
+    def setUpLoggedInUser(self, is_superuser=False):
+        self.user = User(email='test@mactest.co.uk', first_name='Test', last_name='Mactest', username='test',
+                         password='glass onion', is_superuser=is_superuser)
+        self.user.save()
+        # this means that our user is now logged in (as if they had typed username and password)
+        self.client.force_authenticate(user=self.user)
+
+    # we write functions like this for convenience - we can reuse between tests
+    def _get_random_user(self):
+        return User.objects.create(email='{}@test.co.uk'.format(str(uuid.uuid4())), first_name='Test',
+                                   last_name='Test',
+                                   username=str(uuid.uuid4()))
+
+    def _get_random_community(self):
+        return Community.objects.create(name='Test', slug=str(uuid.uuid4()))
+
+    def _get_community_member(self, user, community, is_admin=False):
+        return CommunityMember.objects.create(user=user, community=community, is_admin=is_admin)
+
+    '''
+    list communities - public
+    list community members - public
+    create community - authenticated
+    update, delete, control community - community admin only
+    create, update, delete, control community member - community admin only
+    Admins can't remove admins (or themselves if they're the last admin)
+    community projects - apply Project permissions (same for JobOffers and Circles)
+    '''
+    # only authenticated users can create communities
+    def test_post_community_anonymous(self):
+        response = self.client.post('/communities/', data=json.dumps({}), content_type='application/ld+json')
+        self.assertEqual(response.status_code, 403)
+
+    def test_post_community_authenticated(self):
+        self.setUpLoggedInUser()
+        response = self.client.post('/communities/', data=json.dumps({}), content_type='application/ld+json')
+        self.assertEqual(response.status_code, 201)
+
+    # only community admins can update communities
+    def test_update_community_is_admin(self):
+        self.setUpLoggedInUser()
+        community = self._get_random_community()
+        self._get_community_member(user=self.user, community=community, is_admin=True)
+        response = self.client.patch('/communities/{}/'.format(community.slug), data=json.dumps({}),
+                                     content_type='application/ld+json')
+        self.assertEqual(response.status_code, 200)
+
+    def test_update_community_is_member(self):
+        self.setUpLoggedInUser(is_superuser=False)
+        community = self._get_random_community()
+        self._get_community_member(user=self.user, community=community, is_admin=False)
+        response = self.client.patch('/communities/{}/'.format(community.slug), data=json.dumps({}),
+                                     content_type='application/ld+json')
+        self.assertEqual(response.status_code, 403)
+
+    def test_update_community_is_auth_super_user(self):
+        self.setUpLoggedInUser(is_superuser=True)
+        community = self._get_random_community()
+        self._get_community_member(user=self.user, community=community, is_admin=False)
+        response = self.client.patch('/communities/{}/'.format(community.slug), data=json.dumps({}),
+                                     content_type='application/ld+json')
+        self.assertEqual(response.status_code, 403)
+
+    # only community admins can delete communities
+    def test_delete_community_is_admin(self):
+        self.setUpLoggedInUser(is_superuser=False)
+        community = self._get_random_community()
+        self._get_community_member(user=self.user, community=community, is_admin=True)
+        response = self.client.delete('/communities/{}/'.format(community.slug))
+        self.assertEqual(response.status_code, 403)
+
+    def test_delete_community_is_member(self):
+        self.setUpLoggedInUser(is_superuser=False)
+        community = self._get_random_community()
+        self._get_community_member(user=self.user, community=community, is_admin=False)
+        response = self.client.delete('/communities/{}/'.format(community.slug))
+        self.assertEqual(response.status_code, 403)
+
+    def test_delete_community_is_auth_super_user(self):
+        self.setUpLoggedInUser(is_superuser=True)
+        community = self._get_random_community()
+        self._get_community_member(user=self.user, community=community, is_admin=False)
+        response = self.client.delete('/communities/{}/'.format(community.slug))
+        self.assertEqual(response.status_code, 403)
+
+    # TODO: https://git.startinblox.com/djangoldp-packages/djangoldp/issues/363
+    '''
+    def test_get_community_is_member(self):
+        self.setUpLoggedInUser(is_superuser=False)
+        community = self._get_random_community()
+        me = self._get_community_member(user=self.user, community=community, is_admin=False)
+
+        response = self.client.get('/communities/{}/members/'.format(community.slug))
+        self.assertEqual(response.status_code, 200)
+        self.assertNotIn({'mode': {'@type': 'add'}}, response.data['permissions'])
+        self.assertEqual(len(response.data['permissions']), 1)
+    '''
+
+    # only community admins can do any operation on community members
+    def test_add_community_member_is_admin(self):
+        self.setUpLoggedInUser(is_superuser=False)
+        another_user = self._get_random_user()
+
+        community = self._get_random_community()
+        me = self._get_community_member(user=self.user, community=community, is_admin=True)
+
+        body = {
+            'http://happy-dev.fr/owl#community': community.urlid,
+            'http://happy-dev.fr/owl#user': another_user.urlid
+        }
+
+        response = self.client.post('/communities/{}/members/'.format(community.slug),
+                                    body=json.dumps(body), content_type='application/ld+json')
+        self.assertEqual(response.status_code, 201)
+
+    def test_add_community_member_is_member(self):
+        self.setUpLoggedInUser(is_superuser=False)
+        another_user = self._get_random_user()
+
+        community = self._get_random_community()
+        me = self._get_community_member(user=self.user, community=community, is_admin=False)
+
+        body = {
+            'http://happy-dev.fr/owl#community': community.urlid,
+            'http://happy-dev.fr/owl#user': another_user.urlid
+        }
+
+        response = self.client.post('/communities/{}/members/', body=json.dumps(body), content_type='application/ld+json')
+        self.assertEqual(response.status_code, 404)
+
+    # TODO: https://git.startinblox.com/djangoldp-packages/djangoldp-community/issues/3
+    '''
+    def test_add_community_member_is_admin_no_parent(self):
+        self.setUpLoggedInUser(is_superuser=False)
+        another_user = self._get_random_user()
+
+        community = self._get_random_community()
+        me = self._get_community_member(user=self.user, community=community, is_admin=True)
+
+        body = {
+            'http://happy-dev.fr/owl#community': community.urlid,
+            'http://happy-dev.fr/owl#user': another_user.urlid
+        }
+
+        response = self.client.post('/community-members/', body=json.dumps(body), content_type='application/ld+json')
+        self.assertEqual(response.status_code, 201)
+    '''
+
+    def test_delete_community_member_is_admin(self):
+        self.setUpLoggedInUser(is_superuser=False)
+        another_user = self._get_random_user()
+
+        community = self._get_random_community()
+        me = self._get_community_member(user=self.user, community=community, is_admin=True)
+        member = self._get_community_member(user=another_user, community=community, is_admin=False)
+
+        response = self.client.delete('/community-members/{}/'.format(member.pk))
+        self.assertEqual(response.status_code, 204)
+
+    def test_delete_community_member_is_member(self):
+        self.setUpLoggedInUser(is_superuser=False)
+        another_user = self._get_random_user()
+
+        community = self._get_random_community()
+        me = self._get_community_member(user=self.user, community=community, is_admin=False)
+        member = self._get_community_member(user=another_user, community=community, is_admin=False)
+
+        response = self.client.delete('/community-members/{}/'.format(member.pk))
+        self.assertEqual(response.status_code, 403)
+
+    def test_delete_community_member_is_auth_super_user(self):
+        self.setUpLoggedInUser(is_superuser=True)
+        another_user = self._get_random_user()
+
+        community = self._get_random_community()
+        me = self._get_community_member(user=self.user, community=community, is_admin=False)
+        member = self._get_community_member(user=another_user, community=community, is_admin=False)
+
+        response = self.client.delete('/community-members/{}/'.format(member.pk))
+        self.assertEqual(response.status_code, 403)
+
+    # community admins cannot remove other admins
+    def test_delete_community_admin_is_admin(self):
+        self.setUpLoggedInUser(is_superuser=False)
+        another_user = self._get_random_user()
+
+        community = self._get_random_community()
+        me = self._get_community_member(user=self.user, community=community, is_admin=True)
+        member = self._get_community_member(user=another_user, community=community, is_admin=True)
+
+        response = self.client.delete('/community-members/{}/'.format(member.pk))
+        self.assertEqual(response.status_code, 403)
+
+    # community admins can remove themselves
+    def test_delete_self_is_admin(self):
+        self.setUpLoggedInUser(is_superuser=False)
+        another_user = self._get_random_user()
+
+        community = self._get_random_community()
+        me = self._get_community_member(user=self.user, community=community, is_admin=True)
+        member = self._get_community_member(user=another_user, community=community, is_admin=True)
+
+        response = self.client.delete('/community-members/{}/'.format(me.pk))
+        self.assertEqual(response.status_code, 204)
+
+    # community admins cannot remove themselves if they are the last admin
+    def test_delete_self_is_last_admin(self):
+        self.setUpLoggedInUser(is_superuser=False)
+        another_user = self._get_random_user()
+
+        community = self._get_random_community()
+        me = self._get_community_member(user=self.user, community=community, is_admin=True)
+        member = self._get_community_member(user=another_user, community=community, is_admin=False)
+
+        response = self.client.delete('/community-members/{}/'.format(me.pk))
+        self.assertEqual(response.status_code, 403)
+
+    # regular users can remove themselves
+    def test_delete_self(self):
+        self.setUpLoggedInUser(is_superuser=True)
+        another_user = self._get_random_user()
+
+        community = self._get_random_community()
+        me = self._get_community_member(user=self.user, community=community, is_admin=False)
+        member = self._get_community_member(user=another_user, community=community, is_admin=True)
+
+        response = self.client.delete('/community-members/{}/'.format(me.pk))
+        self.assertEqual(response.status_code, 204)
diff --git a/djangoldp_community/views.py b/djangoldp_community/views.py
index 7cde7fa7b10dedfa207ef43ec73806a4471dd237..6e324579fe964b1188c2f0e1da4b296b465e51ad 100644
--- a/djangoldp_community/views.py
+++ b/djangoldp_community/views.py
@@ -1,12 +1,38 @@
+from django.http import Http404
 from djangoldp.views import LDPViewSet
+from djangoldp.utils import is_authenticated_user
+
+
+class CommunityMembersViewset(LDPViewSet):
+
+    def get_parent(self):
+        raise NotImplementedError("get_parent not implemented in CommunityMembersViewSet")
+
+    def is_safe_create(self, user, validated_data, *args, **kwargs):
+        from djangoldp_community.models import Community
+
+        try:
+            if 'community' in validated_data.keys():
+                community = Community.objects.get(urlid=validated_data['community']['urlid'])
+            else:
+                community = self.get_parent()
+
+            if community.allow_self_registration or \
+                    (is_authenticated_user(user) and community.members.filter(user=user, is_admin=True).exists()):
+                return True
+        except Community.DoesNotExist:
+            return True
+        except (KeyError, AttributeError):
+            raise Http404('community not specified with urlid')
+
+        return False
+
 
 class OpenCommunitiesViewset(LDPViewSet):
   def get_queryset(self):
     queryset = super().get_queryset().exclude(allow_self_registration=False)
     # invalidate cache for every open communities, unless that if /open-communities/ is loaded before /communities/xyz/, the last one will get wrong permission nodes
-    from djangoldp.permissions import LDPPermissions
     from djangoldp.serializers import LDListMixin, LDPSerializer
-    LDPPermissions.invalidate_cache()
     LDListMixin.to_representation_cache.reset()
     for result in queryset:
       if(result.urlid):
diff --git a/setup.cfg b/setup.cfg
index a631829ccc5b58fe0c3c39838b9eea36190b3f43..8729cad5e88331f3e5b9df3e158acbcb89ba0e45 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -10,11 +10,12 @@ license = MIT
 [options]
 packages = find:
 install_requires =
-    djangoldp~=2.0
+    djangoldp~=2.1
     djangoldp_account~=2.0
-    djangoldp_circle~=2.0
-    djangoldp_project~=2.0
+    djangoldp_circle~=2.1
+    djangoldp_project~=2.1
     djangoldp_joboffer~=2.0
+    djangoldp_conversation~=2.0
 
 [semantic_release]
 version_source = tag