diff --git a/src/backoffice/templates/backoffice/assembly_detail.html b/src/backoffice/templates/backoffice/assembly_detail.html
index 9ae3747639f8a0d4abefce12e075f59da4552d0b..da2710a8433420a664a2a631741fc1e9030e97e3 100644
--- a/src/backoffice/templates/backoffice/assembly_detail.html
+++ b/src/backoffice/templates/backoffice/assembly_detail.html
@@ -91,6 +91,49 @@
     </div>
   </div>
 </div>
+{% if assembly.requests %}
+<div class="row mt-3">
+  <div class="col-md-12">
+    <div class="card">
+      <div class="card-header">{% trans 'assembly_requests' %}</div>
+      <div class="card-body">
+        <p class="card-text"><small class="text-muted">{% trans 'assembly_requests_help' %}</small></p>
+        <table class="table">
+          <tbody>
+          {% for child in assembly.requests.all %}
+            {% if child.source_assembly != assembly %}
+            <tr>
+              <td>
+                <a href="{% url 'backoffice:assembly' pk=child.source_assembly.id %}">{{ child.source_assembly.name }}</a>
+                {% if child.source_assembly.state_assembly == "hidden" %}<i class="bi bi-eye-slash" title='hidden'></i> {% endif %}
+              </td>
+              <td>{{ child.state }}</td>
+              <td>
+                {% if child.state != child.RequestsState.ACCEPTED %}
+                <form action="{% url 'backoffice:assembly-editchildren' pk=child.destination_assembly.id %}" method="POST" style="display: inline;">{% csrf_token %}
+                  <input type="hidden" name="approve" value="{{ child.source_assembly.pk }}">
+                  <input type="hidden" name="next" value="{% url 'backoffice:assembly' pk=assembly.pk %}">
+                  <button type="submit" class="btn btn-sm btn-primary">{% trans 'approve' %}</button>
+                </form>
+                {% endif %}
+                {% if child.state != child.RequestsState.REJECTED %}
+                <form action="{% url 'backoffice:assembly-editchildren' pk=child.destination_assembly.id %}" method="POST" style="display: inline;">{% csrf_token %}
+                  <input type="hidden" name="reject" value="{{ child.source_assembly.pk }}">
+                  <input type="hidden" name="next" value="{% url 'backoffice:assembly' pk=assembly.pk %}">
+                  <button type="submit" class="btn btn-sm btn-danger">{% trans 'reject' %}</button>
+                </form>
+                {% endif %}
+              </td>
+            </tr>
+            {% endif %}
+          {% endfor %}
+          </tbody>
+        </table>
+      </div>
+    </div>
+  </div>
+</div>
+{% endif %}
 
 <div class="row mt-3">
   {% if assembly.hierarchy == 'regular' %}
@@ -126,11 +169,20 @@
             <tr>
               <th>Name</th>
               <th>Beschreibung</th>
+              <th>Status</th>
             </tr>
           </thead>
           <tbody>
             {% for child in assembly.children.all %}
-            <tr><td><a href="{% url 'backoffice:assembly' pk=child.pk %}">{{ child.name }}</a></td><td>{{ child.description|truncatechars:200 }}</td></tr>
+            <tr>
+              <td><a href="{% url 'backoffice:assembly' pk=child.pk %}">{{ child.name }}</a></td>
+              <td>{{ child.description|truncatechars:200 }}</td>
+              <td>
+                <span class="font-weight-bold{% if child.state_parent == 'accepted' %} text-success{% elif child.state_parent == 'rejected' %} text-danger{% else %} text-default{% endif %}">
+                {{ child.state_parent }}
+                </span>
+              </td>
+            </tr>
             {% endfor %}
           </tbody>
         </table>
diff --git a/src/backoffice/templates/backoffice/assembly_edit.html b/src/backoffice/templates/backoffice/assembly_edit.html
index 5b4b655362053b94dea87fc5c3ee80c9d191b360..f29d72928ee5d6d0da2f14d504797d2fb86c6cfe 100644
--- a/src/backoffice/templates/backoffice/assembly_edit.html
+++ b/src/backoffice/templates/backoffice/assembly_edit.html
@@ -61,18 +61,41 @@
         {% trans 'assemblyedit_parent__intro' %}
       </p>
 
