diff --git a/hiboo/account/login.py b/hiboo/account/login.py
index 316189eb545682dca2bf8e06b88bec27b8e01179..590ade9fc4dae9d21f67bb0b814221cca3906dda 100644
--- a/hiboo/account/login.py
+++ b/hiboo/account/login.py
@@ -2,6 +2,7 @@ from hiboo import models, utils, security
 from hiboo.account import blueprint, forms
 from flask_babel import lazy_gettext as _
 from flask import session
+from authlib.jose import JsonWebToken
 
 import datetime
 import flask_login
@@ -32,6 +33,20 @@ def signout():
 
 @blueprint.route("/signup", methods=["GET", "POST"])
 def signup():
+    if not flask.current_app.config['OPEN_SIGNUP']:
+        token = flask.request.args.get('token') or flask.abort(403)
+        key = flask.current_app.config["SECRET_KEY"]
+        jwt = JsonWebToken(['HS512'])
+        claims_options = {
+            'exp': {'essential': True, 'value': datetime.datetime.now().timestamp()},
+            'aud': {'essential': True, 'value': flask.url_for('.signup')}
+        }
+        try:
+            claims = jwt.decode(token, key, claims_options=claims_options)
+            claims.validate()
+        except Exception as e:
+            flask.flash(_("Invalid or expired signup link"), "danger")
+            return flask.redirect(flask.url_for(".signin"))
     form = forms.SignupForm()
     if form.validate_on_submit():
         conflict = models.User.query.filter_by(username=form.username.data).first()
@@ -72,4 +87,4 @@ def reset(token_uuid):
         models.db.session.commit()
         flask.flash(_("Successfully reset your password"), "success")
         return flask.redirect(flask.url_for(".signin"))
-    return flask.render_template("account_reset.html", form=form)
\ No newline at end of file
+    return flask.render_template("account_reset.html", form=form)
diff --git a/hiboo/application/social.py b/hiboo/application/social.py
index 60e7b7411289a09c06eb007faf7e585922b3a05a..2a632be6ba9c2a010addea01d7de191d69bb5f9e 100644
--- a/hiboo/application/social.py
+++ b/hiboo/application/social.py
@@ -43,8 +43,8 @@ class SynapseApplication(base.SAMLApplication):
 
     def configure(self, form):
         return {
-            "acs": form.application_uri.data + "/_matrix/saml2/authn_response",
-            "entityid": form.application_uri.data + "/_matrix/saml2/metadata.xml",
+            "acs": form.application_uri.data + "/_synapse/client/saml2/authn_response",
+            "entityid": form.application_uri.data + "/_synapse/client/saml2/metadata.xml",
             "sign_mode": "response"
         }
 
diff --git a/hiboo/application/templates/application_peertube.html b/hiboo/application/templates/application_peertube.html
index 2128c25f9ecca97b44014badc855d45cb14e228d..5966d10bb190f6ad288199de99c39b2ed0d631da 100644
--- a/hiboo/application/templates/application_peertube.html
+++ b/hiboo/application/templates/application_peertube.html
@@ -6,10 +6,10 @@
 <dd><pre>{{ url_for("sso.oidc_discovery", service_uuid=service.uuid, _external=True) }}</pre></dd>
 
 <dt>Client ID</dt>
-<dd><pre>service.config["client_id"]</pre></dd>
+<dd><pre>{{ service.config["client_id"] }}</pre></dd>
 
 <dt>Client secret</dt>
-<dd><pre>service.config["client_secret"]</pre></dd>
+<dd><pre>{{ service.config["client_secret"] }}</pre></dd>
 
 <dt>Scope</dt>
 <dd><pre>openid email profile</pre></dd>
@@ -20,4 +20,4 @@
 <dt>Email property</dt>
 <dd><pre>email</pre></dd>
 
-{% include "application_oidc.html" %}
\ No newline at end of file
+{% include "application_oidc.html" %}
diff --git a/hiboo/cli.py b/hiboo/cli.py
index e9cf242a845f5f82310cfb85dfc225bf567470d6..e9e70ea69d98a4aa91553de627e56306d0af0833 100644
--- a/hiboo/cli.py
+++ b/hiboo/cli.py
@@ -1,6 +1,9 @@
 from flask import cli
