diff --git a/src/backoffice/forms.py b/src/backoffice/forms/__init__.py
similarity index 100%
rename from src/backoffice/forms.py
rename to src/backoffice/forms/__init__.py
diff --git a/src/backoffice/forms/mqtt.py b/src/backoffice/forms/mqtt.py
new file mode 100644
index 0000000000000000000000000000000000000000..5f13f312d74519090e8bdc61dbef7a22592de0f9
--- /dev/null
+++ b/src/backoffice/forms/mqtt.py
@@ -0,0 +1,35 @@
+from django.forms import ModelForm
+from core.models import MqttTopic, MqttCredential, MqttCredentialAccess, MqttUserAccess
+
+
+class MqttTopicCreateForm(ModelForm):
+    class Meta:
+        model = MqttTopic
+        fields = [
+            'name',
+            'access_mode',
+            'subtree_wildcard',
+            'description',
+        ]
+
+
+class MqttTopicEditForm(ModelForm):
+    class Meta:
+        model = MqttTopic
+        fields = [
+            'access_mode',
+            'subtree_wildcard',
+            'description',
+        ]
+
+
+class MqttCredentialAccessForm(ModelForm):
+    class Meta:
+        model = MqttCredentialAccess
+        fields = [
+            'conference_member',
+            'access_mode',
+        ]
+
+    def __init__(self):
+        self.fields['conference_member'].disabled = True
diff --git a/src/core/integrations/mqtt.py b/src/core/integrations/mqtt.py
new file mode 100644
index 0000000000000000000000000000000000000000..914e927cebfa8c44a05acfdd2172d2ef171c2bd6
--- /dev/null
+++ b/src/core/integrations/mqtt.py
@@ -0,0 +1,31 @@
+class MqttIntegration:
+    def __init__(self):
+        # probably needs some info how to reach the Mqtt broker + credentials
+        pass
+
+    def initialize(self):
+        """ called at startup """
+        pass
+
+    def create_or_update_user(self, username: str, password: str, admin: bool):
+        """
+        Creates user or updates password / admin access.
+        If User was created, also create a private topic where only the user can publish/subscribe.
+        """
+        pass
+
+    def remove_user(self, username):
+        """ removes user with username `username`. """
+        pass
+
+    def create_or_update_topic(self, topic_path: str, public_subscribe: bool, public_publish: bool):
+        """ creates topic at path `topic_path` or updates global access """
+        pass
+
+    def remove_topic(self, topic_path: str):
+        """ removes topic at `topic_path` """
+        pass
+
+    def update_user_permission(self, topic_path: str, username: str, subscribe: bool, publish: bool):
+        """ updates permissions for user `username` at topic `topic_path` """
+        pass
diff --git a/src/core/models/__init__.py b/src/core/models/__init__.py
index 7d128951df1495c15d919e98cff4e901f4b14810..f917e8462aef5d6228ee06f3fd2217b338aac07f 100644
--- a/src/core/models/__init__.py
+++ b/src/core/models/__init__.py
@@ -6,6 +6,7 @@ from .board import BulletinBoardEntry
 from .events import Event, EventAttachment, EventLikeCount, EventParticipant
 from .pages import StaticPage, StaticPageRevision
 from .messages import DirectMessage
+from .mqtt import MqttTopic, MqttCredential, MqttCredentialAccess, MqttUserAccess
 from .rooms import Room, RoomLink
 from .schedules import ScheduleSource, ScheduleSourceImport, ScheduleSourceMapping
 from .shared import BackendMixin