+      {% with assembly_requests=assembly.requests.all %}
+      {% if assembly_requests %}
+      <div class="input-group">
+        <ul>
+        {% for request in assembly_requests %}
+        <li>
+          {% if request.source_assembly == assembly %}
+          {{ request.destination_assembly }}
+          {% else %}
+          {{ request.source_assembly }}
+          {% endif %}
+          <span class="badge {% if request.state == request.RequestsState.REJECTED %}bg-danger{% elif request.state == request.RequestsState.ACCEPTED %}bg-success{% else %}bg-primary{% endif %} rounded-pill py-1 px-2 text-decoration-none">{{ request.state }}</span>
+        </li>
+        {% endfor %}
+        </li>
+      </div>
+      {% endif %}
+
       <div class="input-group">
         <label class="form-label" for="assembly_parent">{% trans 'assembly_parent' %}</label>&nbsp;
         <select id="assembly_parent" name="parent_id">
           <option value=""{% if assembly.parent is None %} selected="selected"{% endif %}>-- {% trans 'none' %} --</option>
+          {% if assembly.parent.pk %}
+          <option value="{{ assembly.parent.pk }}" selected="selected">{{ assembly.parent }}</option>
+          {% else %}
           {% if assembly.parent is not None and assembly.parent.hierarchy == assembly.Hierarchy.CLUSTER_RESTRICTED %}
           <option value="{{ assembly.parent.pk }}" selected="selected">{{ assembly.parent.name }}</option>
           {% endif %}
           {% for cluster in clusters %}
           <option value="{{ cluster.pk }}"{% if assembly.parent.pk == cluster.pk %} selected="selected"{% endif %}>{{ cluster.name }}</option>
           {% endfor %}
+          {% endif %}
         </select>
       </div>
+      {% endwith %}
     </div>
   </div>
 </div></div>
diff --git a/src/backoffice/templates/backoffice/assembly_editchildren.html b/src/backoffice/templates/backoffice/assembly_editchildren.html
index e325c1703ec39944f7ce7da8f61ad2ac9fe305e0..b7624381868c02c2d3a24aa352a2ba75da9d683a 100644
--- a/src/backoffice/templates/backoffice/assembly_editchildren.html
+++ b/src/backoffice/templates/backoffice/assembly_editchildren.html
@@ -42,19 +42,43 @@
         {% trans 'assemblyedit_children' %}
       </div>
       <div class="card-body">
-        {% with children=assembly.children.all %}
+        {% with children=assembly.requests.all %}
         {% if children|length > 0 %}
-        <ul>
+        <table class="table">
+          <thead>
+            <tr>
+              <th>{% trans 'Assembly__name' %}</th>
+              <th>{% trans 'Assembly__requests_state' %}</th>
+              <th> </th>
+          </thead>
+          <tbody>
           {% for child in children %}
-            <li>
-              {% if child.state_assembly == "hidden" %}<i class="bi bi-eye-slash" title='hidden'></i> {% endif %}<a href="{% url 'backoffice:assembly' pk=child.id %}">{{ child.name }}</a>
-              <form action="{% url 'backoffice:assembly-editchildren' pk=assembly.id %}" method="POST" style="display: inline;">{% csrf_token %}
-                <input type="hidden" name="delete" value="{{ child.pk }}">
-                <button type="submit" class="btn btn-sm btn-secondary">{% trans 'delete' %}</button>
-              </form>
-            </li>
+            <tr>
+              <td>
+                <a href="{% url 'backoffice:assembly' pk=child.destination_assembly.id %}">{{ child.destination_assembly.name }}</a>
+                {% if child.destination_assembly.state_assembly == "hidden" %}<i class="bi bi-eye-slash" title='hidden'></i> {% endif %}
+              </td>
+              <td>{{ child.state }}</td>
+              <td>
+                {% if child.source_assembly != assembly %}
+                {% if child.state != child.RequestsState.APPROVED %}
+                <form action="{% url 'backoffice:assembly-editchildren' pk=assembly.id %}" method="POST" style="display: inline;">{% csrf_token %}
+                  <input type="hidden" name="approve" value="{{ child.source_assembly.pk }}">
+                  <button type="submit" class="btn btn-sm btn-primary">{% trans 'approve' %}</button>
+                </form>
+                {% endif %}
+                {% if child.state != child.RequestsState.REJECTED %}
+                <form action="{% url 'backoffice:assembly-editchildren' pk=assembly.id %}" method="POST" style="display: inline;">{% csrf_token %}
+                  <input type="hidden" name="reject" value="{{ child.source_assembly.pk }}">
+                  <button type="submit" class="btn btn-sm btn-danger">{% trans 'reject' %}</button>
+                </form>
+                {% endif %}
+                {% endif %}
+              </td>
+            </tr>
           {% endfor %}