+from hiboo import models
 from hiboo.profile import common
 
+import time
+
 
 tasks = cli.AppGroup("tasks")
 
@@ -19,3 +22,10 @@ def tasks_loop():
     while True:
         common.apply_all_transitions()
         time.sleep(30)
+
+
+@tasks.command("purge")
+def purge():
+    # Temporary command to list purgeable accounts before we include
+    # this in the main task loop
+    print(models.User.get_unused().all())
diff --git a/hiboo/configuration.py b/hiboo/configuration.py
index e36dbe777df23e60bcdd034fe57bc4df10f93e3f..a2005a9e3ba66818fc4abea8068d6d15e8436be8 100644
--- a/hiboo/configuration.py
+++ b/hiboo/configuration.py
@@ -14,6 +14,7 @@ DEFAULT_CONFIG = {
     'TEMPLATES_AUTO_RELOAD': False,
     'MAIL_DOMAIN': 'tedomum.net',
     'WEBSITE_NAME': 'Hiboo',
+    'OPEN_SIGNUP': True,
     'API_TOKEN': 'changeMe'
 }
 
diff --git a/hiboo/models.py b/hiboo/models.py
index 518dec9222081bd68e22eba6c2ad5713b82bc4dc..b54eef96ad3b972d6a50a9dacd771977eeacc6a4 100644
--- a/hiboo/models.py
+++ b/hiboo/models.py
@@ -114,6 +114,17 @@ class User(db.Model):
         profile.status = Profile.ACTIVE
         return profile
 
