diff --git a/Pipfile b/Pipfile
new file mode 100644
index 0000000000000000000000000000000000000000..b25b76589174b23a27f6721516ff6c15ed1e933e
--- /dev/null
+++ b/Pipfile
@@ -0,0 +1,15 @@
+[[source]]
+url = "https://pypi.org/simple"
+verify_ssl = true
+name = "pypi"
+
+[packages]
+flask = "*"
+sqlalchemy = "*"
+cerberus = "*"
+argon2-cffi = "*"
+
+[dev-packages]
+
+[requires]
+python_version = "3.9"
diff --git a/Pipfile.lock b/Pipfile.lock
new file mode 100644
index 0000000000000000000000000000000000000000..94864488035876e5ba9262788e87592da2701240
--- /dev/null
+++ b/Pipfile.lock
@@ -0,0 +1,337 @@
+{
+    "_meta": {
+        "hash": {
+            "sha256": "23ba3a07b72940777356e477ceb4af1aa92774e7516dcec9038e675bdb1af013"
+        },
+        "pipfile-spec": 6,
+        "requires": {
+            "python_version": "3.9"
+        },
+        "sources": [
+            {
+                "name": "pypi",
+                "url": "https://pypi.org/simple",
+                "verify_ssl": true
+            }
+        ]
+    },
+    "default": {
+        "argon2-cffi": {
+            "hashes": [
+                "sha256:8c976986f2c5c0e5000919e6de187906cfd81fb1c72bf9d88c01177e77da7f80",
+                "sha256:d384164d944190a7dd7ef22c6aa3ff197da12962bd04b17f64d4e93d934dba5b"
+            ],
+            "index": "pypi",
+            "version": "==21.3.0"
+        },
+        "argon2-cffi-bindings": {
+            "hashes": [
+                "sha256:20ef543a89dee4db46a1a6e206cd015360e5a75822f76df533845c3cbaf72670",
+                "sha256:2c3e3cc67fdb7d82c4718f19b4e7a87123caf8a93fde7e23cf66ac0337d3cb3f",
+                "sha256:3b9ef65804859d335dc6b31582cad2c5166f0c3e7975f324d9ffaa34ee7e6583",
+                "sha256:3e385d1c39c520c08b53d63300c3ecc28622f076f4c2b0e6d7e796e9f6502194",
+                "sha256:58ed19212051f49a523abb1dbe954337dc82d947fb6e5a0da60f7c8471a8476c",
+                "sha256:5e00316dabdaea0b2dd82d141cc66889ced0cdcbfa599e8b471cf22c620c329a",
+                "sha256:603ca0aba86b1349b147cab91ae970c63118a0f30444d4bc80355937c950c082",
+                "sha256:6a22ad9800121b71099d0fb0a65323810a15f2e292f2ba450810a7316e128ee5",
+                "sha256:8cd69c07dd875537a824deec19f978e0f2078fdda07fd5c42ac29668dda5f40f",
+                "sha256:93f9bf70084f97245ba10ee36575f0c3f1e7d7724d67d8e5b08e61787c320ed7",
+                "sha256:9524464572e12979364b7d600abf96181d3541da11e23ddf565a32e70bd4dc0d",
+                "sha256:b2ef1c30440dbbcba7a5dc3e319408b59676e2e039e2ae11a8775ecf482b192f",
+                "sha256:b746dba803a79238e925d9046a63aa26bf86ab2a2fe74ce6b009a1c3f5c8f2ae",
+                "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3",
+                "sha256:bd46088725ef7f58b5a1ef7ca06647ebaf0eb4baff7d1d0d177c6cc8744abd86",
+                "sha256:ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367",
+                "sha256:d4966ef5848d820776f5f562a7d45fdd70c2f330c961d0d745b784034bd9f48d",
+                "sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93",
+                "sha256:ed2937d286e2ad0cc79a7087d3c272832865f779430e0cc2b4f3718d3159b0cb",
+                "sha256:f1152ac548bd5b8bcecfb0b0371f082037e47128653df2e8ba6e914d384f3c3e",
+                "sha256:f9f8b450ed0547e3d473fdc8612083fd08dd2120d6ac8f73828df9b7d45bb351"
+            ],
+            "markers": "python_version >= '3.6'",
+            "version": "==21.2.0"
+        },
+        "cerberus": {
+            "hashes": [
+                "sha256:d1b21b3954b2498d9a79edf16b3170a3ac1021df88d197dc2ce5928ba519237c"
+            ],
+            "index": "pypi",
+            "version": "==1.3.4"
+        },
+        "cffi": {
+            "hashes": [
+                "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3",
+                "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2",
+                "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636",
+                "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20",
+                "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728",
+                "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27",
+                "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66",
+                "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443",
+                "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0",
+                "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7",
+                "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39",
+                "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605",
+                "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a",
+                "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37",
+                "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029",
+                "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139",
+                "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc",
+                "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df",
+                "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14",
+                "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880",
+                "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2",
+                "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a",
+                "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e",
+                "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474",
+                "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024",
+                "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8",
+                "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0",
+                "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e",
+                "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a",
+                "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e",
+                "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032",
+                "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6",
+                "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e",
+                "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b",
+                "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e",
+                "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954",
+                "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962",
+                "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c",
+                "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4",
+                "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55",
+                "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962",
+                "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023",
+                "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c",
+                "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6",
+                "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8",
+                "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382",
+                "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7",
+                "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc",
+                "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997",
+                "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796"
+            ],
+            "version": "==1.15.0"
+        },
+        "click": {
+            "hashes": [
+                "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e",
+                "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==8.1.3"
+        },
+        "flask": {
+            "hashes": [
+                "sha256:315ded2ddf8a6281567edb27393010fe3406188bafbfe65a3339d5787d89e477",
+                "sha256:fad5b446feb0d6db6aec0c3184d16a8c1f6c3e464b511649c8918a9be100b4fe"
+            ],
+            "index": "pypi",
+            "version": "==2.1.2"
+        },
+        "greenlet": {
+            "hashes": [
+                "sha256:0051c6f1f27cb756ffc0ffbac7d2cd48cb0362ac1736871399a739b2885134d3",
+                "sha256:00e44c8afdbe5467e4f7b5851be223be68adb4272f44696ee71fe46b7036a711",
+                "sha256:013d61294b6cd8fe3242932c1c5e36e5d1db2c8afb58606c5a67efce62c1f5fd",
+                "sha256:049fe7579230e44daef03a259faa24511d10ebfa44f69411d99e6a184fe68073",
+                "sha256:14d4f3cd4e8b524ae9b8aa567858beed70c392fdec26dbdb0a8a418392e71708",
+                "sha256:166eac03e48784a6a6e0e5f041cfebb1ab400b394db188c48b3a84737f505b67",
+                "sha256:17ff94e7a83aa8671a25bf5b59326ec26da379ace2ebc4411d690d80a7fbcf23",
+                "sha256:1e12bdc622676ce47ae9abbf455c189e442afdde8818d9da983085df6312e7a1",
+                "sha256:21915eb821a6b3d9d8eefdaf57d6c345b970ad722f856cd71739493ce003ad08",
+                "sha256:288c6a76705dc54fba69fbcb59904ae4ad768b4c768839b8ca5fdadec6dd8cfd",
+                "sha256:2bde6792f313f4e918caabc46532aa64aa27a0db05d75b20edfc5c6f46479de2",
+                "sha256:32ca72bbc673adbcfecb935bb3fb1b74e663d10a4b241aaa2f5a75fe1d1f90aa",
+                "sha256:356b3576ad078c89a6107caa9c50cc14e98e3a6c4874a37c3e0273e4baf33de8",
+                "sha256:40b951f601af999a8bf2ce8c71e8aaa4e8c6f78ff8afae7b808aae2dc50d4c40",
+                "sha256:572e1787d1460da79590bf44304abbc0a2da944ea64ec549188fa84d89bba7ab",
+                "sha256:58df5c2a0e293bf665a51f8a100d3e9956febfbf1d9aaf8c0677cf70218910c6",
+                "sha256:64e6175c2e53195278d7388c454e0b30997573f3f4bd63697f88d855f7a6a1fc",
+                "sha256:7227b47e73dedaa513cdebb98469705ef0d66eb5a1250144468e9c3097d6b59b",
+                "sha256:7418b6bfc7fe3331541b84bb2141c9baf1ec7132a7ecd9f375912eca810e714e",
+                "sha256:7cbd7574ce8e138bda9df4efc6bf2ab8572c9aff640d8ecfece1b006b68da963",
+                "sha256:7ff61ff178250f9bb3cd89752df0f1dd0e27316a8bd1465351652b1b4a4cdfd3",
+                "sha256:833e1551925ed51e6b44c800e71e77dacd7e49181fdc9ac9a0bf3714d515785d",
+                "sha256:8639cadfda96737427330a094476d4c7a56ac03de7265622fcf4cfe57c8ae18d",
+                "sha256:8c5d5b35f789a030ebb95bff352f1d27a93d81069f2adb3182d99882e095cefe",
+                "sha256:8c790abda465726cfb8bb08bd4ca9a5d0a7bd77c7ac1ca1b839ad823b948ea28",
+                "sha256:8d2f1fb53a421b410751887eb4ff21386d119ef9cde3797bf5e7ed49fb51a3b3",
+                "sha256:903bbd302a2378f984aef528f76d4c9b1748f318fe1294961c072bdc7f2ffa3e",
+                "sha256:93f81b134a165cc17123626ab8da2e30c0455441d4ab5576eed73a64c025b25c",
+                "sha256:95e69877983ea39b7303570fa6760f81a3eec23d0e3ab2021b7144b94d06202d",
+                "sha256:9633b3034d3d901f0a46b7939f8c4d64427dfba6bbc5a36b1a67364cf148a1b0",
+                "sha256:97e5306482182170ade15c4b0d8386ded995a07d7cc2ca8f27958d34d6736497",
+                "sha256:9f3cba480d3deb69f6ee2c1825060177a22c7826431458c697df88e6aeb3caee",
+                "sha256:aa5b467f15e78b82257319aebc78dd2915e4c1436c3c0d1ad6f53e47ba6e2713",
+                "sha256:abb7a75ed8b968f3061327c433a0fbd17b729947b400747c334a9c29a9af6c58",
+                "sha256:aec52725173bd3a7b56fe91bc56eccb26fbdff1386ef123abb63c84c5b43b63a",
+                "sha256:b11548073a2213d950c3f671aa88e6f83cda6e2fb97a8b6317b1b5b33d850e06",
+                "sha256:b1692f7d6bc45e3200844be0dba153612103db241691088626a33ff1f24a0d88",
+                "sha256:b336501a05e13b616ef81ce329c0e09ac5ed8c732d9ba7e3e983fcc1a9e86965",
+                "sha256:b8c008de9d0daba7b6666aa5bbfdc23dcd78cafc33997c9b7741ff6353bafb7f",
+                "sha256:b92e29e58bef6d9cfd340c72b04d74c4b4e9f70c9fa7c78b674d1fec18896dc4",
+                "sha256:be5f425ff1f5f4b3c1e33ad64ab994eed12fc284a6ea71c5243fd564502ecbe5",
+                "sha256:dd0b1e9e891f69e7675ba5c92e28b90eaa045f6ab134ffe70b52e948aa175b3c",
+                "sha256:e30f5ea4ae2346e62cedde8794a56858a67b878dd79f7df76a0767e356b1744a",
+                "sha256:e6a36bb9474218c7a5b27ae476035497a6990e21d04c279884eb10d9b290f1b1",
+                "sha256:e859fcb4cbe93504ea18008d1df98dee4f7766db66c435e4882ab35cf70cac43",
+                "sha256:eb6ea6da4c787111adf40f697b4e58732ee0942b5d3bd8f435277643329ba627",
+                "sha256:ec8c433b3ab0419100bd45b47c9c8551248a5aee30ca5e9d399a0b57ac04651b",
+                "sha256:eff9d20417ff9dcb0d25e2defc2574d10b491bf2e693b4e491914738b7908168",
+                "sha256:f0214eb2a23b85528310dad848ad2ac58e735612929c8072f6093f3585fd342d",
+                "sha256:f276df9830dba7a333544bd41070e8175762a7ac20350786b322b714b0e654f5",
+                "sha256:f3acda1924472472ddd60c29e5b9db0cec629fbe3c5c5accb74d6d6d14773478",
+                "sha256:f70a9e237bb792c7cc7e44c531fd48f5897961701cdaa06cf22fc14965c496cf",
+                "sha256:f9d29ca8a77117315101425ec7ec2a47a22ccf59f5593378fc4077ac5b754fce",
+                "sha256:fa877ca7f6b48054f847b61d6fa7bed5cebb663ebc55e018fda12db09dcc664c",
+                "sha256:fdcec0b8399108577ec290f55551d926d9a1fa6cad45882093a7a07ac5ec147b"
+            ],
+            "markers": "python_version >= '3' and platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32')))))",
+            "version": "==1.1.2"
+        },
+        "importlib-metadata": {
+            "hashes": [
+                "sha256:5d26852efe48c0a32b0509ffbc583fda1a2266545a78d104a6f4aff3db17d700",
+                "sha256:c58c8eb8a762858f49e18436ff552e83914778e50e9d2f1660535ffb364552ec"
+            ],
+            "markers": "python_version < '3.10'",
+            "version": "==4.11.4"
+        },
+        "itsdangerous": {
+            "hashes": [
+                "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44",
+                "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==2.1.2"
+        },
+        "jinja2": {
+            "hashes": [
+                "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852",
+                "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==3.1.2"
+        },
+        "markupsafe": {
+            "hashes": [
+                "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003",
+                "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88",
+                "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5",
+                "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7",
+                "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a",
+                "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603",
+                "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1",
+                "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135",
+                "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247",
+                "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6",
+                "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601",
+                "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77",
+                "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02",
+                "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e",
+                "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63",
+                "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f",
+                "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980",
+                "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b",
+                "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812",
+                "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff",
+                "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96",
+                "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1",
+                "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925",
+                "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a",
+                "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6",
+                "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e",
+                "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f",
+                "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4",
+                "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f",
+                "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3",
+                "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c",
+                "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a",
+                "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417",
+                "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a",
+                "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a",
+                "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37",
+                "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452",
+                "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933",
+                "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a",
+                "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==2.1.1"
+        },
+        "pycparser": {
+            "hashes": [
+                "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9",
+                "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"
+            ],
+            "version": "==2.21"
+        },
+        "setuptools": {
+            "hashes": [
+                "sha256:990a4f7861b31532871ab72331e755b5f14efbe52d336ea7f6118144dd478741",
+                "sha256:c1848f654aea2e3526d17fc3ce6aeaa5e7e24e66e645b5be2171f3f6b4e5a178"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==62.6.0"
+        },
+        "sqlalchemy": {
+            "hashes": [
+                "sha256:07865d93e4ca77b59a5ce0f36fbae8161f7dfe57ba17934a3e442cf95dcb3c49",
+                "sha256:1ac6b091b322ec54a30c751dfcb736987e317f5c53a5cf3beb62e11a18210319",
+                "sha256:380e09881cdf3c87e90b8995425f7ea618e6bbd33c6b7c9234af21c4b6b3c143",
+                "sha256:3abe087b641788abbbe94abbf9f15f50bb985f72c0669ef35d1941d2912a276d",
+                "sha256:42810e560b57e981ed0a947b65a4936b398b4fca97e5b56e10a9c5a151568de2",
+                "sha256:42a60988aad143a4b2745711548833f57340d7f35586160140361314a509e6f7",
+                "sha256:470fd9d820fbd25c2a2a2929327c44aaff9d5871a20e0cadd32d293540817517",
+                "sha256:492f25432f0a998bcaa35e907f9d33f436d208326bb1e6c0f8485e8117502a3d",
+                "sha256:55c09559e45d3f067435620195238f983d4a23f796650f959f19964ba9104c6f",
+                "sha256:57ea67a9206eab2abe130e4fdae0662f10cca3dc72ba27553f70a7d613588571",
+                "sha256:63f8e68356b53072a653e8f61c5f1c19721469af4dbfdb3e3356073e9918f1fe",
+                "sha256:6edadd6a0a722c22558e1d1f5360d3e85fa938bc69d9049d29968a643de6dd34",
+                "sha256:737f4feee88d78230fa38027ad5645cb327fe9aac0dd0bde3f8fa7026ed81910",
+                "sha256:77831317da71adec7b785ebf9e6467b59ba1e186de1ba13c94b4e4951387ba64",
+                "sha256:82701a4cbb14affc6c1ae62dcebdaff65611b7c7f96f9d0e92a34a8be112a8fa",
+                "sha256:93ae1d2ef42fbf0f0b3d44b35225bda123310df4b33c9bf662e7b50a68c48a98",
+                "sha256:97ba370e31b70be94f2f1e85494a5c90f8cf50381ddc02ab95a33a4a86371e02",
+                "sha256:a57edcbbb45e8307153c5d4635407df71529ed263666064c0524b0c412778306",
+                "sha256:ad2447f17425e6889f0fb2b229844799aabafc90ff780123067fc5846a30992c",
+                "sha256:b33388891faf67d0c4a7bb65657dd1a068168eda4b793cb929c4c3894adfdcf2",
+                "sha256:b8cd779ef29718f3d2c558042ccc45c03006c599dd722fb760faca641a2f32ac",
+                "sha256:bdea12b997b174903292cf19f40d36cad46b44b645725b9485164684d1849bfd",
+                "sha256:bf05b312bf0165f92fa0eb09e7661c26f2f06c7a89694ecb79fa15a933deb768",
+                "sha256:c715347cac3b1c563941162fbbf751d3a5e0c356a33cb20925699f4910504a8f",
+                "sha256:cd1aba14bbb1ecfe8b5cc52dc840a7e071cfcce6bff545037cf56714c48dfc92",
+                "sha256:cf1afb1deec19de7ba282062de8a8c4f931ef120faa8b3dc6fca826bbc2f6a9d",
+                "sha256:cfdb1b3763aa4bddccd7b627b9466fce94952dc150a49309eb56e5f50dd00806",
+                "sha256:d3c4191e0348428b127c4c2e25ec9c1e8e895e3c6d9a7f083fca28dce23257ee",
+                "sha256:d8193b4a340d868f2daeeb856dfae9d9d4b011f249128380a83ee7342a887bdb",
+                "sha256:da424c8b285da91733fed2dd40fed7db076818a62859244d311b80fc8ba4d75e",
+                "sha256:e44e5f4d84861f4a2a00da8e55712db0dd2ec3d680544fb5d3ac84d3682d7d4c",
+                "sha256:eba2c5f717fa6d7be040bbc1e4334f1827d31e672cfd53ddbd995935d43e517e",
+                "sha256:f036bdc951b0d64c64ae83e7ff83a1848eea74f1c6e42461347caab2ed7282b9",
+                "sha256:f04789d723fbd6214a63006b4711d7afca37630473edb6ab972c5df2b43b7a56",
+                "sha256:fa64578158cb374e4dd6da2377f1ceabf9973313d171e67fc01a353aa8967858"
+            ],
+            "index": "pypi",
+            "version": "==1.4.38"
+        },
+        "werkzeug": {
+            "hashes": [
+                "sha256:1ce08e8093ed67d638d63879fd1ba3735817f7a80de3674d293f5984f25fb6e6",
+                "sha256:72a4b735692dd3135217911cbeaa1be5fa3f62bffb8745c5215420a03dc55255"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==2.1.2"
+        },
+        "zipp": {
+            "hashes": [
+                "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad",
+                "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==3.8.0"
+        }
+    },
+    "develop": {}
+}
diff --git a/config.example.json b/config.example.json
index 41d8d6e17fa9671b9907fffece11649619883144..ee7037bc5015958b07bb5c67561efb6ae35377b8 100644
--- a/config.example.json
+++ b/config.example.json
@@ -5,6 +5,192 @@
 	"IDENTIFIER_HASH_MEMORY_COST": 65536,
 	"IDENTIFIER_HASH_PARALLELISM": 4,
 	"POINTS": [
-	
+		"bydleni:nerozprodame-byty",
+		"bydleni:transparentni-pravidla-s-bytovym-fondem",
+		"bydleni:pece-o-bytove-fondy",
+		"bydleni:spoluprace-s-ministerstvem-pro-mistni-rozvoj",
+		"bydleni:komunitni-bydleni",
+		"bydleni:dostupne-socialni-bydleni",
+		"bydleni:mapovani-potreb-lidi-v-obci",
+		"bydleni:vztah-k-developerum",
+		"bydleni:ferove-mistni-poplatky",
+		"bydleni:mmr-rekonstrukce-socialniho-bydleni",
+		"otevrena-radnice:jasne-informace-z-radnice",
+		"otevrena-radnice:transparentni-rozhodovani-ve-verejnem-zajmu",
+		"otevrena-radnice:pruhledne-hospodareni-s-majetkem",
+		"otevrena-radnice:hospodarny-transparentni-rozpocet",
+		"otevrena-radnice:verejna-zadavaci-dokumentace",
+		"otevrena-radnice:prime-zakazky-vyjimka",
+		"otevrena-radnice:zverejnovani-zakazek-mimo-prazdniny",
+		"otevrena-radnice:zadavaci-rizeni-u-opakovanych-zakazek",
+		"otevrena-radnice:dbat-na-projektove-dokumentace",
+		"otevrena-radnice:svobodny-software-a-formaty",
+		"otevrena-radnice:granty-a-dotace-pro-mesto",
+		"otevrena-radnice:odborni-zastupci-mesta-v-organech",
+		"otevrena-radnice:pravidla-pro-obce-i-jejich-organizace",
+		"otevrena-radnice:pravidla-pro-nominace-a-odmenovani-v-spolecnostech",
+		"otevrena-radnice:uzemni-plan",
+		"otevrena-radnice:mesta-bez-barier",
+		"otevrena-radnice:kvalitni-verejny-prostor",
+		"otevrena-radnice:ucast-na-rozhodovani",
+		"otevrena-radnice:radnicni-media",
+		"otevrena-radnice:nekale-jednani-a-stret-zajmu",
+		"priroda:obec-pujde-prikladem",
+		"priroda:udrzitelne-vyuzivani-obecni-zelene",
+		"priroda:pece-o-zelen",
+		"priroda:komunitni-pestitelstvi",
+		"priroda:hajime-zajmy-obyvatel",
+		"priroda:cista-moderni-nezavisla-energetika",
+		"priroda:obce-pripravene-na-oteplovani",
+		"priroda:kvalita-ovzdusi",
+		"priroda:kvalitni-verejne-stravovani",
+		"priroda:voda-je-dulezita",
+		"priroda:obnovitelne-zdroje",
+		"aktivni-verejnost:referendum-a-obcanske-iniciativy",
+		"aktivni-verejnost:pozornost-peticim",
+		"aktivni-verejnost:elektronicka-zpetna-vazba-a-diskuze",
+		"aktivni-verejnost:podpora-cinnosti-spolku",
+		"aktivni-verejnost:podpora-mistniho-podnikani",
+		"verejne-sluzby:ani-obec-neni-velkym-bratrem",
+		"verejne-sluzby:kvalitni-verejne-sluzby",
+		"socialni-politika:dostupnost-terennich-a-ambulantnich-sluzeb",
+		"socialni-politika:zajisteni-kvalitni-pece-v-zarizenich",
+		"socialni-politika:dostupnost-rozvoj-odlehcovacich-sluzeb",
+		"socialni-politika:dostupne-najemni-bydleni",
+		"socialni-politika:planovani-koordinace-v-socialni-oblasti",
+		"socialni-politika:zvyhodnena-nutna-doprava-senioru",
+		"socialni-politika:system-kvalitnich-sluzeb-pece-o-deti",
+		"socialni-politika:podpora-sluzeb-pro-ohrozene-deti",
+		"socialni-politika:podpora-mezigeneracniho-souziti",
+		"socialni-politika:prevence-rozpadu-rodin",{
+	"SECRET_KEY": "odvazne delani neceho spravneho, nebo tak nejak",
+	"IDENTIFIER_HASH_PEPPER": "降り出した雨の音はずっと遠くまで響いた",
+	"IDENTIFIER_HASH_TIME_COST": 3,
+	"IDENTIFIER_HASH_MEMORY_COST": 65536,
+	"IDENTIFIER_HASH_PARALLELISM": 4,
+	"POINTS": [
+		"bydleni:nerozprodame-byty",
+		"bydleni:transparentni-pravidla-s-bytovym-fondem",
+		"bydleni:pece-o-bytove-fondy",
+		"bydleni:spoluprace-s-ministerstvem-pro-mistni-rozvoj",
+		"bydleni:komunitni-bydleni",
+		"bydleni:dostupne-socialni-bydleni",
+		"bydleni:mapovani-potreb-lidi-v-obci",
+		"bydleni:vztah-k-developerum",
+		"bydleni:ferove-mistni-poplatky",
+		"bydleni:mmr-rekonstrukce-socialniho-bydleni",
+		"otevrena-radnice:jasne-informace-z-radnice",
+		"otevrena-radnice:transparentni-rozhodovani-ve-verejnem-zajmu",
+		"otevrena-radnice:pruhledne-hospodareni-s-majetkem",
+		"otevrena-radnice:hospodarny-transparentni-rozpocet",
+		"otevrena-radnice:verejna-zadavaci-dokumentace",
+		"otevrena-radnice:prime-zakazky-vyjimka",
+		"otevrena-radnice:zverejnovani-zakazek-mimo-prazdniny",
+		"otevrena-radnice:zadavaci-rizeni-u-opakovanych-zakazek",
+		"otevrena-radnice:dbat-na-projektove-dokumentace",
+		"otevrena-radnice:svobodny-software-a-formaty",
+		"otevrena-radnice:granty-a-dotace-pro-mesto",
+		"otevrena-radnice:odborni-zastupci-mesta-v-organech",
+		"otevrena-radnice:pravidla-pro-obce-i-jejich-organizace",
+		"otevrena-radnice:pravidla-pro-nominace-a-odmenovani-v-spolecnostech",
+		"otevrena-radnice:uzemni-plan",
+		"otevrena-radnice:mesta-bez-barier",
+		"otevrena-radnice:kvalitni-verejny-prostor",
+		"otevrena-radnice:ucast-na-rozhodovani",
+		"otevrena-radnice:radnicni-media",
+		"otevrena-radnice:nekale-jednani-a-stret-zajmu",
+		"priroda:obec-pujde-prikladem",
+		"priroda:udrzitelne-vyuzivani-obecni-zelene",
+		"priroda:pece-o-zelen",
+		"priroda:komunitni-pestitelstvi",
+		"priroda:hajime-zajmy-obyvatel",
+		"priroda:cista-moderni-nezavisla-energetika",
+		"priroda:obce-pripravene-na-oteplovani",
+		"priroda:kvalita-ovzdusi",
+		"priroda:kvalitni-verejne-stravovani",
+		"priroda:voda-je-dulezita",
+		"priroda:obnovitelne-zdroje",
+		"aktivni-verejnost:referendum-a-obcanske-iniciativy",
+		"aktivni-verejnost:pozornost-peticim",
+		"aktivni-verejnost:elektronicka-zpetna-vazba-a-diskuze",
+		"aktivni-verejnost:podpora-cinnosti-spolku",
+		"aktivni-verejnost:podpora-mistniho-podnikani",
+		"verejne-sluzby:ani-obec-neni-velkym-bratrem",
+		"verejne-sluzby:kvalitni-verejne-sluzby",
+		"socialni-politika:dostupnost-terennich-a-ambulantnich-sluzeb",
+		"socialni-politika:zajisteni-kvalitni-pece-v-zarizenich",
+		"socialni-politika:dostupnost-rozvoj-odlehcovacich-sluzeb",
+		"socialni-politika:dostupne-najemni-bydleni",
+		"socialni-politika:planovani-koordinace-v-socialni-oblasti",
+		"socialni-politika:zvyhodnena-nutna-doprava-senioru",
+		"socialni-politika:system-kvalitnich-sluzeb-pece-o-deti",
+		"socialni-politika:podpora-sluzeb-pro-ohrozene-deti",
+		"socialni-politika:podpora-mezigeneracniho-souziti",
+		"socialni-politika:prevence-rozpadu-rodin",
+		"socialni-politika:podpora-tymu-pro-znevyhodnene-rodiny",
+		"socialni-politika:podpora-treninkovych-pracovnich-mist",
+		"socialni-politika:podpora-pravidelne-supervize",
+		"socialni-politika:koncepcni-zakladani-nabytkovych-bank",
+		"socialni-politika:navykove-chovani",
+		"zdravotni-pece:dostupnost-pece",
+		"zdravotni-pece:kvalita-pece",
+		"doprava:financovani-verejne-dopravy",
+		"doprava:verejny-prostor-spolecensky-kompromis",
+		"doprava:svoboda-zpusobu-prepravy",
+		"doprava:kvalitni-moderni-mhd",
+		"doprava:propojovani-verejne-dopravy",
+		"vzdelavani:dostupnost-skol",
+		"vzdelavani:kvalita-skol",
+		"vzdelavani:kvalita-vyuky",
+		"vzdelavani:spoluprace-ucitelu",
+		"vzdelavani:klima-ve-skolach",
+		"vzdelavani:digitalizace-skol",
+		"vzdelavani:kvalitni-poradenske-sluzby",
+		"vzdelavani:karierove-poradesntvi-na-skolach",
+		"kultura:podpora-kultury-a-financovani",
+		"kultura:koncepcni-cinnost",
+		"kultura:modernizace-knihoven",
+		"kultura:aktualizace-pametovych-instituci",
+		"kultura:komunitni-projekty-a-volnocasove-aktivity",
+		"kultura:udrzovani-kulturnich-pamatek",
+		"cestovni-ruch:naucne-stezky-cyklostezky",
+		"cestovni-ruch:digitalizace-cestovniho-ruchu",
+		"sport:rekonstrukce-sportovist",
+		"sport:zdravy-zivotni-styl",
+		"sport:pohybova-gramotnost"
+	]
+}
+
+		"socialni-politika:podpora-tymu-pro-znevyhodnene-rodiny",
+		"socialni-politika:podpora-treninkovych-pracovnich-mist",
+		"socialni-politika:podpora-pravidelne-supervize",
+		"socialni-politika:koncepcni-zakladani-nabytkovych-bank",
+		"socialni-politika:navykove-chovani",
+		"zdravotni-pece:dostupnost-pece",
+		"zdravotni-pece:kvalita-pece",
+		"doprava:financovani-verejne-dopravy",
+		"doprava:verejny-prostor-spolecensky-kompromis",
+		"doprava:svoboda-zpusobu-prepravy",
+		"doprava:kvalitni-moderni-mhd",
+		"doprava:propojovani-verejne-dopravy",
+		"vzdelavani:dostupnost-skol",
+		"vzdelavani:kvalita-skol",
+		"vzdelavani:kvalita-vyuky",
+		"vzdelavani:spoluprace-ucitelu",
+		"vzdelavani:klima-ve-skolach",
+		"vzdelavani:digitalizace-skol",
+		"vzdelavani:kvalitni-poradenske-sluzby",
+		"vzdelavani:karierove-poradesntvi-na-skolach",
+		"kultura:podpora-kultury-a-financovani",
+		"kultura:koncepcni-cinnost",
+		"kultura:modernizace-knihoven",
+		"kultura:aktualizace-pametovych-instituci",
+		"kultura:komunitni-projekty-a-volnocasove-aktivity",
+		"kultura:udrzovani-kulturnich-pamatek",
+		"cestovni-ruch:naucne-stezky-cyklostezky",
+		"cestovni-ruch:digitalizace-cestovniho-ruchu",
+		"rekonstrukce-sportovist",
+		"zdravy-zivotni-styl",
+		"pohybova-gramotnost"
 	]
 }
diff --git a/measurer/__init__.py b/measurer/__init__.py
index 0bb5c54f72f2d8848dda6892cab146701d16b3bc..54ab0241a069d3d2798d1f15593cbcb6e8187225 100644
--- a/measurer/__init__.py
+++ b/measurer/__init__.py
@@ -39,7 +39,7 @@ def create_app() -> flask.Flask:
 
 		app.logger.debug("Creating engine")
 
-		sa_engine = sqlalchemy.create_engine(app.config["DATABASE_URI"])
+		sa_engine = sqlalchemy.create_engine(os.environ["DATABASE_URL"])
 
 		app.sa_session_class = sqlalchemy.orm.scoped_session(
 			sqlalchemy.orm.sessionmaker(
diff --git a/measurer/database/models.py b/measurer/database/models.py
index 39948d3704a6cb08747a8a85398eef2615f00f72..55602682cc241b2f8cf1fb2345999cf1a013de72 100644
--- a/measurer/database/models.py
+++ b/measurer/database/models.py
@@ -5,6 +5,8 @@ import sqlalchemy
 from . import Base
 from .utils import UUID, get_uuid
 
+from .. import utils
+
 __all__ = ["Vote"]
 
 
@@ -27,6 +29,7 @@ class Vote(Base):
 
 	identifier = sqlalchemy.Column(
 		sqlalchemy.String(64),
+		default=utils.get_ip_hash,
 		nullable=False
 	)
 	"""An identifier of the person who made the vote. This should be, for example,
diff --git a/measurer/static/simple.css b/measurer/static/simple.css
deleted file mode 100644
index 377a6d13e724fb74321755241be135315a423d02..0000000000000000000000000000000000000000
--- a/measurer/static/simple.css
+++ /dev/null
@@ -1,516 +0,0 @@
-/* Global variables. */
-:root {
-  /* Set sans-serif & mono fonts */
-  --sans-font: -apple-system, BlinkMacSystemFont, "Avenir Next", Avenir,
-    "Nimbus Sans L", Roboto, Noto, "Segoe UI", Arial, Helvetica,
-    "Helvetica Neue", sans-serif;
-  --mono-font: Consolas, Menlo, Monaco, "Andale Mono", "Ubuntu Mono", monospace;
-
-  /* Default (light) theme */
-  --bg: #fff;
-  --accent-bg: #000000;
-  --text: #8a8a8a;
-  --text-light: #585858;
-  --border: #d8dae1;
-  --accent: #737373;
-  --code: #d81b60;
-  --preformatted: #444;
-  --marked: #ffdd33;
-  --disabled: #efefef;
-}
-
-/* Dark theme */
-@media (prefers-color-scheme: dark) {
-  :root {
-    --bg: #212121;
-    --accent-bg: #000000;
-    --text: #dcdcdc;
-    --text-light: #ababab;
-    --border: #666;
-    --accent: #ffb300;
-    --code: #f06292;
-    --preformatted: #ccc;
-    --disabled: #111;
-  }
-  /* Add a bit of transparancy so light media isn't so glaring in dark mode */
-  img,
-  video {
-    opacity: 0.8;
-  }
-}
-
-html {
-  /* Set the font globally */
-  font-family: var(--sans-font);
-  scroll-behavior: smooth;
-}
-
-/* Make the body a nice central block */
-body {
-  color: var(--text);
-  background: var(--bg);
-  font-size: 1.15rem;
-  line-height: 1.5;
-  display: grid;
-  grid-template-columns:
-    1fr min(45rem, 90%) 1fr;
-  margin: 0;
-}
-
-body>* {
-  grid-column: 2;
-}
-
-/* Make the header bg full width, but the content inline with body */
-body > header {
-  background: var(--accent-bg);
-  color: var(--bg);
-  border-bottom: 1px solid var(--border);
-  text-align: center;
-  padding: 1rem;
-  grid-column: 1 / -1;
-  box-sizing: border-box;
-}
-
-body > header h1 {
-  max-width: 1200px;
-  margin: 1rem auto;
-}
-
-body > header p {
-  max-width: 40rem;
-  margin: 1rem auto;
-}
-
-/* Add a little padding to ensure spacing is correct between content and nav */
-main {
-  padding-top: 1.5rem;
-}
-
-body > footer {
-  margin-top: 4rem;
-  padding: 1.5rem 1rem 1.5rem 1rem;
-  color: var(--text-light);
-  font-size: 0.9rem;
-  text-align: center;
-  border-top: 1px solid var(--border);
-}
-
-/* Format headers */
-h1 {
-  font-size: 3rem;
-}
-
-h2 {
-  font-size: 2.6rem;
-  margin-top: 3rem;
-}
-
-h3 {
-  font-size: 2rem;
-  margin-top: 3rem;
-}
-
-h4 {
-  font-size: 1.44rem;
-}
-
-h5 {
-  font-size: 1.15rem;
-}
-
-h6 {
-  font-size: 0.96rem;
-}
-
-/* Fix line height when title wraps */
-h1,
-h2,
-h3 {
-  line-height: 1.1;
-}
-
-/* Reduce header size on mobile */
-@media only screen and (max-width: 720px) {
-  h1 {
-    font-size: 2.5rem;
-  }
-
-  h2 {
-    font-size: 2.1rem;
-  }
-
-  h3 {
-    font-size: 1.75rem;
-  }
-
-  h4 {
-    font-size: 1.25rem;
-  }
-}
-
-/* Format links & buttons */
-a,
-a:visited {
-  color: var(--accent);
-}
-
-a:hover {
-  text-decoration: none;
-}
-
-button,
-[role="button"],
-input[type="submit"],
-input[type="reset"],
-input[type="button"] {
-  border: none;
-  border-radius: 5px;
-  background: var(--accent);
-  font-size: 1rem;
-  color: var(--bg);
-  padding: 0.7rem 0.9rem;
-  margin: 0.5rem 0;
-}
-
-button[disabled],
-[role="button"][aria-disabled="true"],
-input[type="submit"][disabled],
-input[type="reset"][disabled],
-input[type="button"][disabled],
-input[type="checkbox"][disabled],
-input[type="radio"][disabled],
-select[disabled] {
-  opacity: 0.5;
-  cursor: not-allowed;
-}
-
-input:disabled,
-textarea:disabled,
-select:disabled {
-  cursor: not-allowed;
-  background-color: var(--disabled);
-}
-
-input[type="range"] {
-  padding: 0;
-}
-
-/* Set the cursor to '?' while hovering over an abbreviation */
-abbr {
-  cursor: help;
-}
-
-button:focus,
-button:enabled:hover,
-[role="button"]:focus,
-[role="button"]:not([aria-disabled="true"]):hover,
-input[type="submit"]:focus,
-input[type="submit"]:enabled:hover,
-input[type="reset"]:focus,
-input[type="reset"]:enabled:hover,
-input[type="button"]:focus,
-input[type="button"]:enabled:hover {
-  filter: brightness(1.4);
-  cursor: pointer;
-}
-
-/* Format navigation */
-nav {
-  font-size: 1rem;
-  line-height: 2;
-  padding: 1rem 0 0 0;
-}
-
-/* Use flexbox to allow items to wrap, as needed */
-nav ul,
-nav ol {
-  align-content:   space-around;
-  align-items:     center;
-  display:         flex;
-  flex-direction:  row;
-  justify-content: center;
-  list-style-type: none;
-  margin:          0;
-  padding:         0;
-}
-
-/* List items are inline elements, make them behave more like blocks */
-nav ul li,
-nav ol li {
-  display: inline-block;
-}
-
-nav a,
-nav a:visited {
-  margin: 0 1rem 1rem 0;
-  border: 1px solid var(--border);
-  border-radius: 5px;
-  color: var(--text);
-  display: inline-block;
-  padding: 0.1rem 1rem;
-  text-decoration: none;
-}
-
-nav a:hover {
-  color: var(--accent);
-  border-color: var(--accent);
-}
-
-nav a:last-child {
-  margin-right: 0;
-}
-
-/* Reduce nav side on mobile */
-@media only screen and (max-width: 750px) {
-  nav a {
-    border: none;
-    padding: 0;
-    color: var(--accent);
-    text-decoration: underline;
-    line-height: 1;
-  }
-}
-
-/* Format the expanding box */
-details {
-  background: var(--accent-bg);
-  border: 1px solid var(--border);
-  border-radius: 5px;
-  margin-bottom: 1rem;
-}
-
-summary {
-  cursor: pointer;
-  font-weight: bold;
-  padding: 0.6rem 1rem;
-}
-
-details[open] {
-  padding: 0.6rem 1rem 0.75rem 1rem;
-}
-
-details[open] summary + * {
-  margin-top: 0;
-}
-
-details[open] summary {
-  margin-bottom: 0.5rem;
-  padding: 0;
-}
-
-details[open] > *:last-child {
-  margin-bottom: 0;
-}
-
-/* Format tables */
-table {
-  border-collapse: collapse;
-  width: 100%;
-  margin: 1.5rem 0;
-}
-
-td,
-th {
-  border: 1px solid var(--border);
-  text-align: left;
-  padding: 0.5rem;
-}
-
-th {
-  background: var(--accent-bg);
-  font-weight: bold;
-}
-
-tr:nth-child(even) {
-  /* Set every other cell slightly darker. Improves readability. */
-  background: var(--accent-bg);
-}
-
-table caption {
-  font-weight: bold;
-  margin-bottom: 0.5rem;
-}
-
-/* Format forms */
-textarea,
-select,
-input {
-  font-size: inherit;
-  font-family: inherit;
-  padding: 0.5rem;
-  margin-bottom: 0.5rem;
-  color: var(--text);
-  background: var(--bg);
-  border: 1px solid var(--border);
-  border-radius: 5px;
-  box-shadow: none;
-  box-sizing: border-box;
-  width: 60%;
-  -moz-appearance: none;
-  -webkit-appearance: none;
-  appearance: none;
-}
-
-/* Add arrow to drop-down */
-select {
-  background-image: linear-gradient(45deg, transparent 49%, var(--text) 51%),
-    linear-gradient(135deg, var(--text) 51%, transparent 49%);
-  background-position: calc(100% - 20px), calc(100% - 15px);
-  background-size: 5px 5px, 5px 5px;
-  background-repeat: no-repeat;
-}
-
-select[multiple] {
-  background-image: none !important;
-}
-
-/* checkbox and radio button style */
-input[type="checkbox"],
-input[type="radio"] {
-  vertical-align: bottom;
-  position: relative;
-}
-
-input[type="radio"] {
-  border-radius: 100%;
-}
-
-input[type="checkbox"]:checked,
-input[type="radio"]:checked {
-  background: var(--accent);
-}
-
-input[type="checkbox"]:checked::after {
-  /* Creates a rectangle with colored right and bottom borders which is rotated to look like a check mark */
-  content: " ";
-  width: 0.1em;
-  height: 0.25em;
-  border-radius: 0;
-  position: absolute;
-  top: 0.05em;
-  left: 0.18em;
-  background: transparent;
-  border-right: solid var(--bg) 0.08em;
-  border-bottom: solid var(--bg) 0.08em;
-  font-size: 1.8em;
-  transform: rotate(45deg);
-}
-input[type="radio"]:checked::after {
-  /* creates a colored circle for the checked radio button  */
-  content: " ";
-  width: 0.25em;
-  height: 0.25em;
-  border-radius: 100%;
-  position: absolute;
-  top: 0.125em;
-  background: var(--bg);
-  left: 0.125em;
-  font-size: 32px;
-}
-
-/* Make the textarea wider than other inputs */
-textarea {
-  width: 80%;
-}
-
-/* Makes input fields wider on smaller screens */
-@media only screen and (max-width: 720px) {
-  textarea,
-  select,
-  input {
-    width: 100%;
-  }
-}
-
-/* Ensures the checkbox and radio inputs do not have a set width like other input fields */
-input[type="checkbox"],
-input[type="radio"] {
-  width: auto;
-}
-
-/* do not show border around file selector button */
-input[type="file"] {
-  border: 0;
-}
-
-/* Misc body elements */
-hr {
-  color: var(--border);
-  border-top: 1px;
-  margin: 1rem auto;
-}
-
-mark {
-  padding: 2px 5px;
-  border-radius: 4px;
-  background: var(--marked);
-}
-
-main img,
-main video {
-  max-width: 100%;
-  height: auto;
-  border-radius: 5px;
-}
-
-figure {
-  margin: 0;
-  text-align: center;
-}
-
-figcaption {
-  font-size: 0.9rem;
-  color: var(--text-light);
-  margin-bottom: 1rem;
-}
-
-blockquote {
-  margin: 2rem 0 2rem 2rem;
-  padding: 0.4rem 0.8rem;
-  border-left: 0.35rem solid var(--accent);
-  color: var(--text-light);
-  font-style: italic;
-}
-
-cite {
-  font-size: 0.9rem;
-  color: var(--text-light);
-  font-style: normal;
-}
-
-/* Use mono font for code elements */
-code,
-pre,
-pre span,
-kbd,
-samp {
-  font-family: var(--mono-font);
-  color: var(--code);
-}
-
-kbd {
-  color: var(--preformatted);
-  border: 1px solid var(--preformatted);
-  border-bottom: 3px solid var(--preformatted);
-  border-radius: 5px;
-  padding: 0.1rem 0.4rem;
-}
-
-pre {
-  padding: 1rem 1.4rem;
-  max-width: 100%;
-  overflow: auto;
-  color: var(--preformatted);
-  background: var(--accent-bg);
-  border: 1px solid var(--border);
-  border-radius: 5px;
-}
-
-/* Fix embedded code within pre */
-pre code {
-  color: var(--preformatted);
-  background: none;
-  margin: 0;
-  padding: 0;
-}
diff --git a/measurer/templates/base.html b/measurer/templates/base.html
deleted file mode 100644
index 1ecc3c7eaec3e69a93bd82aa2c7b1c027e562cc5..0000000000000000000000000000000000000000
--- a/measurer/templates/base.html
+++ /dev/null
@@ -1,11 +0,0 @@
-<!DOCTYPE html>
-<html>
-	<head>
-		<title>{% block current_page %}{% endblock %}</title>
-		<meta charset="utf-8">
-		<link rel="stylesheet" href="{{ url_for('static', filename='simple.css') }}">
-	</head>
-	<body>
-		{% block content %}{% endblock %}
-	</body>
-</html>
diff --git a/measurer/templates/racer_add.html b/measurer/templates/racer_add.html
deleted file mode 100644
index d2b847cf5fb21ff95584b2bd561e4222a8b68bea..0000000000000000000000000000000000000000
--- a/measurer/templates/racer_add.html
+++ /dev/null
@@ -1,26 +0,0 @@
-{% extends 'base.html' %}
-
-{% block current_page %}{{ gettext("Add a racer") }}{% endblock %}
-
-{% block content %}<header>
-	<h1>{{ gettext("Add a racer") }}</h1>
-</header>
-<main>
-	<form method="POST">
-		{{ form.hidden_tag() }}
-		
-		<div>
-			{{ form.number(placeholder=form.number.label.text) }}
-		</div>
-		
-		<div>
-			{{ form.name(placeholder=form.name.label.text) }}
-		</div>
-		
-		<div>
-			{{ form.time(placeholder=form.time.label.text) }}
-		</div>
-		
-		<input type="submit" value="{{ gettext('Add racer') }}">
-	</form>
-</main>{% endblock %}
diff --git a/measurer/templates/racer_edit.html b/measurer/templates/racer_edit.html
deleted file mode 100644
index d155bdf4832441601ae7dea0083b23d8bd679e90..0000000000000000000000000000000000000000
--- a/measurer/templates/racer_edit.html
+++ /dev/null
@@ -1,26 +0,0 @@
-{% extends 'base.html' %}
-
-{% block current_page %}{{ gettext("Edit racer %s").format(racer.number) }}{% endblock %}
-
-{% block content %}<header>
-	<h1>{{ gettext("Edit racer %s").format(racer.number) }}</h1>
-</header>
-<main>
-	<form method="POST">
-		{{ form.hidden_tag() }}
-		
-		<div>
-			{{ form.number(placeholder=form.number.label.text, value=racer.number) }}
-		</div>
-		
-		<div>
-			{{ form.name(placeholder=form.name.label.text, value=racer.name) }}
-		</div>
-		
-		<div>
-			{{ form.time(placeholder=form.time.label.text, value=racer.time.total_seconds()|int) }}
-		</div>
-		
-		<input type="submit" value="{{ gettext('Update racer') }}">
-	</form>
-</main>{% endblock %}
diff --git a/measurer/templates/racer_list.html b/measurer/templates/racer_list.html
deleted file mode 100644
index 54194b25931af53f1d560a806b5ebb4d5abc927a..0000000000000000000000000000000000000000
--- a/measurer/templates/racer_list.html
+++ /dev/null
@@ -1,46 +0,0 @@
-{% extends 'base.html' %}
-
-{% block current_page %}{{ gettext("Racers") }}{% endblock %}
-
-{% block content %}<header>
-	<h1>{{ gettext("Racers") }}</h1>
-</header>
-<main>
-	<a href="{{ url_for('racer.add') }}">{{ gettext("Add a racer") }}</a>
-
-	<table>
-		<tr>
-			<th>{{ gettext("Number") }}</th>
-			<th>{{ gettext("Name") }}</th>
-			<th>{{ gettext("Time") }}</th>
-		</tr>
-		{% for racer in racers %}
-			<tr>
-				<td>
-					<a href="{{ url_for('racer.view', id_=racer.id) }}">{{ racer.number }}</a>
-				</td>
-				<td>{{ racer.name }}</td>
-				<td>{{ racer.time }}</td>
-			</tr>
-		{% endfor %}
-	</table>
-	
-	<form method="GET">
-		<b>{{ gettext("Sort") }}</b>
-
-		<hr>
-
-		<select name="order-by">
-			<option value="number" {% if order_by == 'number' %}selected{% endif %}>{{ gettext("Number") }}</option>
-			<option value="name" {% if order_by == 'name' %}selected{% endif %}>{{ gettext("Name") }}</option>
-			<option value="time" {% if order_by == 'time' %}selected{% endif %}>{{ gettext("Time") }}</option>
-		</select>
-
-		<select name="order-type">
-			<option value="asc" {% if order_type == 'asc' %}selected{% endif %}>{{ gettext("Ascending") }}</option>
-			<option value="desc" {% if order_type == 'desc' %}selected{% endif %}>{{ gettext("Descending") }}</option>
-		</select>
-
-		<input type="submit" value="Sort, without JS!">
-	</form>
-</main>{% endblock %}
diff --git a/measurer/templates/racer_view.html b/measurer/templates/racer_view.html
deleted file mode 100644
index 3ea1427b9404c18b0b845f293947e7a6f8f954b8..0000000000000000000000000000000000000000
--- a/measurer/templates/racer_view.html
+++ /dev/null
@@ -1,23 +0,0 @@
-{% extends 'base.html' %}
-
-{% block current_page %}{{ racer.name }}{% endblock %}
-
-{% block content %}<header>
-	<h1>{{ racer.name }}</h1>
-</header>
-<main>
-	<a href="{{ url_for('racer.list_') }}">{{ gettext("Back to listing") }}</a>
-
-	<section>
-		<p>
-			{{ gettext("Number: ") }}{{ racer.number }}<br>
-			{{ gettext("Time: ") }}{{ racer.time }}
-		</p>
-
-		<section>
-			<a href="{{ url_for('racer.delete', id_=racer.id) }}">{{ gettext("Delete this racer") }}</a>
-			{{ gettext(" or ") }}
-			<a href="{{ url_for('racer.edit', id_=racer.id) }}">{{ gettext("Update information") }}</a>
-		</section>
-	</section>
-</main>{% endblock %}
diff --git a/measurer/types/__init__.py b/measurer/types/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..2e2c6ab95e0342627148a3897bd399e2a2c5bae5
--- /dev/null
+++ b/measurer/types/__init__.py
@@ -0,0 +1,21 @@
+"""Abstract classes for type hints."""
+
+from __future__ import annotations
+
+import abc
+import typing
+
+__all__ = ["SupportsLength"]
+__version__ = "1.0.1"
+
+
+@typing.runtime_checkable
+class SupportsLength(typing.Protocol):
+	"""An abstract class which supports the ``__len__`` method.
+	Adapted from the
+	`typing <https://github.com/python/cpython/blob/3.8/Lib/typing.py>`_ library.
+	"""
+
+	@abc.abstractmethod
+	def __len__(self) -> int:
+		pass
diff --git a/measurer/utils/__pycache__/__init__.cpython-310.pyc b/measurer/utils/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..d2af4122126573faf2d610d4d3c51e7510a2392c
Binary files /dev/null and b/measurer/utils/__pycache__/__init__.cpython-310.pyc differ
diff --git a/measurer/validators/__init__.py b/measurer/validators/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..492c9f997291d44de4b690606c9216ef2ce4e7c5
--- /dev/null
+++ b/measurer/validators/__init__.py
@@ -0,0 +1,338 @@
+"""Validation using the Cerberus library. To extend, see its documentation
+`here <https://docs.python-cerberus.org/en/stable/index.html>`_.
+"""
+
+from __future__ import annotations
+
+import base64
+import datetime
+import functools
+import re
+import typing
+import uuid
+
+import cerberus
+import flask
+import werkzeug.exceptions
+
+import validators
+
+from .. import types
+
+__all__ = ["APIValidator", "validate_json"]
+__version__ = "1.12.0"
+
+
+class APIValidator(cerberus.Validator):
+	"""Cerberus validator with extended functionality."""
+
+	def _check_with_is_base64(
+		self: APIValidator,
+		field: str,
+		value: typing.Union[None, str]
+	) -> None:
+		"""Checks whether or not ``value`` is a valid base64 string, without
+		coercing it into the bytes it represents. This is useful where external
+		tools require untouched base64 strings.
+
+		:param field: The current field.
+		:param value: The current field's value. If :data:`None`, this means the
+			field is nullable. Nothing happens in this case.
+		"""
+
+		if value is None:
+			return
+
+		try:
+			base64.b64decode(value, validate=True)
+		except ValueError:
+			self._error(
+				field,
+				"must be a valid base64 string"
+			)
+
+	def _check_with_is_email(
+		self: APIValidator,
+		field: str,
+		value: typing.Union[None, str]
+	) -> None:
+		"""Checks whether or not ``value`` is a valid email address.
+
+		:param field: The current field.
+		:param value: The current field's value. If :data:`None`, this means the
+			field is nullable. Nothing happens in this case.
+		"""
+
+		if value is None:
+			return
+
+		if not validators.email(value):
+			self._error(
+				field,
+				"must be a valid email address"
+			)
+
+	def _check_with_is_valid_regex(
+		self: APIValidator,
+		field: str,
+		value: typing.Union[None, str]
+	) -> None:
+		"""Checks whether or not ``value`` is a valid regular expression.
+
+		:param field: The current field.
+		:param value: The current field's value. If :data:`None`, this means the
+			field is nullable. Nothing happens in this case.
+		"""
+
+		if value is None:
+			return
+
+		try:
+			re.compile(value)
+		except re.error:
+			self._error(field, "must be a valid regular expression")
+
+	def _check_with_is_public_url(
+		self: APIValidator,
+		field: str,
+		value: typing.Union[None, str]
+	) -> None:
+		"""Checks that ``value`` is a valid URL. If
+		:attr:`debug <heiwa.ConfiguredLockFlask.debug>` mode is not enabled, the
+		URL must also correspond to a public resource.
+
+		:param field: The current field.
+		:param value: The current field's value. If :data:`None`, this means the
+			field is nullable. Nothing happens in this case.
+		"""
+
+		if value is None:
+			return
+
+		if not validators.url(
+			value,
+			public=(not flask.current_app.debug)
+		):
+			self._error(
+				field,
+				"must be a valid public URL"
+			)
+
+	def _check_with_has_no_duplicates(
+		self: APIValidator,
+		field: str,
+		value: typing.Union[None, typing.List[typing.Any]]
+	) -> None:
+		"""Checks that the list in ``value`` contains no duplicate items.
+
+		:param field: The current field.
+		:param value: The current field's value. If :data:`None`, this means the
+			field is nullable. Nothing happens in this case.
+		"""
+
+		if value is None:
+			return
+
+		if len(value) != len(set(value)):
+			self._error(
+				field,
+				"must contain no duplicate items"
+			)
+
+	def _normalize_coerce_convert_to_uuid(
+		self: APIValidator,
+		value: typing.Union[None, str]
+	) -> typing.Union[None, uuid.UUID]:
+		"""Converts the ``value`` to an :class:`UUID <uuid.UUID>`.
+
+		:param value: The current field's value. If :data:`None`, this means the
+			field is nullable. Nothing happens in this case.
+
+		:returns: The converted UUID.
+		"""
+
+		if value is None:
+			return None
+
+		return uuid.UUID(value)
+
+	def _normalize_coerce_convert_to_datetime(
+		self: APIValidator,
+		value: typing.Union[None, str]
+	) -> typing.Union[None, datetime.datetime]:
+		"""As long as ``value`` is a string formatted as per
+		`ISO-8601 <https://wikiless.org/wiki/ISO_8601>`_, it's converted to a
+		:class:`datetime <datetime.datetime>` object.
+
+		:raises ValueError: Raised when the datetime does not have a specified
+			time zone.
+
+		:param value: The current field's value. If :data:`None`, this means the
+			field is nullable. Nothing happens in this case.
+
+		:returns: The converted date and time.
+		"""
+
+		if value is None:
+			return None
+
+		result = datetime.datetime.fromisoformat(value)
+
+		if result.tzinfo is None:
+			raise ValueError("must have a timezone")
+
+		return result
+
+	def _normalize_coerce_decode_base64(
+		self: APIValidator,
+		value: typing.Union[None, str]
+	) -> typing.Union[None, bytes]:
+		"""Converts the base64-encoded ``value`` to the bytes it represents.
+
+		:param value: The current field's value. If :data:`None`, this means the
+			field is nullable. Nothing happens in this case.
+
+		:returns: The decoded bytes.
+		"""
+
+		if value is None:
+			return None
+
+		return base64.b64decode(
+			value,
+			validate=True
+		)
+
+	def _validate_length_divisible_by(
+		self: APIValidator,
+		divider: int,
+		field: str,
+		value: typing.Union[None, types.SupportsLength]
+	) -> None:
+		"""Checks whether or not the length of ``value`` is divisible by ``divider``.
+		If not, an error is raised.
+
+		:param divider: The required divider.
+		:param field: The current field.
+		:param value: The current field's value, which must support the ``__len__``
+			method. If :data:`None`, this means the field is nullable. Nothing
+			happens in this case.
+
+		The rule's arguments are validated against this schema:
+		{
+			'type': 'integer'
+		}
+		"""
+
+		if value is None:
+			return
+
+		if len(value) % divider != 0:
+			self._error(
+				field,
+				f"length must be divisible by {divider}"
+			)
+
+	def _validate_not_null_dependencies(
+		self: APIValidator,
+		dependencies: typing.Union[
+			typing.Iterable[str],
+			str
+		],
+		field: str,
+		value: typing.Any
+	) -> None:
+		"""The same as the ``dependencies`` rule (see `here https://docs.pytho`
+		n-cerberus.org/en/stable/validation-rules.html#dependencies`_), but the
+		specified elements must not be :data:`None`, instead of just being set.
+		The current field must also not be :data:`None`, otherwise nothing
+		happens.
+
+		:param dependencies: The list of dependencies. Either a string containing
+			a single field, or an iterable containing multiple. This is done
+			mostly to preserve the original ``dependencies`` rule's behaviour.
+		:param field: The current field.
+		:param value: The current field's value.
+
+		The rule's arguments are validated against this schema:
+		{
+			'type': ['string', 'list']
+		}
+		"""
+
+		if value is None:
+			return
+
+		if isinstance(dependencies, str):
+			dependency_list = (dependencies,)
+		else:
+			dependency_list = dependencies
+
+		for dependency in dependency_list:
+			found_name, found_value = self._lookup_field(dependency)
+
+			if found_value is not None:
+				continue
+
+			if found_name is None:
+				found_name = dependency
+
+			self._error(
+				field,
+				f"depends on {found_name} being set and not null"
+			)
+
+	types_mapping = cerberus.Validator.types_mapping.copy()
+	types_mapping["uuid"] = cerberus.TypeDefinition(
+		"uuid",
+		(uuid.UUID,),
+		()
+	)
+
+
+def validate_json(
+	schema: typing.Dict[
+		str,
+		typing.Union[
+			str,
+			typing.Dict
+		]
+	],
+	*args,
+	**kwargs
+) -> typing.Callable:
+	"""Checks JSON data sent in the :attr:`flask.request` against a Cerberus
+	schema. If the validation was sucessful, sets :attr:`flask.g.json` to the
+	document the validator outputs - in case there were any coercions or other
+	modiciations done by it.
+
+	:param schema: The cerberus schema.
+	
+	:raises werkzeug.exceptions.BadRequest: Raised when the JSON is invalid.
+	"""
+
+	def wrapper(
+		func: typing.Callable
+	) -> typing.Callable:
+		@functools.wraps(func)
+		def decorator(*wrapped_args, **wrapped_kwargs) -> typing.Any:
+			if flask.request.json is None:
+				raise werkzeug.exceptions.BadRequest
+
+			if not isinstance(flask.request.json, dict):
+				raise werkzeug.exceptions.BadRequest
+
+			validator = APIValidator(
+				schema,
+				*args,
+				**kwargs
+			)
+
+			if not validator.validate(flask.request.json):
+				raise werkzeug.exceptions.BadRequest(validator.errors)
+
+			flask.g.json = validator.document
+
+			return func(*wrapped_args, **wrapped_kwargs)
+		return decorator
+	return wrapper
diff --git a/measurer/views/measurer.py b/measurer/views/measurer.py
new file mode 100644
index 0000000000000000000000000000000000000000..f14734ed7619527951200e1c106d2f6c4c9af876
--- /dev/null
+++ b/measurer/views/measurer.py
@@ -0,0 +1,63 @@
+import http.client
+import typing
+
+import flask
+import sqlalchemy
+import werkzeug.exceptions
+
+from .. import database, utils, validators
+
+
+measurer_blueprint = flask.Blueprint(
+	"measurer",
+	__name__
+)
+
+
+@measurer_blueprint.route("/", methods=["POST"])
+@validators.validate_json({
+	"point": {
+		"type": "string",
+		"allowed": flask.current_app.config["POINTS"],
+		"nullable": False
+	}
+})
+def add_vote() -> typing.Union[flask.Response, int]:
+	vote_exists = flask.g.sa_session.execute(
+		sqlalchemy.select(database.Vote).
+		where(
+			sqlalchemy.and_(
+				database.Vote.point == flask.g.json["point"],
+				database.Vote.identifier == utils.get_ip_hash()
+			)
+		).
+		exists().
+		select()
+	).scalars().one()
+
+	if vote_exists:
+		raise werkzeug.exceptions.Forbidden
+
+	new_vote = database.Vote(point=flask.g.json["point"])
+	flask.g.sa_session.add(new_vote)
+	flask.g.sa_session.commit()
+
+	return flask.jsonify(None), http.client.NO_CONTENT
+
+
+@measurer_blueprint.route("/", methods=["GET"])
+def list_votes() -> typing.Union[flask.Response, int]:
+	votes = flask.g.sa_session.execute(
+		sqlalchemy.select(
+			database.Vote.point,
+			sqlalchemy.func.count(database.Vote.point)
+		).
+		group_by(database.Vote.point)
+	).all()
+
+	result = {}
+
+	for vote in votes:
+		result[vote[0]] = vote[1]
+
+	return flask.jsonify(result), http.client.OK
diff --git a/measurer/views/racer.py b/measurer/views/racer.py
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000