-        </ul>
+          </tbody>
+        </table>
         {% else %}
         {% trans 'no_entries' %}
         {% endif %}
diff --git a/src/backoffice/views/assemblies.py b/src/backoffice/views/assemblies.py
index 5d0e94b96cd7318fdcef0ac29f327012228ae2cd..52e1bb61922bb11b51dad3411669d62a81eaddf3 100644
--- a/src/backoffice/views/assemblies.py
+++ b/src/backoffice/views/assemblies.py
@@ -1,5 +1,8 @@
+from http.client import ACCEPTED
+from importlib.util import source_hash
 import logging
 from datetime import date
+from sys import deactivate_stack_trampoline
 
 from rest_framework.authtoken.models import Token
 
@@ -7,9 +10,10 @@ from django.conf import settings
 from django.contrib import messages
 from django.core.exceptions import PermissionDenied
 from django.http import Http404, HttpResponse
-from django.shortcuts import get_object_or_404, redirect, render
-from django.urls import reverse
+from django.shortcuts import get_object_or_404, get_list_or_404, redirect, render
+from django.urls import conf, reverse
 from django.utils import timezone
+from django.utils.http import url_has_allowed_host_and_scheme
 from django.utils.html import format_html
 from django.utils.safestring import mark_safe
 from django.utils.text import format_lazy
@@ -18,9 +22,10 @@ from django.utils.translation import gettext_lazy as _
 from django.views.generic import TemplateView, View
 from django.views.generic.edit import CreateView, FormView, UpdateView
 
+from core.admin import AssemblyMemberInline, AssemblyRequestsAdmin
 from core.integrations import BigBlueButton, Hangar, IntegrationError, WorkAdventure
-from core.models.assemblies import Assembly, AssemblyLink, AssemblyMember
-from core.models.conference import ConferenceExportCache
+from core.models.assemblies import Assembly, AssemblyLink, AssemblyMember, AssemblyRequests
+from core.models.conference import Conference, ConferenceExportCache
 from core.models.events import Event
 from core.models.rooms import Room, RoomLink
 from core.models.sso import Application
@@ -281,17 +286,50 @@ class EditAssemblyView(AssemblyMixin, UpdateView):
                 if parent is not None and parent.hierarchy == Assembly.Hierarchy.CLUSTER_RESTRICTED:
                     raise PermissionDenied
                 if parent is not None:
