diff --git a/.gitignore b/.gitignore
index a391fafbf..e1ee2d011 100644
--- a/.gitignore
+++ b/.gitignore
@@ -127,3 +127,5 @@ junit.xml
 !plugin-boilerplate/{{ cookiecutter.project_slug }}/.env
 
 field-diagrams/
+
+*.http
\ No newline at end of file
diff --git a/backend/src/baserow/api/groups/invitations/views.py b/backend/src/baserow/api/groups/invitations/views.py
index 01e1e57ae..90143b43d 100644
--- a/backend/src/baserow/api/groups/invitations/views.py
+++ b/backend/src/baserow/api/groups/invitations/views.py
@@ -324,7 +324,10 @@ class AcceptGroupInvitationView(APIView):
         group_user = CoreHandler().accept_group_invitation(
             request.user, group_invitation
         )
-        return Response(GroupUserGroupSerializer(group_user).data)
+        groupuser_group = (
+            CoreHandler().get_groupuser_group_queryset().get(id=group_user.id)
+        )
+        return Response(GroupUserGroupSerializer(groupuser_group).data)
 
 
 class RejectGroupInvitationView(APIView):
diff --git a/backend/src/baserow/api/groups/schemas.py b/backend/src/baserow/api/groups/schemas.py
deleted file mode 100644
index 05cd40481..000000000
--- a/backend/src/baserow/api/groups/schemas.py
+++ /dev/null
@@ -1,21 +0,0 @@
-from drf_spectacular.plumbing import build_object_type
-
-group_user_schema = build_object_type(
-    {
-        "order": {
-            "type": "integer",
-            "description": "The order of the group, lowest first.",
-            "example": 0,
-        },
-        "id": {
-            "type": "integer",
-            "description": "The unique identifier of the group.",
-            "example": 1,
-        },
-        "name": {
-            "type": "string",
-            "description": "The name given to the group.",
-            "example": "Bram's group",
-        },
-    }
-)
diff --git a/backend/src/baserow/api/groups/serializers.py b/backend/src/baserow/api/groups/serializers.py
index 603ea584a..1ca2c03b9 100644
--- a/backend/src/baserow/api/groups/serializers.py
+++ b/backend/src/baserow/api/groups/serializers.py
@@ -2,9 +2,14 @@ from rest_framework import serializers
 
 from baserow.core.models import Group
 
-from .users.serializers import GroupUserGroupSerializer
+from .users.serializers import GroupUserGroupSerializer, GroupUserSerializer
 
-__all__ = ["GroupUserGroupSerializer", "GroupSerializer", "OrderGroupsSerializer"]
+__all__ = [
+    "GroupUserGroupSerializer",
+    "GroupSerializer",
+    "OrderGroupsSerializer",
+    "GroupUserSerializer",
+]
 
 
 class GroupSerializer(serializers.ModelSerializer):
diff --git a/backend/src/baserow/api/groups/users/serializers.py b/backend/src/baserow/api/groups/users/serializers.py
index 154365b90..cb3b9e304 100644
--- a/backend/src/baserow/api/groups/users/serializers.py
+++ b/backend/src/baserow/api/groups/users/serializers.py
@@ -39,22 +39,36 @@ class GroupUserSerializer(serializers.ModelSerializer):
         return object.user.email
 
 
-class GroupUserGroupSerializer(serializers.ModelSerializer):
+class GroupUserGroupSerializer(serializers.Serializer):
     """
-    This serializers returns all the fields that the GroupSerializer has, but also
-    some user specific values related to the group user relation.
+    This serializers includes relevant fields of the Group model, but also
+    some GroupUser specific fields related to the group user relation.
+
+    Additionally, the list of users are included for each group.
     """
 
-    class Meta:
-        model = GroupUser
-        fields = ("order", "permissions")
+    # Group fields
+    id = serializers.IntegerField(
+        source="group.id", read_only=True, help_text="Group id."
+    )
+    name = serializers.CharField(
+        source="group.name", read_only=True, help_text="Group name."
+    )
+    users = GroupUserSerializer(
+        many=True,
+        source="group.groupuser_set",
+        required=False,
+        read_only=True,
+        help_text="List of all group users.",
+    )
 
-    def to_representation(self, instance):
-        from baserow.api.groups.serializers import GroupSerializer
-
-        data = super().to_representation(instance)
-        data.update(GroupSerializer(instance.group).data)
-        return data
+    # GroupUser fields
+    order = serializers.IntegerField(
+        read_only=True, help_text="The requesting user's order within the group users."
+    )
+    permissions = serializers.CharField(
+        read_only=True, help_text="The requesting user's permissions for the group."
+    )
 
 
 class UpdateGroupUserSerializer(serializers.ModelSerializer):
