diff --git a/backend/areas/users/users.py b/backend/areas/users/users.py
index 08f22c6f5f9266e6efc2c9dbfcfcb115a6501388..b1740ebd57eba73348a1ac4127d2f8652261368c 100644
--- a/backend/areas/users/users.py
+++ b/backend/areas/users/users.py
@@ -1,7 +1,7 @@
 from flask import jsonify, request
-from flask_jwt_extended import get_jwt, jwt_required
 from flask_cors import cross_origin
 from flask_expects_json import expects_json
+from flask_jwt_extended import get_jwt, jwt_required
 
 from areas import api_v1
 from helpers import KratosApi
diff --git a/backend/config.py b/backend/config.py
index 1972c59497b1229589837c66ca6973bb1e7fd408..6e5d37ac1221925e8403612674225bd0ccf5314d 100644
--- a/backend/config.py
+++ b/backend/config.py
@@ -21,3 +21,5 @@ SQLALCHEMY_TRACK_MODIFICATIONS = False
 # running in a Kubernetes pod. Set it to "false" to load the config from the
 # `KUBECONFIG` environment variable.
 LOAD_INCLUSTER_CONFIG = os.environ.get("LOAD_INCLUSTER_CONFIG").lower() == "true"
+
+DEMO_INSTANCE = os.environ.get("DASHBOARD_DEMO_INSTANCE", "False").lower() in ('true', '1')
diff --git a/backend/web/login/login.py b/backend/web/login/login.py
index 12d4ff1aecaa1aa69fc960ff9033aa30b09006bf..25f09a006ace2a225fdf17d3f674a810a6a64fa4 100644
--- a/backend/web/login/login.py
+++ b/backend/web/login/login.py
@@ -16,7 +16,7 @@ from ory_hydra_client.models import AcceptConsentRequest, AcceptLoginRequest, Co
 import ory_hydra_client.exceptions as hydra_exceptions
 import ory_kratos_client
 from ory_kratos_client.api import frontend_api, identity_api
-from flask import abort, redirect, render_template, request, current_app
+from flask import abort, current_app, jsonify, redirect, render_template, request
 
 from database import db
 from helpers import KratosUser
@@ -24,6 +24,8 @@ from config import *
 from web import web
 from areas.apps import AppRole, App, OAuthClientApp
 from areas.roles import RoleService
+from areas.roles.models import Role
+from areas.users.user_service import UserService
 
 
 # This is a circular import and should be solved differently
@@ -159,7 +161,7 @@ def login():
     # or `not identity`
     #     User is not logged in yet.
     # In either case, we present the login screen now.
-    return render_template("login.html", api_url=KRATOS_PUBLIC_URL, dashboard_url=DASHBOARD_URL, refresh=refresh)
+    return render_template("login.html", api_url=KRATOS_PUBLIC_URL, dashboard_url=DASHBOARD_URL, refresh=refresh, demo=DEMO_INSTANCE)
 
 
 @web.route("/auth", methods=["GET", "POST"])
@@ -543,3 +545,14 @@ def logout():
             ex)
     return redirect(kratos_api_response.logout_url)
 
+
+if DEMO_INSTANCE:
+    @web.route("/demo-user", methods=["POST"])
+    def demo_user():
+        data = request.get_json()
+        defaults = {
+            "name": "",
+            "app_roles": [{"name": "dashboard", "role_id": Role.ADMIN_ROLE_ID}],
+        }
+        UserService.post_user({**defaults, **data})
+        return jsonify("User created successfully. You should receive an email to confirm your address and set a password.")
diff --git a/backend/web/templates/login.html b/backend/web/templates/login.html
index 515472f1103544e17d36b44d9d67665c09db8c0d..11d9f8a5315dfb97662604ad906c3022e2729971 100644
--- a/backend/web/templates/login.html
+++ b/backend/web/templates/login.html
@@ -24,5 +24,63 @@
     <div id="contentHelp">
         <a href='recovery'>Set new password</a> | <a href='https://stackspin.net'>About stackspin</a>
     </div>
+{% if demo %}
+  <br>
+  <script>
+    function submitSignup() {
+      let result = document.querySelector('#signup-result');
+      let email = document.querySelector('#signup-email');
+      let xhr = new XMLHttpRequest();
+      xhr.responseType = 'json';
+      let url = "/web/demo-user";
+      xhr.open("POST", url, true);
+      xhr.setRequestHeader("Content-Type", "application/json");
+      xhr.onreadystatechange = function () {
+        if (xhr.readyState === 4) {
+          // In the success case, we get a plain (json) string; in the error
+          // case, we get an object with `errorMessage` property.
+          if (typeof(this.response) == 'object' && 'errorMessage' in this.response) {
+            window.console.log("Error in sign-up request.");
+            result.classList.remove('alert-success');
+            result.classList.add('alert-danger');
+            result.innerHTML = this.response.errorMessage;
+          } else {
+            result.classList.add('alert-success');
+            result.classList.remove('alert-danger');
+            result.innerHTML = this.response;
+          }
+          result.style.visibility = 'visible';
+        }
+      };
+      // Converting JSON data to string
+      var data = JSON.stringify({"email": email.value });
+      // Sending data with the request
+      xhr.send(data);
+      }
+  </script>
+  <h4>Sign up for this demo instance</h4>
+  Enter your email address here to create an account on this Stackspin
+  instance.
+  <div class="alert alert-warning" style="margin-top: 1em;">
+    Warning: this is a demo instance! That means that:
+    <ul>
+      <li>Anyone can create an account on this same instance, like yourself,
+        and will share the same set of users and data. So any data you create
+        or upload, including the email address you enter here, becomes
+        essentially public information.</li>
+      <li>Every night (Europe/Amsterdam time), this instance gets automatically
+        reset to an empty state, so any data you create or upload will be
+        destroyed.</li>
+    </ul>
+  </div>
+  <div class="form-group">
+    <label for="signup-email">Email address</label>
+    <input type="email" class="form-control" id="signup-email" name="signup-email" placeholder="Your email address to sign up with.">
+  </div>
+  <div class="form-group">
+    <button class="btn btn-primary" onclick="submitSignup()">Sign up</button>
+    <div id="signup-result" class="alert" style="visibility: hidden; margin-top: 1em;"></div>
+  </div>
+{% endif %}
 
 {% endblock %}