+                    assembly_requests = AssemblyRequests.objects.filter(source_assembly=assembly, destination_assembly=parent)
+                    if not assembly_requests:
+                        logger.info(
+                            'Assigning assembly "%(assembly_slug)s" (%(assembly_pk)s) to "%(parent_slug)s" (%(parent_pk)s) upon request by <%(user)s>.',
+                            {
+                                'assembly_slug': assembly.slug,
+                                'assembly_pk': assembly.pk,
+                                'parent_slug': parent.slug,
+                                'parent_pk': parent.pk,
+                                'user': self.request.user.username,
+                            },
+                        )
+                        request = AssemblyRequests.objects.create(
+                            source_assembly=assembly,
+                            destination_assembly=parent,
+                            request=AssemblyRequests.RequestsRequest.ASSEMBLY_LINK,
+                            state=AssemblyRequests.RequestsState.REQUESTED,
+                        )
+                        request.save()
+                else:
+                    request = AssemblyRequests.objects.filter(
+                        source_assembly=assembly,
+                        request=AssemblyRequests.RequestsRequest.ASSEMBLY_LINK,
+                        state=AssemblyRequests.RequestsState.ACCEPTED,
+                    ).delete()
+                    assembly.parent = None
+                    assembly.save()
                     logger.info(
-                        'Assigning assembly "%(assembly_slug)s" (%(assembly_pk)s) to "%(parent_slug)s" (%(parent_pk)s) upon request by <%(user)s>.',
+                        'Unassigned assembly "%(slug)s" (%(pk)s) upon request by <%(user)s>.',
                         {
-                            'assembly_slug': assembly.slug,
-                            'assembly_pk': assembly.pk,
-                            'parent_slug': parent.slug,
-                            'parent_pk': parent.pk,
+                            'slug': assembly.slug,
+                            'pk': assembly.pk,
                             'user': self.request.user.username,
                         },
                     )
-                else:
+            elif parent_id is None:
+                if AssemblyRequests.objects.filter(
+                    source_assembly=assembly, request=AssemblyRequests.RequestsRequest.ASSEMBLY_LINK, state=AssemblyRequests.RequestsState.REQUESTED
+                ):
+                    request = AssemblyRequests.objects.filter(
+                        source_assembly=assembly,
+                        request=AssemblyRequests.RequestsRequest.ASSEMBLY_LINK,
+                        state=AssemblyRequests.RequestsState.REQUESTED,
+                    ).delete()
                     logger.info(
                         'Unassigned assembly "%(slug)s" (%(pk)s) upon request by <%(user)s>.',
                         {
@@ -300,9 +338,6 @@ class EditAssemblyView(AssemblyMixin, UpdateView):
                             'user': self.request.user.username,
                         },
                     )
-                changes['parent'] = (assembly.parent.slug if assembly.parent_id is not None else None, parent.slug if parent is not None else None)
-                changes['parent_id'] = (assembly.parent_id, parent_id)
-                assembly.parent = parent
 
             elif 'parent_id' in changes:
                 # no change happened, delete that part of the log entry (artifact of combobox and Django's Form)
@@ -382,8 +417,8 @@ class AssemblyEditChildrenView(AssemblyMixin, View):
 
     def get_object(self, *args, **kwargs):
         assembly = self.assembly
-        # say 'Not Found' if assembly is not a cluster or clusters aren't supported at all
-        if not assembly.is_cluster or not self.conference.support_clusters:
+        # say 'Not Found' clusters aren't supported at all
+        if not self.conference.support_clusters:
             raise Http404
 
         # bail out if the current user is not associated as a contact
@@ -398,26 +433,69 @@ class AssemblyEditChildrenView(AssemblyMixin, View):
 
         changed = False
         add_id = request.POST.get('add', None)
+        approve_id = request.POST.get('approve', None)
+        reject_id = request.POST.get('reject', None)
         remove_id = request.POST.get('delete', None)
 
+        redirect_to = request.POST.get('next') or 'backoffice:assembly-editchildren'
+        url_is_safe = url_has_allowed_host_and_scheme(
+            url=redirect_to,
+            allowed_hosts=[self.request.get_host()],
+            require_https=self.request.is_secure(),
+        )
+        if not url_is_safe:
+            redirect_to = 'backoffice:assembly-editchildren'
+
+        if approve_id is not None:
+            assembly_request = AssemblyRequests.objects.filter(source_assembly=approve_id, destination_assembly=assembly)
+            assembly_request.update(
+                state=AssemblyRequests.RequestsState.ACCEPTED,
+            )
+            if assembly.is_cluster:
+                child_id = approve_id
+                parent = assembly
+            else:
+                child_id = assembly.pk
+                parent = get_object_or_404(Assembly, conference=self.conference, pk=approve_id)
+            child = get_object_or_404(Assembly, conference=self.conference, pk=child_id)
+            child.parent = parent
+            child.save()
+
+        if reject_id is not None:
+            assembly_request = AssemblyRequests.objects.filter(source_assembly=reject_id, destination_assembly=assembly)
+            assembly_request.update(
+                state=AssemblyRequests.RequestsState.REJECTED,
+            )
+            if assembly.is_cluster:
+                child_id = reject_id
+            else:
+                child_id = assembly.pk
+            child = get_object_or_404(Assembly, conference=self.conference, pk=child_id)
+            child.parent = None
+            child.save()
+
         if add_id is not None:
             child = get_object_or_404(Assembly, conference=self.conference, pk=add_id, hierarchy=Assembly.Hierarchy.REGULAR)
             if child.parent != assembly:
