diff --git a/aleksis/core/migrations/0047_add_room_model.py b/aleksis/core/migrations/0047_add_room_model.py
new file mode 100644
index 0000000000000000000000000000000000000000..2194017cf1678ab8c5bc4af61b7e7ed05e4cacf3
--- /dev/null
+++ b/aleksis/core/migrations/0047_add_room_model.py
@@ -0,0 +1,55 @@
+# Generated by Django 3.2.15 on 2022-11-20 14:20
+
+from django.apps import apps
+import django.contrib.sites.managers
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+import oauth2_provider.generators
+import oauth2_provider.models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('sites', '0002_alter_domain_unique'),
+        ('core', '0046_notification_create_field_icon'),
+    ]
+
+    if "chronos" in apps.app_configs:
+        recorder = migrations.recorder
+        if not recorder.MigrationRecorder.Migration.objects.filter(app="core", name="0046_add_room_model").exists():
+            dependencies.append(('chronos', '0012_add_supervision_global_permission'))
+
+    operations = [
+        migrations.CreateModel(
+            name='Room',
+            fields=[
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('extended_data', models.JSONField(default=dict, editable=False)),
+                ('short_name', models.CharField(max_length=255, verbose_name='Short name')),
+                ('name', models.CharField(max_length=255, verbose_name='Long name')),
+                ('site', models.ForeignKey(default=1, editable=False, on_delete=django.db.models.deletion.CASCADE, to='sites.site')),
+            ],
+            options={
+                'verbose_name': 'Room',
+                'verbose_name_plural': 'Rooms',
+                'ordering': ['name', 'short_name'],
+                'permissions': (('view_room_timetable', 'Can view room timetable'),),
+            },
+            managers=[
+                ('objects', django.contrib.sites.managers.CurrentSiteManager()),
+            ],
+        ),
+        migrations.AddConstraint(
+            model_name='room',
+            constraint=models.UniqueConstraint(fields=('site_id', 'short_name'), name='unique_room_short_name_per_site'),
+        ),
+        # Migrate data from Chronos table; deletion will be handled by Chronos
+        migrations.RunSQL(
+            """
+            -- Copy rooms from chronos if table exists
+            DO $$BEGIN INSERT INTO core_room SELECT * FROM chronos_room; EXCEPTION WHEN undefined_table THEN NULL; END$$;
+            """
+        ),
+    ]
diff --git a/aleksis/core/models.py b/aleksis/core/models.py
index 9bf3f0d12eb8bf7f9b9b7c03319647903acf6ea8..fff6d2cfa71a70b30d2f92a39f782ffc151c6927 100644
--- a/aleksis/core/models.py
+++ b/aleksis/core/models.py
@@ -1465,3 +1465,25 @@ class PersonalICalUrl(models.Model):
 
     def get_absolute_url(self):
         return reverse("ical_feed", kwargs={"slug": self.uuid})
+
+
+class Room(ExtensibleModel):
+    short_name = models.CharField(verbose_name=_("Short name"), max_length=255)
+    name = models.CharField(verbose_name=_("Long name"), max_length=255)
+
+    def __str__(self) -> str:
+        return f"{self.name} ({self.short_name})"
+
+    def get_absolute_url(self) -> str:
+        return reverse("timetable", args=["room", self.id])
+
+    class Meta:
+        permissions = (("view_room_timetable", _("Can view room timetable")),)
+        ordering = ["name", "short_name"]
+        verbose_name = _("Room")
+        verbose_name_plural = _("Rooms")
+        constraints = [
+            models.UniqueConstraint(
+                fields=["site_id", "short_name"], name="unique_room_short_name_per_site"
+            ),
+        ]
diff --git a/aleksis/core/search_indexes.py b/aleksis/core/search_indexes.py
index 7583a774eaadebb1cbfdb35d08dc0ddcfb355eea..ea0b4dae56290791bf9f99c919aa6d2d7925b64e 100644
--- a/aleksis/core/search_indexes.py
+++ b/aleksis/core/search_indexes.py
@@ -1,4 +1,4 @@
-from .models import Group, Person
+from .models import Group, Person, Room
 from .util.search import Indexable, SearchIndex
 
 
@@ -12,3 +12,9 @@ class GroupIndex(SearchIndex, Indexable):
     """Haystack index for searching groups."""
 
     model = Group
+
+
+class RoomIndex(SearchIndex, Indexable):
+    """Haystack index for searching rooms."""
+
+    model = Room