diff --git a/backend/src/baserow/api/groups/users/views.py b/backend/src/baserow/api/groups/users/views.py
index 4ffa42a0e..b495ebc9a 100644
--- a/backend/src/baserow/api/groups/users/views.py
+++ b/backend/src/baserow/api/groups/users/views.py
@@ -23,11 +23,7 @@ from baserow.core.exceptions import (
 from baserow.core.handler import CoreHandler
 from baserow.core.models import GroupUser
 
-from .serializers import (
-    GroupUserGroupSerializer,
-    GroupUserSerializer,
-    UpdateGroupUserSerializer,
-)
+from .serializers import GroupUserSerializer, UpdateGroupUserSerializer
 
 
 class GroupUsersView(APIView):
@@ -95,7 +91,7 @@ class GroupUserView(APIView):
         ),
         request=UpdateGroupUserSerializer,
         responses={
-            200: GroupUserGroupSerializer,
+            200: GroupUserSerializer,
             400: get_error_schema(
                 [
                     "ERROR_USER_NOT_IN_GROUP",
@@ -123,7 +119,7 @@ class GroupUserView(APIView):
             base_queryset=GroupUser.objects.select_for_update(of=("self",)),
         )
         group_user = CoreHandler().update_group_user(request.user, group_user, **data)
-        return Response(GroupUserGroupSerializer(group_user).data)
+        return Response(GroupUserSerializer(group_user).data)
 
     @extend_schema(
         parameters=[
diff --git a/backend/src/baserow/api/groups/views.py b/backend/src/baserow/api/groups/views.py
index ec59029d6..9750bf91b 100644
--- a/backend/src/baserow/api/groups/views.py
+++ b/backend/src/baserow/api/groups/views.py
@@ -1,7 +1,6 @@
 from django.db import transaction
 
 from drf_spectacular.openapi import OpenApiParameter, OpenApiTypes
-from drf_spectacular.plumbing import build_array_type
 from drf_spectacular.utils import extend_schema
 from rest_framework.permissions import IsAuthenticated
 from rest_framework.response import Response
@@ -34,11 +33,9 @@ from baserow.core.exceptions import (
     UserNotInGroup,
 )
 from baserow.core.handler import CoreHandler
-from baserow.core.models import GroupUser
 from baserow.core.trash.exceptions import CannotDeleteAlreadyDeletedItem
 
 from .errors import ERROR_GROUP_USER_IS_LAST_ADMIN
-from .schemas import group_user_schema
 from .serializers import GroupSerializer, OrderGroupsSerializer
 
 
@@ -56,13 +53,15 @@ class GroupsView(APIView):
             "are custom for each user. The order is configurable via the "
             "**order_groups** endpoint."
         ),
-        responses={200: build_array_type(group_user_schema)},
+        responses={200: GroupUserGroupSerializer(many=True)},
     )
     def get(self, request):
         """Responds with a list of serialized groups where the user is part of."""
 
-        groups = GroupUser.objects.filter(user=request.user).select_related("group")
-        serializer = GroupUserGroupSerializer(groups, many=True)
+        groupuser_groups = (
+            CoreHandler().get_groupuser_group_queryset().filter(user=request.user)
+        )
+        serializer = GroupUserGroupSerializer(groupuser_groups, many=True)
         return Response(serializer.data)
 
     @extend_schema(
@@ -75,7 +74,7 @@ class GroupsView(APIView):
             "created via other endpoints."
         ),
         request=GroupSerializer,
-        responses={200: group_user_schema},
+        responses={200: GroupUserGroupSerializer},
     )
     @transaction.atomic
     @validate_body(GroupSerializer)
@@ -160,7 +159,7 @@ class GroupView(APIView):
         ),
         request=GroupSerializer,
         responses={
-            200: group_user_schema,
+            204: None,
             400: get_error_schema(
                 [
                     "ERROR_USER_NOT_IN_GROUP",
diff --git a/backend/src/baserow/api/user/serializers.py b/backend/src/baserow/api/user/serializers.py
index 2f2f8ab1c..115d8bb01 100644
--- a/backend/src/baserow/api/user/serializers.py
+++ b/backend/src/baserow/api/user/serializers.py
@@ -48,6 +48,20 @@ class UserSerializer(serializers.ModelSerializer):
         }
 
 
+class PublicUserSerializer(serializers.ModelSerializer):
+    """
+    Serializer that exposes only fields that can be shared
+    about the user for the whole group.
+    """
+
+    class Meta:
+        model = User
+        fields = ("id", "username", "first_name")
+        extra_kwargs = {
+            "id": {"read_only": True},
+        }
+
+
 class RegisterSerializer(serializers.Serializer):
     name = serializers.CharField(min_length=2, max_length=150)
     email = serializers.EmailField(
diff --git a/backend/src/baserow/core/handler.py b/backend/src/baserow/core/handler.py
index 59431e22f..b9b176c8e 100644
--- a/backend/src/baserow/core/handler.py
+++ b/backend/src/baserow/core/handler.py
@@ -12,7 +12,7 @@ from django.contrib.auth import get_user_model
 from django.contrib.auth.models import AbstractUser
 from django.core.files.storage import default_storage
 from django.db import transaction
-from django.db.models import Count, Q, QuerySet
+from django.db.models import Count, Prefetch, Q, QuerySet
 from django.utils import translation
 
 from itsdangerous import URLSafeSerializer
@@ -153,6 +153,22 @@ class CoreHandler:
 
         return group
 
+    def get_groupuser_group_queryset(self) -> QuerySet[GroupUser]:
+        """
+        Returns GroupUser queryset that will prefetch groups and their users.
+        """
+
+        groupusers_with_user_and_profile = GroupUser.objects.select_related(
+            "user"
+        ).select_related("user__profile")
+        groupuser_groups = GroupUser.objects.select_related("group").prefetch_related(
+            Prefetch(
+                "group__groupuser_set",
+                queryset=groupusers_with_user_and_profile,
+            )
+        )
+        return groupuser_groups
+
     def create_group(self, user: User, name: str) -> GroupUser:
         """
         Creates a new group for an existing user.
@@ -243,7 +259,10 @@ class CoreHandler:
         group_user_id = group_user.id
         group_user.delete()
         group_user_deleted.send(
-            self, group_user_id=group_user_id, group_user=group_user, user=user
+            self,
+            group_user_id=group_user_id,
+            group_user=group_user,
+            user=user,
         )
 
     def delete_group_by_id(self, user: AbstractUser, group_id: int):
@@ -394,7 +413,10 @@ class CoreHandler:
         group_user.delete()
 
         group_user_deleted.send(
-            self, group_user_id=group_user_id, group_user=group_user, user=user
+            self,
+            group_user_id=group_user_id,
+            group_user=group_user,
+            user=user,
         )
 
     def get_group_invitation_signer(self):
diff --git a/backend/src/baserow/core/signals.py b/backend/src/baserow/core/signals.py
index 86d976c79..ecf394bd3 100644
--- a/backend/src/baserow/core/signals.py
+++ b/backend/src/baserow/core/signals.py
@@ -6,6 +6,11 @@ before_user_deleted = Signal()
 
 before_group_deleted = Signal()
 
+user_updated = Signal()
+user_deleted = Signal()
+user_restored = Signal()
+user_permanently_deleted = Signal()
+
 group_created = Signal()
 group_updated = Signal()
 group_deleted = Signal()
diff --git a/backend/src/baserow/core/user/handler.py b/backend/src/baserow/core/user/handler.py
index ec8a51339..bae7fc24b 100644
--- a/backend/src/baserow/core/user/handler.py
+++ b/backend/src/baserow/core/user/handler.py
@@ -19,9 +19,15 @@ from baserow.core.exceptions import (
     GroupInvitationEmailMismatch,
 )
 from baserow.core.handler import CoreHandler
-from baserow.core.models import Group, Template, UserLogEntry, UserProfile
+from baserow.core.models import Group, GroupUser, Template, UserLogEntry, UserProfile
 from baserow.core.registries import plugin_registry
-from baserow.core.signals import before_user_deleted
+from baserow.core.signals import (
+    before_user_deleted,
+    user_deleted,
+    user_permanently_deleted,
+    user_restored,
+    user_updated,
+)
 from baserow.core.trash.handler import TrashHandler
 
 from .emails import (
@@ -189,7 +195,8 @@ class UserHandler:
         language: Optional[str] = None,
     ) -> AbstractUser:
         """
-        Updates the user's account editable properties
+        Updates the user's account editable properties. Handles the scenario
+        when a user edits his own account.
 
         :param user: The user instance to update.
         :param first_name: The new user first name.
@@ -205,6 +212,8 @@ class UserHandler:
             user.profile.language = language
             user.profile.save()
 
+        user_updated.send(self, performed_by=user, user=user)
+
         return user
 
     def get_reset_password_signer(self) -> URLSafeTimedSerializer:
@@ -374,6 +383,8 @@ class UserHandler:
             email = AccountDeletionScheduled(user, days_left, to=[user.email])
             email.send()
 
+        user_deleted.send(self, performed_by=user, user=user)
+
     def cancel_user_deletion(self, user: AbstractUser):
         """
         Cancels a previously scheduled user account deletion. This action send an email
@@ -389,6 +400,8 @@ class UserHandler:
             email = AccountDeletionCanceled(user, to=[user.email])
             email.send()
 
+        user_restored.send(self, performed_by=user, user=user)
+
     def delete_expired_users(self, grace_delay: Optional[timedelta] = None):
         """
         Executes all previously scheduled user account deletions for which
@@ -416,9 +429,14 @@ class UserHandler:
             profile__to_be_deleted=True, last_login__lt=limit_date
         )
 
-        deleted_user_info = [
-            (u.username, u.email, u.profile.language) for u in users_to_delete.all()
-        ]
+        group_users = GroupUser.objects.filter(user__in=users_to_delete)
+
+        deleted_user_info = []
+        for u in users_to_delete.all():
+            group_ids = [gu.group_id for gu in group_users if gu.user_id == u.id]
+            deleted_user_info.append(
+                (u.id, u.username, u.email, u.profile.language, group_ids)
+            )
 
         # A group need to be deleted if there was an admin before and there is no
         # *active* admin after the users deletion.
@@ -447,7 +465,8 @@ class UserHandler:
                 TrashHandler.permanently_delete(group)
             users_to_delete.delete()
 
-        for (username, email, language) in deleted_user_info:
+        for (id, username, email, language, group_ids) in deleted_user_info:
             with translation.override(language):
                 email = AccountDeleted(username, to=[email])
                 email.send()
+            user_permanently_deleted.send(self, user_id=id, group_ids=group_ids)
diff --git a/backend/src/baserow/ws/signals.py b/backend/src/baserow/ws/signals.py
index 7f1331285..edf0e5390 100644
--- a/backend/src/baserow/ws/signals.py
+++ b/backend/src/baserow/ws/signals.py
@@ -2,10 +2,68 @@ from django.db import transaction
 from django.dispatch import receiver
 
 from baserow.api.applications.serializers import get_application_serializer
-from baserow.api.groups.serializers import GroupSerializer, GroupUserGroupSerializer
+from baserow.api.groups.serializers import (
+    GroupSerializer,
+    GroupUserGroupSerializer,
+    GroupUserSerializer,
+)
+from baserow.api.user.serializers import PublicUserSerializer
 from baserow.core import signals
+from baserow.core.handler import CoreHandler
+from baserow.core.models import GroupUser
 
-from .tasks import broadcast_to_group, broadcast_to_users
+from .tasks import broadcast_to_group, broadcast_to_groups, broadcast_to_users
+
+
+@receiver(signals.user_updated)
+def user_updated(sender, performed_by, user, **kwargs):
+    group_ids = list(
+        GroupUser.objects.filter(user=user).values_list("group_id", flat=True)
+    )
+
+    transaction.on_commit(
+        lambda: broadcast_to_groups.delay(
+            group_ids,
+            {"type": "user_updated", "user": PublicUserSerializer(user).data},
+            getattr(performed_by, "web_socket_id", None),
+        )
+    )
+
+
+@receiver(signals.user_deleted)
+def user_deleted(sender, performed_by, user, **kwargs):
+    group_ids = list(
+        GroupUser.objects.filter(user=user).values_list("group_id", flat=True)
+    )
+
+    transaction.on_commit(
+        lambda: broadcast_to_groups.delay(
+            group_ids, {"type": "user_deleted", "user": PublicUserSerializer(user).data}
+        )
+    )
+
+
+@receiver(signals.user_restored)
+def user_restored(sender, performed_by, user, **kwargs):
+    group_ids = list(
+        GroupUser.objects.filter(user=user).values_list("group_id", flat=True)
+    )
+
+    transaction.on_commit(
+        lambda: broadcast_to_groups.delay(
+            group_ids,
+            {"type": "user_restored", "user": PublicUserSerializer(user).data},
+        )
+    )
+
+
+@receiver(signals.user_permanently_deleted)
+def user_permanently_deleted(sender, user_id, group_ids, **kwargs):
+    transaction.on_commit(
+        lambda: broadcast_to_groups.delay(
+            group_ids, {"type": "user_permanently_deleted", "user_id": user_id}
+        )
+    )
 
 
 @receiver(signals.group_created)
@@ -45,36 +103,32 @@ def group_deleted(sender, group_id, group, group_users, user=None, **kwargs):
     )
 
 
-@receiver(signals.group_user_updated)
-def group_user_updated(sender, group_user, user, **kwargs):
+@receiver(signals.group_user_added)
+def group_user_added(sender, group_user, user, **kwargs):
     transaction.on_commit(
-        lambda: broadcast_to_users.delay(
-            [group_user.user_id],
+        lambda: broadcast_to_group.delay(
+            group_user.group_id,
             {
-                "type": "group_updated",
+                "type": "group_user_added",
+                "id": group_user.id,
                 "group_id": group_user.group_id,
-                "group": GroupUserGroupSerializer(group_user).data,
+                "group_user": GroupUserSerializer(group_user).data,
             },
             getattr(user, "web_socket_id", None),
         )
     )
 
 
-@receiver(signals.group_restored)
-def group_restored(sender, group_user, user, **kwargs):
+@receiver(signals.group_user_updated)
+def group_user_updated(sender, group_user, user, **kwargs):
     transaction.on_commit(
-        lambda: broadcast_to_users.delay(
-            [group_user.user_id],
+        lambda: broadcast_to_group.delay(
+            group_user.group_id,
             {
-                "type": "group_restored",
+                "type": "group_user_updated",
+                "id": group_user.id,
                 "group_id": group_user.group_id,
-                "group": GroupUserGroupSerializer(group_user).data,
-                "applications": [
-                    get_application_serializer(application).data
-                    for application in group_user.group.application_set.select_related(
-                        "content_type", "group"
-                    ).all()
-                ],
+                "group_user": GroupUserSerializer(group_user).data,
             },
             getattr(user, "web_socket_id", None),
         )
@@ -82,11 +136,47 @@ def group_restored(sender, group_user, user, **kwargs):
 
 
 @receiver(signals.group_user_deleted)
-def group_user_deleted(sender, group_user, user, **kwargs):
+def group_user_deleted(sender, group_user_id, group_user, user, **kwargs):
+    def broadcast_to_group_and_removed_user():
+        payload = {
+            "type": "group_user_deleted",
+            "id": group_user_id,
+            "group_id": group_user.group_id,
+            "group_user": GroupUserSerializer(group_user).data,
+        }
+        broadcast_to_group.delay(
+            group_user.group_id,
+            payload,
+            getattr(user, "web_socket_id", None),
+        )
+        broadcast_to_users.delay(
+            [group_user.user_id],
+            payload,
+            getattr(user, "web_socket_id", None),
+        )
+
+    transaction.on_commit(broadcast_to_group_and_removed_user)
+
+
+@receiver(signals.group_restored)
+def group_restored(sender, group_user, user, **kwargs):
+    groupuser_groups = (
+        CoreHandler().get_groupuser_group_queryset().get(id=group_user.id)
+    )
     transaction.on_commit(
         lambda: broadcast_to_users.delay(
             [group_user.user_id],
-            {"type": "group_deleted", "group_id": group_user.group_id},
+            {
+                "type": "group_restored",
+                "group_id": group_user.group_id,
+                "group": GroupUserGroupSerializer(groupuser_groups).data,
+                "applications": [
+                    get_application_serializer(application).data
+                    for application in group_user.group.application_set.select_related(
+                        "content_type", "group"
+                    ).all()
+                ],
+            },
             getattr(user, "web_socket_id", None),
         )
     )
diff --git a/backend/src/baserow/ws/tasks.py b/backend/src/baserow/ws/tasks.py
index 47d0479be..95eeebd74 100644
--- a/backend/src/baserow/ws/tasks.py
+++ b/backend/src/baserow/ws/tasks.py
@@ -1,3 +1,5 @@
+from typing import Iterable
+
 from baserow.config.celery import app
 
 
@@ -92,3 +94,33 @@ def broadcast_to_group(self, group_id, payload, ignore_web_socket_id=None):
         return
 
     broadcast_to_users(user_ids, payload, ignore_web_socket_id)
+
+
+@app.task(bind=True)
+def broadcast_to_groups(
+    self, group_ids: Iterable[int], payload: dict, ignore_web_socket_id: str = None
+):
+    """
+    Broadcasts a JSON payload to all users that are in the provided groups.
+
+    :param group_ids: Ids of groups to broadcast to.
+    :param payload: A dictionary object containing the payload that must be
+        broadcasted.
+    :param ignore_web_socket_id: The web socket id to which the message must not be
+        sent. This is normally the web socket id that has originally made the change
+        request.
+    """
+
+    from baserow.core.models import GroupUser
+
+    user_ids = list(
+        GroupUser.objects.filter(group_id__in=group_ids)
+        .distinct("user_id")
+        .order_by("user_id")
+        .values_list("user_id", flat=True)
+    )
+
+    if len(user_ids) == 0:
+        return
+
+    broadcast_to_users(user_ids, payload, ignore_web_socket_id)
diff --git a/backend/tests/baserow/api/groups/test_group_views.py b/backend/tests/baserow/api/groups/test_group_views.py
index c599dbf1e..0d87d66f7 100644
--- a/backend/tests/baserow/api/groups/test_group_views.py
+++ b/backend/tests/baserow/api/groups/test_group_views.py
@@ -5,6 +5,7 @@ from rest_framework.status import HTTP_200_OK, HTTP_400_BAD_REQUEST, HTTP_404_NO
 
 from baserow.core.handler import CoreHandler
 from baserow.core.models import Group, GroupUser
+from baserow.test_utils.helpers import is_dict_subset
 
 
 @pytest.mark.django_db
@@ -12,8 +13,12 @@ def test_list_groups(api_client, data_fixture):
     user, token = data_fixture.create_user_and_token(
         email="test@test.nl", password="password", first_name="Test1"
     )
-    user_group_2 = data_fixture.create_user_group(user=user, order=2)
-    user_group_1 = data_fixture.create_user_group(user=user, order=1)
+    user_group_2 = data_fixture.create_user_group(
+        user=user, order=2, permissions="ADMIN"
+    )
+    user_group_1 = data_fixture.create_user_group(
+        user=user, order=1, permissions="MEMBER"
+    )
     data_fixture.create_group()
 
     response = api_client.get(
@@ -24,8 +29,107 @@ def test_list_groups(api_client, data_fixture):
     assert len(response_json) == 2
     assert response_json[0]["id"] == user_group_1.group.id
     assert response_json[0]["order"] == 1
+    assert response_json[0]["name"] == user_group_1.group.name
+    assert response_json[0]["permissions"] == "MEMBER"
     assert response_json[1]["id"] == user_group_2.group.id
     assert response_json[1]["order"] == 2
+    assert response_json[1]["name"] == user_group_2.group.name
+    assert response_json[1]["permissions"] == "ADMIN"
+
+
+@pytest.mark.django_db
+def test_list_groups_with_users(api_client, data_fixture):
+    user, token = data_fixture.create_user_and_token(
+        email="test@test.nl", password="password", first_name="Test1"
+    )
+    user_group_1 = data_fixture.create_user_group(
+        user=user, order=1, permissions="MEMBER"
+    )
+    user_group_1_2 = data_fixture.create_user_group(
+        order=1, permissions="ADMIN", group=user_group_1.group
+    )
+    user_group_1_3 = data_fixture.create_user_group(
+        order=1, permissions="MEMBER", group=user_group_1.group
+    )
+
+    user_group_2 = data_fixture.create_user_group(
+        user=user, order=2, permissions="ADMIN"
+    )
+    user_group_2_2 = data_fixture.create_user_group(
+        order=2, permissions="MEMBER", group=user_group_2.group
+    )
+
+    response = api_client.get(
+        reverse("api:groups:list"), **{"HTTP_AUTHORIZATION": f"JWT {token}"}
+    )
+    assert response.status_code == HTTP_200_OK
+    response_json = response.json()
+
+    expected_result = [
+        {
+            "id": user_group_1.group.id,
+            "name": user_group_1.group.name,
+            "order": 1,
+            "permissions": "MEMBER",
+            "users": [
+                {
+                    "email": user_group_1.user.email,
+                    "group": user_group_1.group.id,
+                    "id": user_group_1.id,
+                    "name": user_group_1.user.first_name,
+                    "permissions": "MEMBER",
+                    "to_be_deleted": False,
+                    "user_id": user_group_1.user.id,
+                },
+                {
+                    "email": user_group_1_2.user.email,
+                    "group": user_group_1_2.group.id,
+                    "id": user_group_1_2.id,
+                    "name": user_group_1_2.user.first_name,
+                    "permissions": "ADMIN",
+                    "to_be_deleted": False,
+                    "user_id": user_group_1_2.user.id,
+                },
+                {
+                    "email": user_group_1_3.user.email,
+                    "group": user_group_1_3.group.id,
+                    "id": user_group_1_3.id,
+                    "name": user_group_1_3.user.first_name,
+                    "permissions": "MEMBER",
+                    "to_be_deleted": False,
+                    "user_id": user_group_1_3.user.id,
+                },
+            ],
+        },
+        {
+            "id": user_group_2.group.id,
+            "name": user_group_2.group.name,
+            "order": 2,
+            "permissions": "ADMIN",
+            "users": [
+                {
+                    "email": user_group_2.user.email,
+                    "group": user_group_2.group.id,
+                    "id": user_group_2.id,
+                    "name": user_group_2.user.first_name,
+                    "permissions": "ADMIN",
+                    "to_be_deleted": False,
+                    "user_id": user_group_2.user.id,
+                },
+                {
+                    "email": user_group_2_2.user.email,
+                    "group": user_group_2_2.group.id,
+                    "id": user_group_2_2.id,
+                    "name": user_group_2_2.user.first_name,
+                    "permissions": "MEMBER",
+                    "to_be_deleted": False,
+                    "user_id": user_group_2_2.user.id,
+                },
+            ],
+        },
+    ]
+
+    assert is_dict_subset(expected_result, response_json)
 
 
 @pytest.mark.django_db
diff --git a/backend/tests/baserow/ws/test_ws_signals.py b/backend/tests/baserow/ws/test_ws_signals.py
index 4a5ae6120..78c9c2f0d 100644
--- a/backend/tests/baserow/ws/test_ws_signals.py
+++ b/backend/tests/baserow/ws/test_ws_signals.py
@@ -1,6 +1,8 @@
+from datetime import timedelta
 from unittest.mock import patch
 
 from django.db import transaction
+from django.utils import timezone
 
 import pytest
 
@@ -10,6 +12,78 @@ from baserow.core.models import (
     GROUP_USER_PERMISSION_MEMBER,
 )
 from baserow.core.trash.handler import TrashHandler
+from baserow.core.user.handler import UserHandler
+
+
+@pytest.mark.django_db(transaction=True)
+@patch("baserow.ws.signals.broadcast_to_groups")
+def test_user_updated_name(mock_broadcast_to_groups, data_fixture):
+    user = data_fixture.create_user(first_name="Albert")
+    group_user = CoreHandler().create_group(user=user, name="Test")
+    group_user_2 = CoreHandler().create_group(user=user, name="Test 2")
+
+    UserHandler().update_user(user, first_name="Jack")
+
+    mock_broadcast_to_groups.delay.assert_called_once()
+    args = mock_broadcast_to_groups.delay.call_args
+    assert args[0][0] == [group_user.group.id, group_user_2.group.id]
+    assert args[0][1]["type"] == "user_updated"
+    assert args[0][1]["user"]["id"] == user.id
+    assert args[0][1]["user"]["first_name"] == "Jack"
+
+
+@pytest.mark.django_db(transaction=True)
+@patch("baserow.ws.signals.broadcast_to_groups")
+def test_schedule_user_deletion(mock_broadcast_to_groups, data_fixture):
+    user = data_fixture.create_user(first_name="Albert", password="albert")
+    group_user = CoreHandler().create_group(user=user, name="Test")
+    group_user_2 = CoreHandler().create_group(user=user, name="Test 2")
+
+    UserHandler().schedule_user_deletion(user, password="albert")
+
+    mock_broadcast_to_groups.delay.assert_called_once()
+    args = mock_broadcast_to_groups.delay.call_args
+    assert args[0][0] == [group_user.group.id, group_user_2.group.id]
+    assert args[0][1]["type"] == "user_deleted"
+    assert args[0][1]["user"]["id"] == user.id
+
+
+@pytest.mark.django_db(transaction=True)
+@patch("baserow.ws.signals.broadcast_to_groups")
+def test_cancel_user_deletion(mock_broadcast_to_groups, data_fixture):
+    user = data_fixture.create_user(first_name="Albert", password="albert")
+    user.profile.to_be_deleted = True
+    user.save()
+    group_user = CoreHandler().create_group(user=user, name="Test")
+    group_user_2 = CoreHandler().create_group(user=user, name="Test 2")
+
+    UserHandler().cancel_user_deletion(user)
+
+    mock_broadcast_to_groups.delay.assert_called_once()
+    args = mock_broadcast_to_groups.delay.call_args
+    assert args[0][0] == [group_user.group.id, group_user_2.group.id]
+    assert args[0][1]["type"] == "user_restored"
+    assert args[0][1]["user"]["id"] == user.id
+
+
+@pytest.mark.django_db(transaction=True)
+@patch("baserow.ws.signals.broadcast_to_groups")
+def test_user_permanently_deleted(mock_broadcast_to_groups, data_fixture):
+    user = data_fixture.create_user(first_name="Albert", password="albert")
+    user.profile.to_be_deleted = True
+    user.profile.save()
+    user.last_login = timezone.now() - timedelta(weeks=100)
+    user.save()
+    group_user = CoreHandler().create_group(user=user, name="Test")
+    group_user_2 = CoreHandler().create_group(user=user, name="Test 2")
+
+    UserHandler().delete_expired_users(grace_delay=timedelta(days=1))
+
+    mock_broadcast_to_groups.delay.assert_called_once()
+    args = mock_broadcast_to_groups.delay.call_args
+    assert args[0][0] == [group_user.group.id, group_user_2.group.id]
+    assert args[0][1]["type"] == "user_permanently_deleted"
+    assert args[0][1]["user_id"] == user.id
 
 
 @pytest.mark.django_db(transaction=True)
@@ -106,8 +180,31 @@ def test_group_deleted(mock_broadcast_to_users, data_fixture):
 
 
 @pytest.mark.django_db(transaction=True)
-@patch("baserow.ws.signals.broadcast_to_users")
-def test_group_user_updated(mock_broadcast_to_users, data_fixture):
+@patch("baserow.ws.signals.broadcast_to_group")
+def test_group_user_added(mock_broadcast_to_group, data_fixture):
+    user_1 = data_fixture.create_user()
+    user_2 = data_fixture.create_user()
+    group = data_fixture.create_group()
+    group_user_1 = data_fixture.create_user_group(user=user_1, group=group)
+    group_invitation = data_fixture.create_group_invitation(
+        email=user_2.email, permissions="MEMBER", group=group
+    )
+
+    group_user_2 = CoreHandler().accept_group_invitation(user_2, group_invitation)
+
+    mock_broadcast_to_group.delay.assert_called_once()
+    args = mock_broadcast_to_group.delay.call_args
+    assert args[0][0] == group.id
+    assert args[0][1]["type"] == "group_user_added"
+    assert args[0][1]["id"] == group_user_2.id
+    assert args[0][1]["group_id"] == group.id
+    assert args[0][1]["group_user"]["user_id"] == group_user_2.user_id
+    assert args[0][1]["group_user"]["permissions"] == "MEMBER"
+
+
+@pytest.mark.django_db(transaction=True)
+@patch("baserow.ws.signals.broadcast_to_group")
+def test_group_user_updated(mock_broadcast_to_group, data_fixture):
     user_1 = data_fixture.create_user()
     user_2 = data_fixture.create_user()
     group = data_fixture.create_group()
@@ -117,31 +214,76 @@ def test_group_user_updated(mock_broadcast_to_users, data_fixture):
         user=user_2, group_user=group_user_1, permissions="MEMBER"
     )
 
-    mock_broadcast_to_users.delay.assert_called_once()
-    args = mock_broadcast_to_users.delay.call_args
-    assert args[0][0] == [user_1.id]
-    assert args[0][1]["type"] == "group_updated"
-    assert args[0][1]["group"]["id"] == group.id
-    assert args[0][1]["group"]["name"] == group.name
-    assert args[0][1]["group"]["permissions"] == "MEMBER"
+    mock_broadcast_to_group.delay.assert_called_once()
+    args = mock_broadcast_to_group.delay.call_args
+    assert args[0][0] == group.id
+    assert args[0][1]["type"] == "group_user_updated"
+    assert args[0][1]["id"] == group_user_1.id
     assert args[0][1]["group_id"] == group.id
+    assert args[0][1]["group_user"]["user_id"] == group_user_1.user_id
+    assert args[0][1]["group_user"]["permissions"] == "MEMBER"
 
 
 @pytest.mark.django_db(transaction=True)
+@patch("baserow.ws.signals.broadcast_to_group")
 @patch("baserow.ws.signals.broadcast_to_users")
-def test_group_user_deleted(mock_broadcast_to_users, data_fixture):
+def test_group_user_deleted(
+    mock_broadcast_to_users, mock_broadcast_to_group, data_fixture
+):
     user_1 = data_fixture.create_user()
     user_2 = data_fixture.create_user()
     group = data_fixture.create_group()
     group_user_1 = data_fixture.create_user_group(user=user_1, group=group)
+    group_user_id = group_user_1.id
     data_fixture.create_user_group(user=user_2, group=group)
     CoreHandler().delete_group_user(user=user_2, group_user=group_user_1)
 
     mock_broadcast_to_users.delay.assert_called_once()
     args = mock_broadcast_to_users.delay.call_args
     assert args[0][0] == [user_1.id]
-    assert args[0][1]["type"] == "group_deleted"
+    assert args[0][1]["type"] == "group_user_deleted"
+    assert args[0][1]["id"] == group_user_id
     assert args[0][1]["group_id"] == group.id
+    assert args[0][1]["group_user"]["user_id"] == group_user_1.user_id
+
+    mock_broadcast_to_group.delay.assert_called_once()
+    args = mock_broadcast_to_group.delay.call_args
+    assert args[0][0] == group.id
+    assert args[0][1]["type"] == "group_user_deleted"
+    assert args[0][1]["id"] == group_user_id
+    assert args[0][1]["group_id"] == group.id
+    assert args[0][1]["group_user"]["user_id"] == group_user_1.user_id
+
+
+@pytest.mark.django_db(transaction=True)
+@patch("baserow.ws.signals.broadcast_to_group")
+@patch("baserow.ws.signals.broadcast_to_users")
+def test_user_leaves_group(
+    mock_broadcast_to_users, mock_broadcast_to_group, data_fixture
+):
+    user_1 = data_fixture.create_user()
+    user_2 = data_fixture.create_user()
+    group = data_fixture.create_group()
+    group_user_1 = data_fixture.create_user_group(user=user_1, group=group)
+    group_user_id = group_user_1.id
+    data_fixture.create_user_group(user=user_2, group=group)
+    CoreHandler().leave_group(user_1, group)
+
+    mock_broadcast_to_users.delay.assert_called_once()
+    args = mock_broadcast_to_users.delay.call_args
+    assert args[0][0] == [user_1.id]
+    assert args[0][1]["type"] == "group_user_deleted"
+    assert args[0][1]["id"] == group_user_id
+    assert args[0][1]["group_id"] == group.id
+    assert args[0][1]["group_user"]["user_id"] == group_user_1.user_id
+
+    mock_broadcast_to_group.delay.assert_called_once()
+    args = mock_broadcast_to_group.delay.call_args
+    assert args[0][0] == group.id
+    assert args[0][1]["type"] == "group_user_deleted"
+    assert args[0][1]["id"] == group_user_id
+    assert args[0][1]["group_id"] == group.id
+    assert args[0][1]["group_user"]["user_id"] == group_user_1.user_id
 
 
 @pytest.mark.django_db(transaction=True)
diff --git a/backend/tests/baserow/ws/test_ws_tasks.py b/backend/tests/baserow/ws/test_ws_tasks.py
index 18faa9711..8d324332b 100644
--- a/backend/tests/baserow/ws/test_ws_tasks.py
+++ b/backend/tests/baserow/ws/test_ws_tasks.py
@@ -7,6 +7,7 @@ from baserow.config.asgi import application
 from baserow.ws.tasks import (
     broadcast_to_channel_group,
     broadcast_to_group,
+    broadcast_to_groups,
     broadcast_to_users,
 )
 
@@ -234,3 +235,89 @@ async def test_broadcast_to_group(data_fixture):
     await communicator_1.disconnect()
     await communicator_2.disconnect()
     await communicator_3.disconnect()
+
+
+@pytest.mark.run(order=6)
+@pytest.mark.asyncio
+@pytest.mark.django_db(transaction=True)
+async def test_broadcast_to_groups(data_fixture):
+    user_1, token_1 = data_fixture.create_user_and_token()
+    user_2, token_2 = data_fixture.create_user_and_token()
+    user_3, token_3 = data_fixture.create_user_and_token()
+    user_4, token_4 = data_fixture.create_user_and_token()
+    group_1 = data_fixture.create_group(users=[user_1, user_2, user_4])
+    group_2 = data_fixture.create_group(users=[user_2, user_3])
+
+    communicator_1 = WebsocketCommunicator(
+        application,
+        f"ws/core/?jwt_token={token_1}",
+        headers=[(b"origin", b"http://localhost")],
+    )
+    await communicator_1.connect()
+    response_1 = await communicator_1.receive_json_from()
+    web_socket_id_1 = response_1["web_socket_id"]
+
+    communicator_2 = WebsocketCommunicator(
+        application,
+        f"ws/core/?jwt_token={token_2}",
+        headers=[(b"origin", b"http://localhost")],
+    )
+    await communicator_2.connect()
+    response_2 = await communicator_2.receive_json_from()
+    web_socket_id_2 = response_2["web_socket_id"]
+
+    communicator_3 = WebsocketCommunicator(
+        application,
+        f"ws/core/?jwt_token={token_3}",
+        headers=[(b"origin", b"http://localhost")],
+    )
+    await communicator_3.connect()
+    await communicator_3.receive_json_from()
+
+    await database_sync_to_async(broadcast_to_groups)([group_1.id], {"message": "test"})
+    response_1 = await communicator_1.receive_json_from(0.1)
+    response_2 = await communicator_2.receive_json_from(0.1)
+    await communicator_3.receive_nothing(0.1)
+
+    assert response_1["message"] == "test"
+    assert response_2["message"] == "test"
+
+    await database_sync_to_async(broadcast_to_groups)(
+        [group_1.id], {"message": "test2"}, ignore_web_socket_id=web_socket_id_1
+    )
+
+    await communicator_1.receive_nothing(0.1)
+    response_2 = await communicator_2.receive_json_from(0.1)
+    await communicator_3.receive_nothing(0.1)
+
+    assert response_2["message"] == "test2"
+
+    communicator_4 = WebsocketCommunicator(
+        application,
+        f"ws/core/?jwt_token={token_4}",
+        headers=[(b"origin", b"http://localhost")],
+    )
+    await communicator_4.connect()
+    response_4 = await communicator_4.receive_json_from()
+    web_socket_id_4 = response_4["web_socket_id"]
+
+    await database_sync_to_async(broadcast_to_groups)(
+        [group_1.id, group_2.id],
+        {"message": "test3"},
+        ignore_web_socket_id=web_socket_id_4,
+    )
+
+    await communicator_1.receive_json_from(0.1)
+    await communicator_2.receive_json_from(0.1)
+    await communicator_3.receive_json_from(0.1)
+    await communicator_4.receive_nothing(0.1)
+
+    assert communicator_1.output_queue.qsize() == 0
+    assert communicator_2.output_queue.qsize() == 0
+    assert communicator_3.output_queue.qsize() == 0
+    assert communicator_4.output_queue.qsize() == 0
+
+    await communicator_1.disconnect()
+    await communicator_2.disconnect()
+    await communicator_3.disconnect()
+    await communicator_4.disconnect()
diff --git a/changelog.md b/changelog.md
index 22c8ed6e6..2e2aecda6 100644
--- a/changelog.md
+++ b/changelog.md
@@ -19,6 +19,8 @@ For example:
 * Force browser language when viewing a public view. [#834](https://gitlab.com/bramw/baserow/-/issues/834)
 * Search automatically after 400ms when chosing a related field via the modal. [#1091](https://gitlab.com/bramw/baserow/-/issues/1091)
 * Add cancel button to field update context [#1020](https://gitlab.com/bramw/baserow/-/issues/1020)
+* New signals `user_updated`, `user_deleted`, `user_restored`, `user_permanently_deleted` were added to track user changes.
+* `list_groups` endpoint now also returns the list of all group users for each group.
 
 ### Bug Fixes
 * Resolve circular dependency in `FieldWithFiltersAndSortsSerializer` [#1113](https://gitlab.com/bramw/baserow/-/issues/1113)
diff --git a/docs/apis/web-socket-api.md b/docs/apis/web-socket-api.md
index d91bc5c43..20983bad8 100644
--- a/docs/apis/web-socket-api.md
+++ b/docs/apis/web-socket-api.md
@@ -123,6 +123,10 @@ are subscribed to the page.
 * `page_add`
 * `page_discard`
 * `before_group_deleted`
+* `user_updated`
+* `user_deleted`
+* `user_restored`
+* `user_permanently_deleted`
 * `group_created`
 * `group_updated`
 * `group_deleted`
diff --git a/web-frontend/modules/core/components/group/GroupMembersModal.vue b/web-frontend/modules/core/components/group/GroupMembersModal.vue
index 80131d2aa..3eb22a747 100644
--- a/web-frontend/modules/core/components/group/GroupMembersModal.vue
+++ b/web-frontend/modules/core/components/group/GroupMembersModal.vue
@@ -196,6 +196,11 @@ export default {
 
       try {
         await GroupService(this.$client).updateUser(user.id, { permissions })
+        await this.$store.dispatch('group/forceUpdateGroupUser', {
+          groupId: this.group.id,
+          id: user.id,
+          values: { permissions },
+        })
       } catch (error) {
         user.permissions = oldPermissions
         notifyIf(error, 'group')
@@ -208,7 +213,13 @@ export default {
       try {
         await GroupService(this.$client).deleteUser(user.id)
         const index = this.users.findIndex((u) => u.id === user.id)
+        const userId = this.users[index].user_id
         this.users.splice(index, 1)
+        await this.$store.dispatch('group/forceDeleteGroupUser', {
+          groupId: this.group.id,
+          id: user.id,
+          values: { user_id: userId },
+        })
       } catch (error) {
         user._.loading = false
         notifyIf(error, 'group')
diff --git a/web-frontend/modules/core/plugins/realTimeHandler.js b/web-frontend/modules/core/plugins/realTimeHandler.js
index a3e4ca898..2a454b589 100644
--- a/web-frontend/modules/core/plugins/realTimeHandler.js
+++ b/web-frontend/modules/core/plugins/realTimeHandler.js
@@ -199,6 +199,39 @@ export class RealTimeHandler {
       store.dispatch('auth/forceUpdateUserData', data.user_data)
     })
 
+    this.registerEvent('user_updated', ({ store }, data) => {
+      store.dispatch('group/forceUpdateGroupUserAttributes', {
+        userId: data.user.id,
+        values: {
+          name: data.user.first_name,
+        },
+      })
+    })
+
+    this.registerEvent('user_deleted', ({ store }, data) => {
+      store.dispatch('group/forceUpdateGroupUserAttributes', {
+        userId: data.user.id,
+        values: {
+          to_be_deleted: true,
+        },
+      })
+    })
+
+    this.registerEvent('user_restored', ({ store }, data) => {
+      store.dispatch('group/forceUpdateGroupUserAttributes', {
+        userId: data.user.id,
+        values: {
+          to_be_deleted: false,
+        },
+      })
+    })
+
+    this.registerEvent('user_permanently_deleted', ({ store }, data) => {
+      store.dispatch('group/forceDeleteUser', {
+        userId: data.user_id,
+      })
+    })
+
     this.registerEvent('group_created', ({ store }, data) => {
       store.dispatch('group/forceCreate', data.group)
     })
@@ -226,6 +259,29 @@ export class RealTimeHandler {
       store.dispatch('group/forceOrder', data.group_ids)
     })
 
+    this.registerEvent('group_user_added', ({ store }, data) => {
+      store.dispatch('group/forceAddGroupUser', {
+        groupId: data.group_id,
+        values: data.group_user,
+      })
+    })
+
+    this.registerEvent('group_user_updated', ({ store }, data) => {
+      store.dispatch('group/forceUpdateGroupUser', {
+        id: data.id,
+        groupId: data.group_id,
+        values: data.group_user,
+      })
+    })
+
+    this.registerEvent('group_user_deleted', ({ store }, data) => {
+      store.dispatch('group/forceDeleteGroupUser', {
+        id: data.id,
+        groupId: data.group_id,
+        values: data.group_user,
+      })
+    })
+
     this.registerEvent('application_created', ({ store }, data) => {
       store.dispatch('application/forceCreate', data.application)
     })
diff --git a/web-frontend/modules/core/store/auth.js b/web-frontend/modules/core/store/auth.js
index 0a23c70b7..c57894c8c 100644
--- a/web-frontend/modules/core/store/auth.js
+++ b/web-frontend/modules/core/store/auth.js
@@ -160,9 +160,21 @@ export const actions = {
   /**
    * Updates the account information is the authenticated user.
    */
-  async update({ commit }, values) {
+  async update({ getters, commit, dispatch }, values) {
     const { data } = await AuthService(this.$client).update(values)
     commit('UPDATE_USER_DATA', { user: data })
+    dispatch(
+      'group/forceUpdateGroupUserAttributes',
+      {
+        userId: getters.getUserId,
+        values: {
+          name: data.first_name,
+        },
+      },
+      {
+        root: true,
+      }
+    )
     return data
   },
   forceUpdateUserData({ commit }, data) {
diff --git a/web-frontend/modules/core/store/group.js b/web-frontend/modules/core/store/group.js
index 652244a7f..582347b37 100644
--- a/web-frontend/modules/core/store/group.js
+++ b/web-frontend/modules/core/store/group.js
@@ -69,6 +69,38 @@ export const mutations = {
     })
     state.selected = {}
   },
+  ADD_GROUP_USER(state, { groupId, values }) {
+    const groupIndex = state.items.findIndex((item) => item.id === groupId)
+    if (groupIndex !== -1) {
+      state.items[groupIndex].users.push(values)
+    }
+  },
+  UPDATE_GROUP_USER(state, { groupId, id, values }) {
+    const groupIndex = state.items.findIndex((item) => item.id === groupId)
+    if (groupIndex === -1) {
+      return
+    }
+    const usersIndex = state.items[groupIndex].users.findIndex(
+      (item) => item.id === id
+    )
+    if (usersIndex === -1) {
+      return
+    }
+    Object.assign(
+      state.items[groupIndex].users[usersIndex],
+      state.items[groupIndex].users[usersIndex],
+      values
+    )
+  },
+  DELETE_GROUP_USER(state, { groupId, id }) {
+    const groupIndex = state.items.findIndex((item) => item.id === groupId)
+    if (groupIndex === -1) {
+      return
+    }
+    state.items[groupIndex].users = state.items[groupIndex].users.filter(
+      (item) => item.id !== id
+    )
+  },
 }
 
 export const actions = {
@@ -240,6 +272,77 @@ export const actions = {
     })
     return dispatch('application/clearAll', group, { root: true })
   },
+  /**
+   * Forcefully adds a group user in the list of group's users.
+   */
+  forceAddGroupUser({ commit }, { groupId, values }) {
+    commit('ADD_GROUP_USER', { groupId, values })
+  },
+  /**
+   * Forcefully updates a group user in the list of group's users
+   * and updates group's permissions attr if the group user is the
+   * same as the current user.
+   */
+  forceUpdateGroupUser({ commit, rootGetters }, { groupId, id, values }) {
+    commit('UPDATE_GROUP_USER', { groupId, id, values })
+    const userId = rootGetters['auth/getUserId']
+    if (values.user_id === userId) {
+      commit('UPDATE_ITEM', {
+        id: groupId,
+        values: { permissions: values.permissions },
+      })
+    }
+  },
+  /**
+   * Forcefully updates user's properties based on userId across all group users
+   * of all groups. Can be used e.g. to change the user's name across all group
+   * users that represent the same user in the system.
+   */
+  forceUpdateGroupUserAttributes({ commit, rootGetters }, { userId, values }) {
+    const groups = rootGetters['group/getAll']
+    for (const group of groups) {
+      const usersIndex = group.users.findIndex(
+        (item) => item.user_id === userId
+      )
+      if (usersIndex !== -1) {
+        commit('UPDATE_GROUP_USER', {
+          groupId: group.id,
+          id: group.users[usersIndex].id,
+          values,
+        })
+      }
+    }
+  },
+  /**
+   * Forcefully deletes a group user in the list of group's users. If the
+   * group user is the current user, the whole group is removed.
+   */
+  forceDeleteGroupUser({ commit, rootGetters }, { groupId, id, values }) {
+    const userId = rootGetters['auth/getUserId']
+    if (values.user_id === userId) {
+      commit('DELETE_ITEM', { id: groupId })
+    } else {
+      commit('DELETE_GROUP_USER', { groupId, id })
+    }
+  },
+  /**
+   * Forcefully deletes a user by deleting various user group instances
+   * in all groups where the user is present.
+   */
+  forceDeleteUser({ commit, rootGetters }, { userId }) {
+    const groups = rootGetters['group/getAll']
+    for (const group of groups) {
+      const usersIndex = group.users.findIndex(
+        (item) => item.user_id === userId
+      )
+      if (usersIndex !== -1) {
+        commit('DELETE_GROUP_USER', {
+          groupId: group.id,
+          id: group.users[usersIndex].id,
+        })
+      }
+    }
+  },
 }
 
 export const getters = {
diff --git a/web-frontend/test/unit/core/store/group.spec.js b/web-frontend/test/unit/core/store/group.spec.js
new file mode 100644
index 000000000..95f21407c
--- /dev/null
+++ b/web-frontend/test/unit/core/store/group.spec.js
@@ -0,0 +1,456 @@
+import groupStore from '@baserow/modules/core/store/group'
+import { TestApp } from '@baserow/test/helpers/testApp'
+
+describe('Group store', () => {
+  let testApp = null
+  let store = null
+
+  beforeEach(() => {
+    testApp = new TestApp()
+    store = testApp.store
+  })
+
+  afterEach(() => {
+    testApp.afterEach()
+  })
+
+  test('forceAddGroupUser adds user to a group', async () => {
+    const state = Object.assign(groupStore.state(), {
+      items: [
+        {
+          id: 1,
+          name: 'Group 1',
+          order: 1,
+          permissions: 'ADMIN',
+          users: [
+            {
+              id: 73,
+              user_id: 256,
+              group: 1,
+              name: 'John',
+              email: 'john@example.com',
+              permissions: 'ADMIN',
+              to_be_deleted: false,
+              created_on: '2022-08-10T14:20:05.629890Z',
+            },
+          ],
+        },
+      ],
+    })
+    groupStore.state = () => state
+    store.registerModule('test', groupStore)
+
+    await store.dispatch('test/forceAddGroupUser', {
+      groupId: 1,
+      values: {
+        id: 74,
+        user_id: 257,
+        group: 1,
+        name: 'Adam',
+        email: 'adam@example.com',
+        permissions: 'MEMBER',
+        to_be_deleted: false,
+        created_on: '2022-08-10T14:20:05.629890Z',
+      },
+    })
+
+    const group = store.getters['test/get'](1)
+    expect(group.users.length).toBe(2)
+    expect(group.users[1].user_id).toBe(257)
+    expect(group.users[1].permissions).toBe('MEMBER')
+  })
+
+  test('forceUpdateGroupUser updates a user from the group', async () => {
+    const state = Object.assign(groupStore.state(), {
+      items: [
+        {
+          id: 1,
+          name: 'Group 1',
+          order: 1,
+          permissions: 'ADMIN',
+          users: [
+            {
+              id: 73,
+              user_id: 256,
+              group: 1,
+              name: 'John',
+              email: 'john@example.com',
+              permissions: 'ADMIN',
+              to_be_deleted: false,
+              created_on: '2022-08-10T14:20:05.629890Z',
+            },
+          ],
+        },
+      ],
+    })
+    groupStore.state = () => state
+    store.registerModule('test', groupStore)
+
+    await store.dispatch('test/forceUpdateGroupUser', {
+      groupId: 1,
+      id: 73,
+      values: {
+        id: 73,
+        user_id: 256,
+        group: 1,
+        name: 'Petr',
+        email: 'petr@example.com',
+        permissions: 'MEMBER',
+        to_be_deleted: false,
+        created_on: '2022-08-10T14:20:05.629890Z',
+      },
+    })
+
+    const group = store.getters['test/get'](1)
+    expect(group.users.length).toBe(1)
+    expect(group.users[0].name).toBe('Petr')
+    expect(group.users[0].permissions).toBe('MEMBER')
+  })
+
+  test(`forceUpdateGroupUser updates a current group permissions 
+        when the current user is updated`, async () => {
+    await store.dispatch('auth/forceSetUserData', {
+      user: {
+        id: 256,
+      },
+      token:
+        `eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImpvaG5AZXhhb` +
+        `XBsZS5jb20iLCJpYXQiOjE2NjAyOTEwODYsImV4cCI6MTY2MDI5NDY4NiwianRpIjo` +
+        `iNDZmNzUwZWUtMTJhMS00N2UzLWJiNzQtMDIwYWM4Njg3YWMzIiwidXNlcl9pZCI6M` +
+        `iwidXNlcl9wcm9maWxlX2lkIjpbMl0sIm9yaWdfaWF0IjoxNjYwMjkxMDg2fQ.RQ-M` +
+        `NQdDR9zTi8CbbQkRrwNsyDa5CldQI83Uid1l9So`,
+    })
+    const state = Object.assign(groupStore.state(), {
+      items: [
+        {
+          id: 1,
+          name: 'Group 1',
+          order: 1,
+          permissions: 'ADMIN',
+          users: [
+            {
+              id: 73,
+              user_id: 256,
+              group: 1,
+              name: 'John',
+              email: 'john@example.com',
+              permissions: 'ADMIN',
+              to_be_deleted: false,
+              created_on: '2022-08-10T14:20:05.629890Z',
+            },
+          ],
+        },
+      ],
+    })
+    groupStore.state = () => state
+    store.registerModule('test', groupStore)
+
+    await store.dispatch('test/forceUpdateGroupUser', {
+      groupId: 1,
+      id: 73,
+      values: {
+        id: 73,
+        user_id: 256,
+        group: 1,
+        name: 'Petr',
+        email: 'petr@example.com',
+        permissions: 'MEMBER',
+        to_be_deleted: false,
+        created_on: '2022-08-10T14:20:05.629890Z',
+      },
+    })
+
+    const group = store.getters['test/get'](1)
+    expect(group.users.length).toBe(1)
+    expect(group.users[0].name).toBe('Petr')
+    expect(group.users[0].permissions).toBe('MEMBER')
+
+    expect(group.permissions).toBe('MEMBER')
+  })
+
+  test('forceUpdateGroupUserAttributes updates a user across all groups', async () => {
+    const state = Object.assign(groupStore.state(), {
+      items: [
+        {
+          id: 1,
+          name: 'Group 1',
+          order: 1,
+          permissions: 'ADMIN',
+          users: [
+            {
+              id: 73,
+              user_id: 256,
+              group: 1,
+              name: 'John',
+              email: 'john@example.com',
+              permissions: 'ADMIN',
+              to_be_deleted: false,
+              created_on: '2022-08-10T14:20:05.629890Z',
+            },
+          ],
+        },
+        {
+          id: 2,
+          name: 'Group 2',
+          order: 1,
+          permissions: 'ADMIN',
+          users: [
+            {
+              id: 2136,
+              user_id: 456,
+              group: 2,
+              name: 'Peter',
+              email: 'peter@example.com',
+              permissions: 'ADMIN',
+              to_be_deleted: false,
+              created_on: '2022-08-10T14:20:05.629890Z',
+            },
+            {
+              id: 173,
+              user_id: 256,
+              group: 2,
+              name: 'John',
+              email: 'john@example.com',
+              permissions: 'ADMIN',
+              to_be_deleted: false,
+              created_on: '2022-08-10T14:20:05.629890Z',
+            },
+          ],
+        },
+        {
+          id: 3,
+          name: 'Group 3',
+          order: 1,
+          permissions: 'ADMIN',
+          users: [
+            {
+              id: 2132,
+              user_id: 456,
+              group: 3,
+              name: 'Peter',
+              email: 'peter@example.com',
+              permissions: 'ADMIN',
+              to_be_deleted: false,
+              created_on: '2022-08-10T14:20:05.629890Z',
+            },
+          ],
+        },
+      ],
+    })
+    groupStore.state = () => state
+    store.registerModule('test', groupStore)
+
+    await store.dispatch('test/forceUpdateGroupUserAttributes', {
+      userId: 256,
+      values: {
+        name: 'John renamed',
+        to_be_deleted: true,
+      },
+    })
+
+    const group = store.getters['test/get'](1)
+    expect(group.users[0].name).toBe('John renamed')
+    expect(group.users[0].to_be_deleted).toBe(true)
+
+    const group2 = store.getters['test/get'](2)
+    expect(group2.users[0].name).toBe('Peter')
+    expect(group2.users[0].to_be_deleted).toBe(false)
+    expect(group2.users[1].name).toBe('John renamed')
+    expect(group2.users[1].to_be_deleted).toBe(true)
+
+    const group3 = store.getters['test/get'](3)
+    expect(group3.users[0].name).toBe('Peter')
+    expect(group3.users[0].to_be_deleted).toBe(false)
+  })
+
+  test('forceDeleteGroupUser removes a user from the group', async () => {
+    const state = Object.assign(groupStore.state(), {
+      items: [
+        {
+          id: 1,
+          name: 'Group 1',
+          order: 1,
+          permissions: 'ADMIN',
+          users: [
+            {
+              id: 73,
+              user_id: 256,
+              group: 1,
+              name: 'John',
+              email: 'john@example.com',
+              permissions: 'ADMIN',
+              to_be_deleted: false,
+              created_on: '2022-08-10T14:20:05.629890Z',
+            },
+          ],
+        },
+      ],
+    })
+    groupStore.state = () => state
+    store.registerModule('test', groupStore)
+
+    await store.dispatch('test/forceDeleteGroupUser', {
+      groupId: 1,
+      id: 73,
+      values: {
+        id: 73,
+        user_id: 256,
+        group: 1,
+        name: 'John',
+        email: 'john@example.com',
+        permissions: 'ADMIN',
+        to_be_deleted: false,
+        created_on: '2022-08-10T14:20:05.629890Z',
+      },
+    })
+
+    const group = store.getters['test/get'](1)
+    expect(group.users.length).toBe(0)
+  })
+
+  test(`forceDeleteGroupUser removes the whole group if the 
+        current user is being removed`, async () => {
+    await store.dispatch('auth/forceSetUserData', {
+      user: {
+        id: 256,
+      },
+      token:
+        `eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImpvaG5AZXhhb` +
+        `XBsZS5jb20iLCJpYXQiOjE2NjAyOTEwODYsImV4cCI6MTY2MDI5NDY4NiwianRpIjo` +
+        `iNDZmNzUwZWUtMTJhMS00N2UzLWJiNzQtMDIwYWM4Njg3YWMzIiwidXNlcl9pZCI6M` +
+        `iwidXNlcl9wcm9maWxlX2lkIjpbMl0sIm9yaWdfaWF0IjoxNjYwMjkxMDg2fQ.RQ-M` +
+        `NQdDR9zTi8CbbQkRrwNsyDa5CldQI83Uid1l9So`,
+    })
+
+    const state = Object.assign(groupStore.state(), {
+      items: [
+        {
+          id: 1,
+          name: 'Group 1',
+          order: 1,
+          permissions: 'ADMIN',
+          users: [
+            {
+              id: 73,
+              user_id: 256,
+              group: 1,
+              name: 'John',
+              email: 'john@example.com',
+              permissions: 'ADMIN',
+              to_be_deleted: false,
+              created_on: '2022-08-10T14:20:05.629890Z',
+            },
+          ],
+        },
+      ],
+    })
+    groupStore.state = () => state
+    store.registerModule('test', groupStore)
+
+    await store.dispatch('test/forceDeleteGroupUser', {
+      groupId: 1,
+      id: 73,
+      values: {
+        id: 73,
+        user_id: 256,
+        group: 1,
+        name: 'John',
+        email: 'john@example.com',
+        permissions: 'ADMIN',
+        to_be_deleted: false,
+        created_on: '2022-08-10T14:20:05.629890Z',
+      },
+    })
+
+    const groups = store.getters['test/getAll']
+    expect(groups.length).toBe(0)
+  })
+
+  test('forceDeleteUser deletes all group users across all groups', async () => {
+    const state = Object.assign(groupStore.state(), {
+      items: [
+        {
+          id: 1,
+          name: 'Group 1',
+          order: 1,
+          permissions: 'ADMIN',
+          users: [
+            {
+              id: 73,
+              user_id: 256,
+              group: 1,
+              name: 'John',
+              email: 'john@example.com',
+              permissions: 'ADMIN',
+              to_be_deleted: false,
+              created_on: '2022-08-10T14:20:05.629890Z',
+            },
+          ],
+        },
+        {
+          id: 2,
+          name: 'Group 2',
+          order: 1,
+          permissions: 'ADMIN',
+          users: [
+            {
+              id: 2136,
+              user_id: 456,
+              group: 2,
+              name: 'Peter',
+              email: 'peter@example.com',
+              permissions: 'ADMIN',
+              to_be_deleted: false,
+              created_on: '2022-08-10T14:20:05.629890Z',
+            },
+            {
+              id: 173,
+              user_id: 256,
+              group: 2,
+              name: 'John',
+              email: 'john@example.com',
+              permissions: 'ADMIN',
+              to_be_deleted: false,
+              created_on: '2022-08-10T14:20:05.629890Z',
+            },
+          ],
+        },
+        {
+          id: 3,
+          name: 'Group 3',
+          order: 1,
+          permissions: 'ADMIN',
+          users: [
+            {
+              id: 2132,
+              user_id: 456,
+              group: 3,
+              name: 'Peter',
+              email: 'peter@example.com',
+              permissions: 'ADMIN',
+              to_be_deleted: false,
+              created_on: '2022-08-10T14:20:05.629890Z',
+            },
+          ],
+        },
+      ],
+    })
+    groupStore.state = () => state
+    store.registerModule('test', groupStore)
+
+    await store.dispatch('test/forceDeleteUser', {
+      userId: 256,
+    })
+
+    const group = store.getters['test/get'](1)
+    expect(group.users.length).toBe(0)
+
+    const group2 = store.getters['test/get'](2)
+    expect(group2.users[0].name).toBe('Peter')
+    expect(group2.users[0].to_be_deleted).toBe(false)
+    expect(group2.users.length).toBe(1)
+
+    const group3 = store.getters['test/get'](3)
+    expect(group3.users.length).toBe(1)
+  })
+})