@@ -21,6 +22,7 @@ __all__ = [
     'BackendMixin', 'Badge', 'BadgeToken', 'BulletinBoardEntry',
     'Conference', 'ConferenceMember', 'ConferenceMemberTicket', 'ConferenceTag', 'ConferenceTrack',
     'DereferrerStats', 'DirectMessage',
+    'MqttTopic', 'MqttCredential', 'MqttCredentialAccess', 'MqttUserAccess',
     'Event', 'EventAttachment', 'EventLikeCount', 'EventParticipant',
     'PlatformUser',
     'Room', 'RoomLink',
diff --git a/src/core/models/conference.py b/src/core/models/conference.py
index 10f93cbaba92bacee4f9811875ebf0d49315ca0e..bdb74f38146f10ab0285ba3ed10b5ae00036e5e8 100644
--- a/src/core/models/conference.py
+++ b/src/core/models/conference.py
@@ -29,6 +29,8 @@ class ConferenceMember(models.Model):
     permission_groups = models.ManyToManyField(Group, blank=True, related_name='+')
     static_page_groups = pg_fields.ArrayField(models.CharField(max_length=50), blank=True, null=True)
 
+    mqtt_defaultuser = models.ManyToManyField('MqttCredential', related_name='conference_member')
+
     class Meta:
         permissions = [
             ('assembly_team', _('ConferenceMember__permission-assembly_team')),
diff --git a/src/core/models/mqtt.py b/src/core/models/mqtt.py
new file mode 100644
index 0000000000000000000000000000000000000000..798b045d91e8dafae3d9563d348311fee7890b9a
--- /dev/null
+++ b/src/core/models/mqtt.py
@@ -0,0 +1,101 @@
+from django.db import models
+from django.db.models import F
+from django.utils.translation import gettext_lazy as _
+
+from .rooms import Room
+
+
+class MqttTopic(models.Model):
+
+    ACCESS_MODE__NONE = ''
+    ACCESS_MODE__READ = 'r'
+    ACCESS_MODE__WRITE = 'w'
+    ACCESS_MODE__READWRITE = 'rw'
+    ACCESS_MODES = (
+        (ACCESS_MODE__NONE, _("MqttTopic--AccessMode--none")),
+        (ACCESS_MODE__READ, _("MqttTopic--AccessMode--read")),
+        (ACCESS_MODE__WRITE, _("MqttTopic--AccessMode--write")),
+        (ACCESS_MODE__READWRITE, _("MqttTopic--AccessMode--readwrite")),
+    )
+
+    name = models.CharField(
+        max_length=200,
+        help_text=_('MqttTopic--name--help'),
+        verbose_name=_('MqttTopic--name')
+    )
+    description = models.TextField(
+        blank=True, null=True,
+        help_text=_('MqttTopic--description--help'),
+        verbose_name=_('MqttTopic--description')
+    )
+    access_mode = models.CharField(
+        choices=ACCESS_MODES, max_length=max([len(k) for (k, v) in ACCESS_MODES]),
+        help_text=_('MqttTopic--access_mode--help'),
+        verbose_name=_('MqttTopic--access_mode')
+    )
+    subtree_wildcard = models.BooleanField(
+        help_text=_('MqttTopic--subtree_wildcard--help'),
+        verbose_name=_('MqttTopic--subtree_wildcard')
+    )
+    room = models.ForeignKey(Room, on_delete=models.CASCADE, related_name='mqtt_topics')
+
+
+class MqttCredential(models.Model):
+    class Meta:
+        constraints = [
+            models.UniqueConstraint('username', name='mqttcredential__username'),
+        ]
+
+    username = models.CharField(
+        max_length=200,
+        help_text=_('MqttUser--username--help'),
+        verbose_name=_('MqttUser--username')
+    )
+    password = models.CharField(
+        max_length=200,
+        help_text=_('MqttUser--password--help'),
+        verbose_name=_('MqttUser--password')
+    )
+    blocked = models.BooleanField(
+        help_text=_('MqttUser--blocked--help'),
+        verbose_name=_('MqttUser--blocked')
+    )
+    description = models.CharField(
+        help_text=_('MqttTopic--description--help'),
+        verbose_name=_('MqttTopic--description')
+    )
+
+    # ForeignKey am PlatformUser - für den anonymen Clientuser auf den man per SSO zugreifen kann
+    # Many2Many am ConferenceMember für weitere Clientuser die auch Permissions requesten können
+
+
+class MqttUserAccess(models.Model):
+    class Meta:
+        constraints = [
+            models.UniqueConstraint('topic', 'credential', condition=F(granted=False), name='mqttuseraccess_unique_requests'),
+            models.UniqueConstraint('topic', 'credential', condition=F(granted=True), name='mqttuseraccess_unique_grants'),
+        ]
+
+    topic = models.ForeignKey(MqttTopic, on_delete=models.CASCADE, related_name='access')
+    conference_member = models.ForeignKey('ConferenceMember', on_delete=models.CASCADE, related_name='mqtt_access')
+    granted = models.BooleanField(default=False)
+    access_mode = models.CharField(
+        choices=MqttTopic.ACCESS_MODES, max_length=max([len(k) for (k, v) in MqttTopic.ACCESS_MODES]),
+        help_text=_('MqttUserAccess--access_mode--help'),
+        verbose_name=_('MqttUserAccess--access_mode')
+    )
+
+
+class MqttCredentialAccess(models.Model):
+    class Meta:
+        constraints = [
+            models.UniqueConstraint('topic', 'credential', name='mqttcredaccess_unique'),
+        ]
+
+    topic = models.ForeignKey(MqttTopic, on_delete=models.CASCADE, related_name='access')
+    conference_member = models.ForeignKey(MqttCredential, on_delete=models.CASCADE, related_name='access')
+    access_mode = models.CharField(
+        choices=MqttTopic.ACCESS_MODES, max_length=max([len(k) for (k, v) in MqttTopic.ACCESS_MODES]),
+        help_text=_('MqttUserAccess--access_mode--help'),
+        verbose_name=_('MqttUserAccess--access_mode')
+    )
diff --git a/src/core/models/users.py b/src/core/models/users.py
index 2bb966b13d823544968f9c271f3a7f7b7bdf76e3..471fcc9c98d3c509b69449a37e4a9fff8e89127c 100644
--- a/src/core/models/users.py
+++ b/src/core/models/users.py
@@ -181,6 +181,8 @@ class PlatformUser(AbstractUser):
         verbose_name=_("PlatformUser__wa_force_website_trigger"),
     )
 
+    mqtt_defaultuser = models.ForeignKey('MqttCredential', on_delete=models.SET_NULL, related_name='user')
+
     @classmethod
     def get_user_flags(cls, user):
         return {