diff --git a/projects/cherry-shim/BUILD b/projects/cherry-shim/BUILD
new file mode 100644
index 0000000..fbc4ca7
--- /dev/null
+++ b/projects/cherry-shim/BUILD
@@ -0,0 +1,6 @@
+py_project(
+    name = "cherry-shim",
+    lib_deps = [
+        py_requirement("cherrypy"),
+    ]
+)
diff --git a/projects/cherry-shim/src/python/cherry_shim.py b/projects/cherry-shim/src/python/cherry_shim.py
new file mode 100644
index 0000000..482ada6
--- /dev/null
+++ b/projects/cherry-shim/src/python/cherry_shim.py
@@ -0,0 +1,28 @@
+#!/usr/bin/env python3
+
+# Import CherryPy
+import cherrypy
+
+
+def shim(app):
+    # Mount the application
+    cherrypy.tree.graft(app, "/")
+
+    # Unsubscribe the default server
+    cherrypy.server.unsubscribe()
+
+    # Instantiate a new server object
+    server = cherrypy._cpserver.Server()
+
+    def _run(host="0.0.0.0", port=8080, pool_size=16):
+        # Configure the server object
+        server.socket_host = host
+        server.socket_port = port
+        server.thread_pool = pool_size
+        server.subscribe()
+        cherrypy.engine.start()
+        cherrypy.engine.block()
+
+    server.run = _run
+
+    return server
diff --git a/projects/tentacles/BUILD b/projects/tentacles/BUILD
index 83b47df..4305c05 100644
--- a/projects/tentacles/BUILD
+++ b/projects/tentacles/BUILD
@@ -4,6 +4,7 @@ py_project(
     main_deps = [
         "//projects/anosql",
         "//projects/anosql-migrations",
+        "//projects/cherry-shim",
         py_requirement("click"),
         py_requirement("flask"),
         py_requirement("jinja2"),
diff --git a/projects/tentacles/config.toml b/projects/tentacles/config.toml
deleted file mode 100644
index 2e4c34c..0000000
--- a/projects/tentacles/config.toml
+++ /dev/null
@@ -1,10 +0,0 @@
-SECRET_KEY = "SgvzxsO5oPBGInmqsyyGQWAJXkS9"
-UPLOAD_FOLDER = "/home/arrdem/Documents/hobby/programming/source/projects/tentacles/tmp"
-
-[db]
-uri = "/home/arrdem/Documents/hobby/programming/source/projects/tentacles/tentacles.sqlite3"
-
-[[users]]
-email = "root@tirefireind.us"
-group_id = 0
-status_id = 1
diff --git a/projects/tentacles/src/python/tentacles/__main__.py b/projects/tentacles/src/python/tentacles/__main__.py
index 30c6bf3..506aa0a 100644
--- a/projects/tentacles/src/python/tentacles/__main__.py
+++ b/projects/tentacles/src/python/tentacles/__main__.py
@@ -8,6 +8,8 @@ import tomllib
 
 import click
 from flask import Flask, request
+
+from cherry_shim import shim
 from tentacles.blueprints import (
     api,
     file_ui,
@@ -102,10 +104,11 @@ def serve(hostname: str, port: int, config: Path):
 
     # Shove our middleware in there
     app.wsgi_app = custom_ctx(app, app.wsgi_app)
+    cherry = shim(app)
 
     # And run the blame thing
     try:
-        app.run(host=hostname, port=port)
+        cherry.run(host=hostname, port=port)
     finally:
         shutdown_event.set()
 
diff --git a/projects/tentacles/src/python/tentacles/globals.py b/projects/tentacles/src/python/tentacles/globals.py
index 896818a..5470d7c 100644
--- a/projects/tentacles/src/python/tentacles/globals.py
+++ b/projects/tentacles/src/python/tentacles/globals.py
@@ -15,6 +15,7 @@ class Ctx:
     sid: str = None
     username: str = None
     is_admin: bool = None
+    base_url: str = None
 
 
 _ctx = ContextVar("tentacles.ctx")
diff --git a/projects/tentacles/src/python/tentacles/schema.sql b/projects/tentacles/src/python/tentacles/schema.sql
index c6275f5..b2ec1db 100644
--- a/projects/tentacles/src/python/tentacles/schema.sql
+++ b/projects/tentacles/src/python/tentacles/schema.sql
@@ -14,8 +14,9 @@ CREATE TABLE IF NOT EXISTS user_statuses (
  , UNIQUE(name)
 );
 
-INSERT OR IGNORE INTO user_statuses (id, name) VALUES (-1, 'disabled');
+INSERT OR IGNORE INTO user_statuses (id, name) VALUES (-3, 'unapproved');
 INSERT OR IGNORE INTO user_statuses (id, name) VALUES (-2, 'unverified');
+INSERT OR IGNORE INTO user_statuses (id, name) VALUES (-1, 'disabled');
 INSERT OR IGNORE INTO user_statuses (id, name) VALUES (1, 'enabled');
 
 CREATE TABLE IF NOT EXISTS users (
@@ -25,7 +26,9 @@ CREATE TABLE IF NOT EXISTS users (
  , email TEXT
  , hash TEXT
  , status_id INTEGER
+ , verification_token TEXT DEFAULT (lower(hex(randomblob(32))))
  , verified_at TEXT
+ , approved_at TEXT
  , enabled_at TEXT
  , FOREIGN KEY(group_id) REFERENCES groups(id)
  , FOREIGN KEY(status_id) REFERENCES user_statuses(id)
@@ -61,6 +64,7 @@ CREATE TABLE IF NOT EXISTS printers (
    id INTEGER PRIMARY KEY AUTOINCREMENT
  , name TEXT
  , url TEXT
+ , stream_url TEXT
  , api_key TEXT
  , status_id INTEGER
  , last_poll_date TEXT
diff --git a/projects/tentacles/src/python/tentacles/templates/verification_email.html.j2 b/projects/tentacles/src/python/tentacles/templates/verification_email.html.j2
new file mode 100644
index 0000000..814f148
--- /dev/null
+++ b/projects/tentacles/src/python/tentacles/templates/verification_email.html.j2
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<html lang="en">
+  <head>
+    <link rel="stylesheet" href="{{ base_url }}/static/css/style.css" />
+  </head>
+  <body>
+    <nav class="container navbar">
+      <span class="logo">
+        <a class="row" href="/">
+          <img src="{{ base_url }}/static/tentacles.svg" alt="Tentacles">
+          <span class="name color-yellow">Tentacles</span>
+        </a>
+      </span>
+    </nav>
+
+    <div class="container content">
+      <div class="row">
+        <p>
+          Welcome {{ username }}!
+        </p>
+        <p>
+          Before you can use your account, please confirm your email address by clicking <a href="{{ base_url }}/user/verify?token={{token_id}}">this link</a> or pasting the following text into your browser's navigation bar.
+        </p>
+        <pre>
+          <code>
+            {{ base_url }}/user/verify?token={{token_id}}
+          </code>
+        </pre>
+        <p>
+          Once your email has been verified, one of our administrators will review and approve your account. Then you can get to printing!
+        </p>
+      </div>
+    </div>
+  </body>
+  <footer>
+
+  </footer>
+</html>
diff --git a/projects/tentacles/src/python/tentacles/workers.py b/projects/tentacles/src/python/tentacles/workers.py
index dd16ff5..0e89f6c 100644
--- a/projects/tentacles/src/python/tentacles/workers.py
+++ b/projects/tentacles/src/python/tentacles/workers.py
@@ -265,7 +265,13 @@ def send_verifications(app, store: Store):
             fm.send_message(
                 from_addr="root@tirefireind.us",
                 to_addrs=[user.email],
-                msg=render_template("verification_email.html.j2"),
+                subject="Email verification from tentacles",
+                msg=render_template(
+                    "verification_email.html.j2",
+                    base_url=app.config.get("base_url"),
+                    username=user.username,
+                    token_id=user.verification_token,
+                ),
             )
 
 
@@ -280,7 +286,7 @@ def send_approvals(app, store: Store):
 
 
 @corn_job(timedelta(seconds=5))
-def run_worker(app, db_factory):
+def run_worker(app: App, db_factory):
     with app.app_context(), closing(db_factory(app)) as store:
         poll_printers(app, store)
         assign_jobs(app, store)
@@ -291,7 +297,7 @@ def run_worker(app, db_factory):
         send_approvals(app, store)
 
 
-def create_workers(app, db_factory: Callable[[], Store]) -> Event:
+def create_workers(app, db_factory: Callable[[App], Store]) -> Event:
     Thread(target=run_worker, args=[app, db_factory]).start()
 
     return SHUTDOWN
diff --git a/tools/python/constraints.in b/tools/python/constraints.in
new file mode 100644
index 0000000..2b2d16f
--- /dev/null
+++ b/tools/python/constraints.in
@@ -0,0 +1,2 @@
+# jaraco.text==git+https://github.com/arrdem/jaraco.text.git@0dd8d0b25a93c3fad896f3a92d11caff61ff273d
+cherrypy==18.8.0
diff --git a/tools/python/defs.bzl b/tools/python/defs.bzl
index b631eea..abaac17 100644
--- a/tools/python/defs.bzl
+++ b/tools/python/defs.bzl
@@ -44,7 +44,7 @@ def py_pytest(name, srcs, deps, main=None, python_version=None, args=None, **kwa
 
     deps = sets.to_list(sets.make([
         py_requirement("pytest"),
-        py_requirement("pytest-pudb"),
+        # py_requirement("pytest-pudb"),
         py_requirement("pytest-cov"),
         py_requirement("pytest-timeout"),
     ] + deps))
diff --git a/tools/python/requirements.in b/tools/python/requirements.in
index 3692298..8705e45 100644
--- a/tools/python/requirements.in
+++ b/tools/python/requirements.in
@@ -1,3 +1,5 @@
+-c constraints.in
+
 ExifRead
 aiohttp
 aiohttp_basicauth
@@ -7,6 +9,7 @@ autoflake
 beautifulsoup4
 black
 cachetools
+cherrypy
 click
 colored
 flake8
@@ -40,6 +43,7 @@ redis
 requests
 requests
 retry
+pip-tools
 smbus2
 sphinx
 sphinxcontrib-openapi
diff --git a/tools/python/requirements_lock.txt b/tools/python/requirements_lock.txt
index 6f5a5ca..fd233f9 100644
--- a/tools/python/requirements_lock.txt
+++ b/tools/python/requirements_lock.txt
@@ -5,34 +5,42 @@ alabaster==0.7.13
 async-lru==2.0.2
 async-timeout==4.0.2
 attrs==23.1.0
+autocommand==2.2.2
 autoflake==2.1.1
 Babel==2.12.1
 beautifulsoup4==4.12.2
 black==23.3.0
 blinker==1.6.2
-cachetools==5.3.0
-certifi==2022.12.7
+build==0.10.0
+cachetools==5.3.1
+certifi==2023.5.7
 charset-normalizer==3.1.0
+cheroot==10.0.0
+CherryPy==18.8.0
 click==8.1.3
 colored==1.4.4
 commonmark==0.9.1
-coverage==7.2.2
+coverage==7.2.7
 decorator==5.1.1
 deepmerge==1.1.0
-docutils==0.19
-exceptiongroup==1.1.1
+docutils==0.20.1
 ExifRead==3.0.0
 flake8==6.0.0
 Flask==2.3.2
 frozenlist==1.3.3
-hypothesis==6.75.5
+hypothesis==6.75.9
 ibis==3.2.0
 icmplib==3.0.3
 idna==3.4
 imagesize==1.4.1
+inflect==6.0.4
 iniconfig==2.0.0
 isort==5.12.0
 itsdangerous==2.1.2
+jaraco.collections==4.2.0
+jaraco.context==4.3.0
+jaraco.functools==3.7.0
+jaraco.text @ git+https://github.com/arrdem/jaraco.text.git@0dd8d0b25a93c3fad896f3a92d11caff61ff273d
 jedi==0.18.2
 Jinja2==3.1.2
 jsonschema==4.17.3
@@ -43,52 +51,60 @@ libsass==0.22.0
 livereload==2.6.3
 lxml==4.9.2
 Markdown==3.4.3
-MarkupSafe==2.1.2
+MarkupSafe==2.1.3
 mccabe==0.7.0
 meraki==1.33.0
 mirakuru==2.5.1
 mistune==2.0.5
+more-itertools==9.1.0
 multidict==6.0.4
 mypy-extensions==1.0.0
 octorest==0.4
 openapi-schema-validator==0.4.4
 openapi-spec-validator==0.5.6
-packaging==23.0
+packaging==23.1
 parso==0.8.3
 pathable==0.4.3
 pathspec==0.11.1
-picobox==2.2.0
-platformdirs==3.2.0
+picobox==3.0.0
+pip==23.1.2
+pip-tools==6.13.0
+platformdirs==3.5.1
 pluggy==1.0.0
 port-for==0.6.3
+portend==3.1.0
 prompt-toolkit==3.0.38
 proquint==0.2.1
-psutil==5.9.4
+psutil==5.9.5
 psycopg==3.1.9
 psycopg2==2.9.6
 pudb==2022.1.3
 py==1.11.0
 pycodestyle==2.10.0
 pycryptodome==3.18.0
+pydantic==1.10.8
 pyflakes==3.0.1
-Pygments==2.14.0
+Pygments==2.15.1
+pyproject_hooks==1.0.0
 pyrsistent==0.19.3
-pytest==7.2.2
+pytest==7.3.1
 pytest-cov==4.1.0
 pytest-postgresql==5.0.0
 pytest-pudb==0.7.0
 pytest-timeout==2.1.0
+pytz==2023.3
 PyYAML==6.0
 recommonmark==0.7.1
 redis==4.5.5
 requests==2.31.0
 retry==0.9.2
 rfc3339-validator==0.1.4
+setuptools==67.7.2
 six==1.16.0
 smbus2==0.4.2
 snowballstemmer==2.2.0
 sortedcontainers==2.4.0
-soupsieve==2.4
+soupsieve==2.4.1
 Sphinx==7.0.1
 sphinx_mdinclude==0.5.3
 sphinxcontrib-applehelp==1.0.4
@@ -100,19 +116,21 @@ sphinxcontrib-openapi==0.8.1
 sphinxcontrib-programoutput==0.17
 sphinxcontrib-qthelp==1.0.3
 sphinxcontrib-serializinghtml==1.1.5
-termcolor==2.2.0
+tempora==5.2.2
+termcolor==2.3.0
 toml==0.10.2
-tomli==2.0.1
-tornado==6.2
-typing_extensions==4.5.0
+tornado==6.3.2
+typing_extensions==4.6.3
 unify==0.5
 untokenize==0.1.1
-urllib3==1.26.15
+urllib3==2.0.2
 urwid==2.1.2
 urwid-readline==0.13
 wcwidth==0.2.6
-websocket-client==1.5.1
+websocket-client==1.5.2
 Werkzeug==2.3.4
+wheel==0.40.0
 yamllint==1.32.0
-yarl==1.8.2
+yarl==1.9.2
 yaspin==2.3.0
+zc.lockfile==3.0.post1