-                if child.parent is None:
-                    child.parent = assembly
-                    child.save()
-                    changed = True
-                    messages.success(request, gettext('assemblyedit_addedchild').format(child_name=child.name))
-                    logger.info(
-                        'Assembly "%(assembly_name)s" (%(assembly_pk)s): added child "%(child)s" (%(child_pk)s), requested by {%(user)s',
-                        {'assembly_name': assembly.name, 'assembly_pk': assembly.pk, 'child': child, 'child_pk': child.pk, 'user': request.user.username},
-                    )
-                else:
-                    messages.error(request, gettext('assemblyedit_not_adding_foreign_child').format(child_name=child.name))
-                    logger.info(
-                        'Assembly "%(assembly_name)s" (%(assembly_pk)s): could not steal child "%(child)s" (%(child_pk)s), requested by {%(user)s',
-                        {'assembly_name': assembly.name, 'assembly_pk': assembly.pk, 'child': child, 'child_pk': child.pk, 'user': request.user.username},
-                    )
+                assembly_request = AssemblyRequests(
+                    source_assembly=assembly,
+                    destination_assembly=child,
+                    request=AssemblyRequests.RequestsRequest.ASSEMBLY_LINK,
+                    state=AssemblyRequests.RequestsState.REQUESTED,
+                )
+                assembly_request.save()
+
+                messages.success(request, gettext('assemblyedit_addedchild').format(child_name=child.name))
+                logger.info(
+                    'Assembly "%(assembly_name)s" (%(assembly_pk)s): added child "%(child)s" (%(child_pk)s), requested by {%(user)s',
+                    {'assembly_name': assembly.name, 'assembly_pk': assembly.pk, 'child': child, 'child_pk': child.pk, 'user': request.user.username},
+                )
+            else:
+                messages.error(request, gettext('assemblyedit_not_adding_foreign_child').format(child_name=child.name))
+                logger.info(
+                    'Assembly "%(assembly_name)s" (%(assembly_pk)s): could not steal child "%(child)s" (%(child_pk)s), requested by {%(user)s',
+                    {'assembly_name': assembly.name, 'assembly_pk': assembly.pk, 'child': child, 'child_pk': child.pk, 'user': request.user.username},
+                )
 
         if remove_id is not None:
             child = get_object_or_404(Assembly, conference=self.conference, pk=remove_id, hierarchy=Assembly.Hierarchy.REGULAR)
@@ -444,7 +522,7 @@ class AssemblyEditChildrenView(AssemblyMixin, View):
             ConferenceExportCache.signal_assembly_modification(conference=self.conference, assembly=None)
 
         # we're done saving, redirect to children list view again
-        return redirect('backoffice:assembly-editchildren', pk=assembly.pk)
+        return redirect(redirect_to, pk=assembly.pk)
 
     def get(self, *args, **kwargs):
         candidates_qs = (
@@ -477,6 +555,7 @@ class AssemblyEditLinksView(AssemblyMixin, View):
         assembly = self.get_object()
 
         add_id = request.POST.get('add', None)
+        approve_id = request.POST.get('approve', None)
         remove_id = request.POST.get('delete', None)
 
         if add_id is not None:
@@ -500,6 +579,37 @@ class AssemblyEditLinksView(AssemblyMixin, View):
                         {'assembly_name': assembly.name, 'assembly_pk': assembly.pk, 'linkee': linkee, 'linkee_pk': linkee.pk, 'user': request.user.username},
                     )
 
