diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index ba6e5567ed0914500033837eba6a304fcf05ad96..4f114a3f40d589bb08e0dae260833e2c1ac3b4da 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -9,6 +9,13 @@ and this project adheres to `Semantic Versioning`_.
 Unreleased
 ----------
 
+Added
+~~~~~
+
+* GraphQL schema for Rooms
+* [Dev] UpdateIndicator Vue Component to display the status of interactive pages
+
+
 Fixed
 ~~~~~
 
diff --git a/aleksis/core/frontend/components/generic/UpdateIndicator.vue b/aleksis/core/frontend/components/generic/UpdateIndicator.vue
new file mode 100644
index 0000000000000000000000000000000000000000..16d5558bf3f75cf99d789f501b8e856038ccc528
--- /dev/null
+++ b/aleksis/core/frontend/components/generic/UpdateIndicator.vue
@@ -0,0 +1,85 @@
+<template>
+  <v-tooltip bottom>
+    <template #activator="{ on, attrs }">
+      <v-btn
+        right
+        icon
+        v-bind="attrs"
+        v-on="on"
+        @click="handleClick"
+        :loading="status === $options.UPDATING"
+      >
+        <v-icon v-if="status !== $options.UPDATING" :color="color">
+          {{ icon }}
+        </v-icon>
+      </v-btn>
+    </template>
+    <span>{{ text }}</span>
+  </v-tooltip>
+</template>
+
+<script>
+export default {
+  ERROR: "ERROR", // Something went wrong
+  SAVED: "SAVED", // Everything alright
+  UPDATING: "UPDATING", // We are sending something to the server
+  CHANGES: "CHANGES", // the user changed something, but it has not been saved yet
+  name: "UpdateIndicator",
+  emits: ["manual-update"],
+  props: {
+    status: {
+      type: String,
+      required: true,
+    },
+  },
+  computed: {
+    text() {
+      switch (this.status) {
+        case this.$options.SAVED:
+          return this.$t("status.saved");
+        case this.$options.UPDATING:
+          return this.$t("status.updating");
+        case this.$options.CHANGES:
+          return this.$t("status.changes");
+        default:
+          return this.$t("status.error");
+      }
+    },
+    color() {
+      switch (this.status) {
+        case this.$options.SAVED:
+          return "success";
+        case this.$options.CHANGES:
+          return "secondary";
+        case this.$options.UPDATING:
+          return "secondary";
+        default:
+          return "error";
+      }
+    },
+    icon() {
+      switch (this.status) {
+        case this.$options.SAVED:
+          return "$success";
+        case this.$options.CHANGES:
+          return "mdi-dots-horizontal";
+        default:
+          return "$warning";
+      }
+    },
+    isAbleToClick() {
+      return (
+        this.status === this.$options.CHANGES ||
+        this.status === this.$options.ERROR
+      );
+    },
+  },
+  methods: {
+    handleClick() {
+      if (this.isAbleToClick) {
+        this.$emit("manual-update");
+      }
+    },
+  },
+};
+</script>
diff --git a/aleksis/core/frontend/messages/en.json b/aleksis/core/frontend/messages/en.json
index 3adabc47bc60d1bfca2dfd74b0c4b31591c2ea88..ff9693d369fea8f9ce44e07d5cc1dd4b89aae1aa 100644
--- a/aleksis/core/frontend/messages/en.json
+++ b/aleksis/core/frontend/messages/en.json
@@ -224,5 +224,11 @@
   },
   "graphql": {
     "snackbar_error_message": "There was an error retrieving the page data. Please try again."
+  },
+  "status": {
+    "saved": "All changes are saved.",
+    "updating": "Changes are being synced.",
+    "changes": "You have unsaved changes.",
+    "error": "There has been an error while saving the latest changes."
   }
 }
diff --git a/aleksis/core/schema/room.py b/aleksis/core/schema/room.py
new file mode 100644
index 0000000000000000000000000000000000000000..d4f76700793d2f7eededaac1b0361ebeff1a6a5a
--- /dev/null
+++ b/aleksis/core/schema/room.py
@@ -0,0 +1,9 @@
+from graphene_django import DjangoObjectType
+
+from ..models import Room
+
+
+class RoomType(DjangoObjectType):
+    class Meta:
+        model = Room
+        fields = ("id", "name", "short_name")