+    @classmethod
+    def get_unused(cls, interval=datetime.timedelta(hours=2)):
+        return (cls.query
+            .join(cls.profiles.and_(Profile.status != Profile.PURGED), isouter=True)
+            .filter(Profile.uuid == None)
+            .filter(
+                datetime.datetime.now() - interval >
+                sqlalchemy.sql.func.coalesce(cls.updated_at, cls.created_at)
+            )
+        )
+
 
 class Auth(db.Model):
     """ An authenticator is a method to authenticate a user.
diff --git a/hiboo/user/templates/user_list.html b/hiboo/user/templates/user_list.html
index 1414649c8f3933b9eae9fbf2be8f1c0a9ababb95..78ac14af3a96e966cb2004a3cee62d9d0f80b479 100644
--- a/hiboo/user/templates/user_list.html
+++ b/hiboo/user/templates/user_list.html
@@ -24,3 +24,7 @@
   </div>
 </div>
 {% endblock %}
+
+{% block actions %}
+<a href="{{ url_for(".invite") }}" class="btn btn-primary">{% trans %}Sign-up link{% endtrans %}</a>
+{% endblock %}
diff --git a/hiboo/user/views.py b/hiboo/user/views.py
index 7a814435e2a8578f03e7f8f74fe18185a097691c..5ea587490d8442d3f83cb124c20ea31aeb297c44 100644
--- a/hiboo/user/views.py
+++ b/hiboo/user/views.py
@@ -1,6 +1,7 @@
 from hiboo.user import blueprint, forms
 from hiboo import models, utils, security
 from flask_babel import lazy_gettext as _
+from authlib.jose import jwt
 
 import datetime
 import flask
@@ -39,4 +40,21 @@ def password_reset(user_uuid):
     models.db.session.commit()
     reset_link = flask.url_for("account.reset", token_uuid=token.uuid, _external=True)
     flask.flash(_("Reset link: {}").format(reset_link), "success")
-    return flask.redirect(flask.url_for(".details", user_uuid=user.uuid))
\ No newline at end of file
+    return flask.redirect(flask.url_for(".details", user_uuid=user.uuid))
+
+
+@blueprint.route("/invite", methods=["GET", "POST"])
+@security.admin_required()
+@security.confirmation_required("generate a signup link")
+def invite():
+    expired = datetime.datetime.now() + datetime.timedelta(days=1)
+    payload = {
+        "exp": int(expired.timestamp()),
+        "aud": flask.url_for('account.signup')
+    }
+    header = {"alg": "HS512"}
+    key = flask.current_app.config["SECRET_KEY"]
+    token = jwt.encode(header, payload, key)
+    signup_link = flask.url_for("account.signup", token=token, _external=True)
+    flask.flash(_("Signup link: {}").format(signup_link), "success")
+    return flask.redirect(flask.url_for("user.list"))
diff --git a/requirements-prod.txt b/requirements-prod.txt
index 8f0ec4f003faf81b013f38609039de7559ed35cf..23554484ed162755689bf12a2d5342202daee800 100644
--- a/requirements-prod.txt
+++ b/requirements-prod.txt
@@ -1,67 +1,66 @@
-alembic==1.4.2
+alembic==1.6.5
 argon2-cffi==20.1.0
-astroid==2.4.2
-Authlib==0.14.1
-Babel==2.8.0
+Authlib==0.15.4
+axon @ git+https://forge.tedomum.net/tedomum/axon.git@5980a68da60c8de3fb7cbc8402a0f3db84bc3693
+Babel==2.9.1
 bcrypt==3.2.0
 blinker==1.4
-certifi==2020.4.5.1
-cffi==1.14.0
-chardet==3.0.4
+certifi==2021.5.30
+cffi==1.14.6
+charset-normalizer==2.0.3
 click==7.1.2
-cryptography==3.1.1
-decorator==4.4.2
-defusedxml==0.6.0
-dnspython==1.16.0
-email-validator==1.1.0
-Flask==1.1.2
+cryptography==3.4.7
+decorator==5.0.9
+defusedxml==0.7.1
+Deprecated==1.2.12
+dnspython==2.1.0
+elementpath==2.2.3
+email-validator==1.1.3
+Flask==1.1.4
 Flask-Babel==2.0.0
 Flask-DebugToolbar==0.11.0
 Flask-Limiter==1.4
 Flask-Login==0.5.0
-Flask-Migrate==2.5.3
+Flask-Migrate==3.0.1
 flask-redis==0.4.0
 Flask-Script==2.0.6
-Flask-SQLAlchemy==2.4.4
-Flask-WTF==0.14.3
-gunicorn==20.0.4
-idna==2.9
-infinity==1.4
-intervals==0.8.1
-isort==4.3.21
+Flask-SQLAlchemy==2.5.1
+Flask-WTF==0.15.1
+greenlet==1.1.0
+gunicorn==20.1.0
+idna==3.2
+importlib-resources==5.2.0
+infinity==1.5
+intervals==0.9.2
 itsdangerous==1.1.0
-Jinja2==2.11.2
-jwcrypto==0.8
-lazy-object-proxy==1.4.3
+Jinja2==2.11.3
+jwcrypto==0.9.1
 limits==1.5.1
-lxml==4.5.0
-Mako==1.1.2
-MarkupSafe==1.1.1
-mccabe==0.6.1
-mysqlclient==2.0.1
+lxml==4.6.3
+Mako==1.1.4
+MarkupSafe==2.0.1
+mysqlclient==2.0.3
 passlib==1.7.4
-Pillow==7.1.2
-pkgconfig==1.5.1
-psycopg2==2.8.6
+Pillow==8.3.1
+psycopg2==2.9.1
 pycparser==2.20
-pylint==2.5.3
-pyOpenSSL==19.1.0
-pysaml2==6.2.0
-python-dateutil==2.8.1
+pyOpenSSL==20.0.1
+pysaml2==7.0.1
+python-dateutil==2.8.2
 python-editor==1.0.4
-pytz==2020.1
-PyYAML==5.3.1
-redis==3.5.0
-requests==2.23.0
-six==1.14.0
-SQLAlchemy==1.3.16
+pytz==2021.1
+PyYAML==5.4.1
+redis==3.5.3
+requests==2.26.0
+six==1.16.0
+SQLAlchemy==1.4.22
 terminaltables==3.1.0
-toml==0.10.1
-urllib3==1.25.9
-validators==0.14.3
+urllib3==1.26.6
+validators==0.18.2
 Werkzeug==0.16.0
 wrapt==1.12.1
-WTForms==2.3.1
-WTForms-Components==0.10.4
-xmlsec==1.3.8
-git+https://forge.tedomum.net/tedomum/axon.git
+WTForms==2.3.3
+WTForms-Components==0.10.5
+xmlschema==1.6.4
+xmlsec==1.3.11
+zipp==3.5.0