+        if approve_id is not None:
+            logger.info('hallo')
+            try:
+                linkee = Assembly.objects.accessible_by_user(user=request.user, conference=self.conference).get(pk=remove_id)
+            except Assembly.DoesNotExist:
+                messages.warning('404 -Assembly %s', remove_id)
+                linkee = None
+
+            if linkee is not None:
+                count = AssemblyLink.objects.filter(a=assembly, b=linkee, is_public=True, type=AssemblyLink.Type.RELATED).update(
+                    parent_state=Assembly.StateParent.ACCEPTED
+                )
+                logger.info(
+                    'Assembly "%s" (%s): approved link to "%s" (%s) upon request by <%s>',
+                    assembly.slug,
+                    assembly.pk,
+                    linkee.slug,
+                    linkee.pk,
+                    request.user.username,
+                )
+                if count > 0:
+                    messages.success(request, gettext('assemblyedit_approvedlink').format(linked_name=linkee.name))
+                    logger.info(
+                        'Assembly "%s" (%s): approved link to "%s" (%s) upon request by <%s>',
+                        assembly.slug,
+                        assembly.pk,
+                        linkee.slug,
+                        linkee.pk,
+                        request.user.username,
+                    )
+
         if remove_id is not None:
             try:
                 linkee = Assembly.objects.accessible_by_user(user=request.user, conference=self.conference).get(pk=remove_id)
diff --git a/src/core/admin.py b/src/core/admin.py
index 3b6d86532ecc2cfa0854e1020eaa41b6dc2e1d1a..8ea5982a633ccdfdbc0d141fdf365ee990a34817 100644
--- a/src/core/admin.py
+++ b/src/core/admin.py
@@ -56,6 +56,7 @@ from .models import (
     VoucherEntry,
     WorkadventureSession,
     WorkadventureTexture,
+    AssemblyRequests,
 )
 
 logger = logging.getLogger(__name__)
@@ -458,6 +459,10 @@ class AssemblyLogEntryAdmin(admin.ModelAdmin):
     ]
 
 
+class AssemblyRequestsAdmin(admin.ModelAdmin):
+    model = AssemblyRequests
+
+
 class MapFloorAdmin(admin.ModelAdmin):
     list_display = ['index', 'name', 'conference']
     list_display_links = ['name']
@@ -1157,6 +1162,7 @@ admin.site.register(ConferenceTag, ConferenceTagAdmin)
 admin.site.register(ConferenceTrack, ConferenceTrackAdmin)
 admin.site.register(Assembly, AssemblyAdmin)
 admin.site.register(AssemblyLogEntry, AssemblyLogEntryAdmin)
+admin.site.register(AssemblyRequests, AssemblyRequestsAdmin)
 admin.site.register(BadgeCategory, BadgeCategoryAdmin)
 admin.site.register(Badge, BadgeAdmin)
 admin.site.register(BadgeToken, BadgeTokenAdmin)
diff --git a/src/core/migrations/0150_assemblyrequests.py b/src/core/migrations/0150_assemblyrequests.py
new file mode 100644
index 0000000000000000000000000000000000000000..dd63209d38d8691586b48741de8b314d463e1176
--- /dev/null
+++ b/src/core/migrations/0150_assemblyrequests.py
@@ -0,0 +1,24 @@
+# Generated by Django 5.1.2 on 2024-10-19 21:10
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('core', '0149_alter_event_additional_data'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='AssemblyRequests',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('request', models.CharField(choices=[('assembly link', 'Assembly__requests_request-link')], help_text='Assembly__requests_request__help', max_length=20, verbose_name='Assembly__requests_request')),
+                ('state', models.CharField(choices=[('requested', 'Assembly__requests_state-requested'), ('accepted', 'Assembly__requests_state-accepted'), ('rejected', 'Assembly__requests_state-rejected')], default='requested', help_text='Assembly__requests_state__help', max_length=20, verbose_name='Assembly__requests_state')),
+                ('destination_assembly', models.ForeignKey(blank=True, help_text='Assembly__requests__help', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='core.assembly', verbose_name='Assembly__destination')),
+                ('source_assembly', models.ForeignKey(blank=True, help_text='Assembly__requests__help', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='core.assembly', verbose_name='Assembly__source')),
+            ],
+        ),
+    ]
diff --git a/src/core/models/__init__.py b/src/core/models/__init__.py
index 8ab46fecbf381c6c75334de584555bda9c585f30..e686acaa07c95502cc7423c2aa99866d66f164bb 100644
--- a/src/core/models/__init__.py
+++ b/src/core/models/__init__.py
@@ -1,4 +1,4 @@
-from .assemblies import Assembly, AssemblyLink, AssemblyLogEntry, AssemblyMember
+from .assemblies import Assembly, AssemblyLink, AssemblyLogEntry, AssemblyMember, AssemblyRequests
 from .badges import Badge, BadgeCategory, BadgeToken, BadgeTokenTimeConstraint, UserBadge
 from .board import BulletinBoardEntry
 from .conference import Conference, ConferenceExportCache, ConferenceMember, ConferenceNavigationItem, ConferenceTrack
