diff --git a/syng/client.py b/syng/client.py
index 5776d70ea4f9e499776dd7bac9bc4c9916e650a3..ad2ceb949f546dd2bee228206f9a4f6b10adb56c 100644
--- a/syng/client.py
+++ b/syng/client.py
@@ -14,6 +14,7 @@ Excerp from the help::
       --room ROOM, -r ROOM
       --secret SECRET, -s SECRET
       --config-file CONFIG_FILE, -C CONFIG_FILE
+      --key KEY, -k KEY
 
 The config file should be a json file in the following style::
 
@@ -82,6 +83,8 @@ class State:
         a room, this must be identical. Also, if a webclient wants to have
         admin privileges, this must be included.
     :type secret: str
+    :param key: An optional key, if registration on the server is limited.
+    :type key: Optional[str]
     :param preview_duration: Amount of seconds the preview before a song be
         displayed.
     :type preview_duration: int
@@ -98,6 +101,7 @@ class State:
     room: str = ""
     server: str = ""
     secret: str = ""
+    key: Optional[str] = None
     preview_duration: int = 3
     last_song: Optional[datetime.datetime] = None
 
@@ -187,16 +191,16 @@ async def handle_connect() -> None:
     :rtype: None
     """
     logging.info("Connected to server")
-    await sio.emit(
-        "register-client",
-        {
-            "queue": state.queue,
-            "recent": state.recent,
-            "room": state.room,
-            "secret": state.secret,
-            "config": state.get_config(),
-        },
-    )
+    data = {
+        "queue": state.queue,
+        "recent": state.recent,
+        "room": state.room,
+        "secret": state.secret,
+        "config": state.get_config(),
+    }
+    if state.key:
+        data["registration-key"] = state.key
+    await sio.emit("register-client", data)
 
 
 @sio.on("get-meta-info")
@@ -390,6 +394,7 @@ async def aiomain() -> None:
     parser.add_argument("--room", "-r")
     parser.add_argument("--secret", "-s")
     parser.add_argument("--config-file", "-C", default="syng-client.json")
+    parser.add_argument("--key", "-k", default=None)
     parser.add_argument("server")
 
     args = parser.parse_args()
@@ -406,6 +411,8 @@ async def aiomain() -> None:
         if "preview_duration" in config["config"]:
             state.preview_duration = config["config"]["preview_duration"]
 
+    state.key = args.key if args.key else None
+
     if args.room:
         state.room = args.room
 
diff --git a/syng/server.py b/syng/server.py
index baa4d85cc12e398df233de3a87b9a228d4ae6ff2..9aba59a6b0714f4676288941ad03a22bc5037e17 100644
--- a/syng/server.py
+++ b/syng/server.py
@@ -16,6 +16,7 @@ from __future__ import annotations
 
 import asyncio
 import datetime
+import hashlib
 import logging
 import os
 import random
@@ -350,10 +351,12 @@ async def handle_register_client(sid: str, data: dict[str, Any]) -> None:
     Handle the "register-client" message.
 
     The data dictionary should have the following keys:
+        - `registration_key` (Optional), a key corresponding to those stored
+                    in `app["registration-keyfile"]`
         - `room` (Optional), the requested room
         - `config`, an dictionary of initial configurations
         - `queue`, a list of initial entries for the queue. The entries are
-                   encoded as a dictionary.
+                    encoded as a dictionary.
         - `recent`, a list of initial entries for the recent list. The entries
                     are encoded as a dictionary.
         - `secret`, the secret of the room
@@ -363,6 +366,9 @@ async def handle_register_client(sid: str, data: dict[str, Any]) -> None:
     playback client will be replaced if and only if, the new playback
     client has the same secret.
 
+    If registration is restricted, abort, if the given key is not in the
+    registration keyfile.
+
     If no room is provided, a fresh room id is generated.
 
     If the client provides a new room, or a new room id was generated, the
@@ -394,6 +400,25 @@ async def handle_register_client(sid: str, data: dict[str, Any]) -> None:
             client_id = gen_id(length + 1)
         return client_id
 
+    if not app["public"]:
+        with open(app["registration-keyfile"]) as f:
+            raw_keys = f.readlines()
+            keys = [key[:64] for key in raw_keys]
+
+            if (
+                "registration-key" not in data
+                or hashlib.sha256(
+                    data["registration-key"].encode()
+                ).hexdigest()
+                not in keys
+            ):
+                await sio.emit(
+                    "client-registered",
+                    {"success": False, "room": None},
+                    room=sid,
+                )
+                return
+
     room: str = data["room"] if "room" in data and data["room"] else gen_id()
     async with sio.session(sid) as session:
         session["room"] = room
@@ -791,8 +816,14 @@ def main() -> None:
     parser.add_argument("--host", "-H", default="localhost")
     parser.add_argument("--port", "-p", default="8080")
     parser.add_argument("--root-folder", "-r", default="syng/static/")
+    parser.add_argument("--registration-keyfile", "-k", default=None)
     args = parser.parse_args()
 
+    app["public"] = True
+    if args.registration_keyfile:
+        app["public"] = False
+        app["registration-keyfile"] = args.registration_keyfile
+
     app["root_folder"] = args.root_folder
 
     app.add_routes(