@@ -29,6 +29,7 @@ __all__ = [
     'AssemblyLink',
     'AssemblyLogEntry',
     'AssemblyMember',
+    'AssemblyRequests',
     'BackendMixin',
     'Badge',
     'BadgeCategory',
diff --git a/src/core/models/assemblies.py b/src/core/models/assemblies.py
index 7e8139efa490eb36edf4637b6892b26b421b71d9..fb6f582a4aaa3e6c703e97f0320745a5cd287538 100644
--- a/src/core/models/assemblies.py
+++ b/src/core/models/assemblies.py
@@ -409,6 +409,10 @@ class Assembly(TaggedItemMixin, models.Model):
     def public_children(self):
         return Assembly.objects.filter(parent=self).filter(state_assembly__in=Assembly.PUBLIC_STATES).order_by('name')
 
+    @property
+    def requests(self):
+        return AssemblyRequests.objects.filter(Q(source_assembly=self) | Q(destination_assembly=self))
+
     @property
     def linked_assemblies(self):
         qs = self.assembly_links.filter(is_public=True, type__in=AssemblyLink.PUBLIC_TYPES)
@@ -756,6 +760,45 @@ class AssemblyMember(models.Model):
         return f'{self.member} ({self.get_roles_display()})'
 
 
+class AssemblyRequests(models.Model):
+    class RequestsRequest(models.TextChoices):
+        ASSEMBLY_LINK = 'assembly link', _('Assembly__requests_request-link')
+
+    class RequestsState(models.TextChoices):
+        REQUESTED = 'requested', _('Assembly__requests_state-requested')
+        ACCEPTED = 'accepted', _('Assembly__requests_state-accepted')
+        REJECTED = 'rejected', _('Assembly__requests_state-rejected')
+
+    source_assembly = models.ForeignKey(
+        Assembly, blank=True, null=True, related_name='+', on_delete=models.PROTECT, verbose_name=_('Assembly__source'), help_text=_('Assembly__requests__help')
+    )
+    destination_assembly = models.ForeignKey(
+        Assembly,
+        blank=True,
+        null=True,
+        related_name='+',
+        on_delete=models.PROTECT,
+        verbose_name=_('Assembly__destination'),
+        help_text=_('Assembly__requests__help'),
+    )
+    request = models.CharField(
+        max_length=20,
+        choices=RequestsRequest.choices,
+        verbose_name=_('Assembly__requests_request'),
+        help_text=_('Assembly__requests_request__help'),
+    )
+    state = models.CharField(
+        max_length=20,
+        default=RequestsState.REQUESTED,
+        choices=RequestsState.choices,
+        verbose_name=_('Assembly__requests_state'),
+        help_text=_('Assembly__requests_state__help'),
+    )
+
+    def __str__(self):
+        return f'{self.source_assembly} -- {self.state} --> {self.destination_assembly}'
+
+
 class AssemblyLogEntry(models.Model):
     class Meta:
         verbose_name = _('AssemblyLogEntry')