diff --git a/.env.example b/.env.example
index 57a0ddb3765f70fb5d7a85a8928ff3a11e2ccc79..3a9420c896ff0160819a0c813e85399b2e42bbb2 100644
--- a/.env.example
+++ b/.env.example
@@ -1,2 +1,21 @@
 APP_NAME=
-PORT=
\ No newline at end of file
+APP_KEY=
+PORT=8000
+
+WEB_URL=
+
+DATABASE_NAME=trash
+DATABASE_USER=trash
+DATABASE_PASS=trash
+
+MAIL_DRIVER=log
+MAIL_FROM_ADDRESS=
+MAIL_FROM_NAME=
+
+SENDGRID_KEY=
+
+GCS_BUCKET=
+
+SENTRY_DSN=
+
+SLACK_WEBHOOK=
diff --git a/package-lock.json b/package-lock.json
index d14b10d2d4dc0215de345a2489521f9c1e24d81e..9f229114c5047aefebecbee4667e16b6ee04580f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
 {
   "name": "jetsam-server",
-  "version": "1.1.0",
+  "version": "1.1.1",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
@@ -540,6 +540,34 @@
       "resolved": "https://registry.npmjs.org/@koa/multer/-/multer-2.0.2.tgz",
       "integrity": "sha512-cWZUbpRNcLjLWDQ6QTWMX3ucXS+xHpJh7ngrBnsywhL7XygU5M090dDeiC9Sq1jM7IwKn9WkFZA1LyPU0q89qA=="
     },
+    "@sendgrid/client": {
+      "version": "6.5.3",
+      "resolved": "https://registry.npmjs.org/@sendgrid/client/-/client-6.5.3.tgz",
+      "integrity": "sha512-+K4yTMSNChfwKuuMGpnK1Xz7SnBoh3VDT8sILVwSMJRH3s18mOf5Bv/xbAxawqX4Wz50rlSrpbA5A3FwiSDzJA==",
+      "requires": {
+        "@sendgrid/helpers": "^6.5.3",
+        "@types/request": "^2.48.4",
+        "request": "^2.88.0"
+      }
+    },
+    "@sendgrid/helpers": {
+      "version": "6.5.3",
+      "resolved": "https://registry.npmjs.org/@sendgrid/helpers/-/helpers-6.5.3.tgz",
+      "integrity": "sha512-Cr5lV8H8STg8bzdzU5pEctI/SDQ3TCLiq72Ao2r3tGyusIxJ00C7CrXddhjEdYBsHvJZLaBTfG04yp8303em6w==",
+      "requires": {
+        "chalk": "^2.0.1",
+        "deepmerge": "^4.2.2"
+      }
+    },
+    "@sendgrid/mail": {
+      "version": "6.5.4",
+      "resolved": "https://registry.npmjs.org/@sendgrid/mail/-/mail-6.5.4.tgz",
+      "integrity": "sha512-oXQc4rseywV9j2+dXSgZfCLU+8Oj1AI12x071qVBtaKdYUwJI4sgbHvNiowAG/pL+FaWX339wcXH8F28uPzgew==",
+      "requires": {
+        "@sendgrid/client": "^6.5.3",
+        "@sendgrid/helpers": "^6.5.3"
+      }
+    },
     "@sentry/apm": {
       "version": "5.12.2",
       "resolved": "https://registry.npmjs.org/@sentry/apm/-/apm-5.12.2.tgz",
@@ -696,6 +724,11 @@
         "@babel/types": "^7.3.0"
       }
     },
+    "@types/caseless": {
+      "version": "0.12.2",
+      "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz",
+      "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w=="
+    },
     "@types/istanbul-lib-coverage": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz",
@@ -726,12 +759,40 @@
       "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.8.tgz",
       "integrity": "sha512-XLla8N+iyfjvsa0KKV+BP/iGSoTmwxsu5Ci5sM33z9TjohF72DEz95iNvD6pPmemvbQgxAv/909G73gUn8QR7w=="
     },
+    "@types/request": {
+      "version": "2.48.4",
+      "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.4.tgz",
+      "integrity": "sha512-W1t1MTKYR8PxICH+A4HgEIPuAC3sbljoEVfyZbeFJJDbr30guDspJri2XOaM2E+Un7ZjrihaDi7cf6fPa2tbgw==",
+      "requires": {
+        "@types/caseless": "*",
+        "@types/node": "*",
+        "@types/tough-cookie": "*",
+        "form-data": "^2.5.0"
+      },
+      "dependencies": {
+        "form-data": {
+          "version": "2.5.1",
+          "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz",
+          "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==",
+          "requires": {
+            "asynckit": "^0.4.0",
+            "combined-stream": "^1.0.6",
+            "mime-types": "^2.1.12"
+          }
+        }
+      }
+    },
     "@types/stack-utils": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz",
       "integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==",
       "dev": true
     },
+    "@types/tough-cookie": {
+      "version": "2.3.6",
+      "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-2.3.6.tgz",
+      "integrity": "sha512-wHNBMnkoEBiRAd3s8KTKwIuO9biFtTf0LehITzBhSco+HQI0xkXZbLOD55SW3Aqw3oUkHstkm5SPv58yaAdFPQ=="
+    },
     "@types/yargs": {
       "version": "13.0.2",
       "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.2.tgz",
@@ -824,7 +885,6 @@
       "version": "6.10.2",
       "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz",
       "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==",
-      "dev": true,
       "requires": {
         "fast-deep-equal": "^2.0.1",
         "fast-json-stable-stringify": "^2.0.0",
@@ -956,7 +1016,6 @@
       "version": "0.2.4",
       "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
       "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==",
-      "dev": true,
       "requires": {
         "safer-buffer": "~2.1.0"
       }
@@ -964,8 +1023,7 @@
     "assert-plus": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
-      "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=",
-      "dev": true
+      "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
     },
     "assign-symbols": {
       "version": "1.0.0",
@@ -994,8 +1052,7 @@
     "asynckit": {
       "version": "0.4.0",
       "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
-      "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
-      "dev": true
+      "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
     },
     "atob": {
       "version": "2.1.2",
@@ -1006,14 +1063,12 @@
     "aws-sign2": {
       "version": "0.7.0",
       "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
-      "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=",
-      "dev": true
+      "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg="
     },
     "aws4": {
       "version": "1.8.0",
       "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz",
-      "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==",
-      "dev": true
+      "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ=="
     },
     "babel-jest": {
       "version": "24.9.0",
@@ -1148,7 +1203,6 @@
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
       "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=",
-      "dev": true,
       "requires": {
         "tweetnacl": "^0.14.3"
       }
@@ -1394,8 +1448,7 @@
     "caseless": {
       "version": "0.12.0",
       "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
-      "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=",
-      "dev": true
+      "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw="
     },
     "chalk": {
       "version": "2.4.2",
@@ -1608,7 +1661,6 @@
       "version": "1.0.8",
       "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
       "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
-      "dev": true,
       "requires": {
         "delayed-stream": "~1.0.0"
       }
@@ -1824,7 +1876,6 @@
       "version": "1.14.1",
       "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
       "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
-      "dev": true,
       "requires": {
         "assert-plus": "^1.0.0"
       }
@@ -1895,6 +1946,11 @@
       "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=",
       "dev": true
     },
+    "deepmerge": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
+      "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg=="
+    },
     "defer-class": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/defer-class/-/defer-class-1.0.1.tgz",
@@ -1953,8 +2009,7 @@
     "delayed-stream": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
-      "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
-      "dev": true
+      "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
     },
     "delegates": {
       "version": "1.0.0",
@@ -2068,7 +2123,6 @@
       "version": "0.1.2",
       "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
       "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=",
-      "dev": true,
       "requires": {
         "jsbn": "~0.1.0",
         "safer-buffer": "^2.1.0"
@@ -2611,20 +2665,17 @@
     "extsprintf": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
-      "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=",
-      "dev": true
+      "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU="
     },
     "fast-deep-equal": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
-      "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=",
-      "dev": true
+      "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk="
     },
     "fast-json-stable-stringify": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
-      "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=",
-      "dev": true
+      "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I="
     },
     "fast-levenshtein": {
       "version": "2.0.6",
@@ -2747,14 +2798,12 @@
     "forever-agent": {
       "version": "0.6.1",
       "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
-      "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=",
-      "dev": true
+      "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE="
     },
     "form-data": {
       "version": "2.3.3",
       "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
       "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
-      "dev": true,
       "requires": {
         "asynckit": "^0.4.0",
         "combined-stream": "^1.0.6",
@@ -3532,7 +3581,6 @@
       "version": "0.1.7",
       "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
       "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=",
-      "dev": true,
       "requires": {
         "assert-plus": "^1.0.0"
       }
@@ -3695,14 +3743,12 @@
     "har-schema": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
-      "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=",
-      "dev": true
+      "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI="
     },
     "har-validator": {
       "version": "5.1.3",
       "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz",
       "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==",
-      "dev": true,
       "requires": {
         "ajv": "^6.5.5",
         "har-schema": "^2.0.0"
@@ -3828,7 +3874,6 @@
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
       "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=",
-      "dev": true,
       "requires": {
         "assert-plus": "^1.0.0",
         "jsprim": "^1.2.2",
@@ -4944,8 +4989,7 @@
     "jsbn": {
       "version": "0.1.1",
       "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
-      "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=",
-      "dev": true
+      "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM="
     },
     "jsdom": {
       "version": "11.12.0",
@@ -5004,14 +5048,12 @@
     "json-schema": {
       "version": "0.2.3",
       "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
-      "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=",
-      "dev": true
+      "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM="
     },
     "json-schema-traverse": {
       "version": "0.4.1",
       "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
-      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
-      "dev": true
+      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
     },
     "json-stable-stringify-without-jsonify": {
       "version": "1.0.1",
@@ -5022,8 +5064,7 @@
     "json-stringify-safe": {
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
-      "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=",
-      "dev": true
+      "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
     },
     "json5": {
       "version": "2.1.0",
@@ -5055,7 +5096,6 @@
       "version": "1.4.1",
       "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
       "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=",
-      "dev": true,
       "requires": {
         "assert-plus": "1.0.0",
         "extsprintf": "1.3.0",
@@ -5757,8 +5797,7 @@
     "oauth-sign": {
       "version": "0.9.0",
       "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
-      "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==",
-      "dev": true
+      "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ=="
     },
     "oauth2-server": {
       "version": "3.0.1",
@@ -6081,8 +6120,7 @@
     "performance-now": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
-      "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=",
-      "dev": true
+      "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns="
     },
     "pg": {
       "version": "7.12.1",
@@ -6276,8 +6314,7 @@
     "psl": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/psl/-/psl-1.3.0.tgz",
-      "integrity": "sha512-avHdspHO+9rQTLbv1RO+MPYeP/SzsCoxofjVnHanETfQhTJrmB0HlDoW+EiN/R+C0BZ+gERab9NY0lPN2TxNag==",
-      "dev": true
+      "integrity": "sha512-avHdspHO+9rQTLbv1RO+MPYeP/SzsCoxofjVnHanETfQhTJrmB0HlDoW+EiN/R+C0BZ+gERab9NY0lPN2TxNag=="
     },
     "pstree.remy": {
       "version": "1.1.7",
@@ -6330,8 +6367,7 @@
     "punycode": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
-      "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
-      "dev": true
+      "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
     },
     "qs": {
       "version": "6.5.2",
@@ -6507,7 +6543,6 @@
       "version": "2.88.0",
       "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz",
       "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==",
-      "dev": true,
       "requires": {
         "aws-sign2": "~0.7.0",
         "aws4": "^1.8.0",
@@ -6534,14 +6569,12 @@
         "punycode": {
           "version": "1.4.1",
           "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
-          "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
-          "dev": true
+          "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4="
         },
         "tough-cookie": {
           "version": "2.4.3",
           "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
           "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==",
-          "dev": true,
           "requires": {
             "psl": "^1.1.24",
             "punycode": "^1.4.1"
@@ -7167,7 +7200,6 @@
       "version": "1.16.1",
       "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz",
       "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==",
-      "dev": true,
       "requires": {
         "asn1": "~0.2.3",
         "assert-plus": "^1.0.0",
@@ -7641,7 +7673,6 @@
       "version": "0.6.0",
       "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
       "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
-      "dev": true,
       "requires": {
         "safe-buffer": "^5.0.1"
       }
@@ -7649,8 +7680,7 @@
     "tweetnacl": {
       "version": "0.14.5",
       "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
-      "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
-      "dev": true
+      "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q="
     },
     "type": {
       "version": "1.0.3",
@@ -7871,7 +7901,6 @@
       "version": "4.2.2",
       "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
       "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
-      "dev": true,
       "requires": {
         "punycode": "^2.1.0"
       }
@@ -7952,7 +7981,6 @@
       "version": "1.10.0",
       "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
       "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
-      "dev": true,
       "requires": {
         "assert-plus": "^1.0.0",
         "core-util-is": "1.0.2",
diff --git a/package.json b/package.json
index e17e724c3571cc2dcec568d8b709e8036d8443a7..f5fd0bba76ce7f469fce65e94dffb722fac7ab6f 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "jetsam-server",
-	"version": "1.1.1",
+	"version": "1.2.0",
 	"description": "",
 	"main": "index.js",
 	"scripts": {
@@ -16,6 +16,7 @@
 	"dependencies": {
 		"@google-cloud/storage": "^4.1.3",
 		"@koa/multer": "^2.0.2",
+		"@sendgrid/mail": "^6.5.4",
 		"@sentry/node": "^5.12.2",
 		"defer-class": "^1.0.1",
 		"dotenv": "^8.1.0",
diff --git a/public/css/main.css b/public/css/main.css
new file mode 100644
index 0000000000000000000000000000000000000000..ed2aaa02d88a257932f1ef967dcba45752b00a46
--- /dev/null
+++ b/public/css/main.css
@@ -0,0 +1,86 @@
+* {
+	box-sizing: border-box;
+	position: relative;
+}
+.required {
+	color: red;
+}
+.logo {
+	border-radius: 10px;
+	box-shadow: 0px 10px 30px 0 #BBB;
+	margin: 0 5rem;
+}
+.header {
+	padding: 3rem;
+	display: flex;
+	justify-content: center;
+}
+.error-message {
+	padding: 1rem;
+	margin: 1rem 0;
+	text-align: center;
+	background-color: #FF4136;
+	border: 1px solid #FF2015;
+	color: white;
+	font-weight: bold;
+}
+button:not(:disabled).button-primary {
+	background-color: #FF4D75;
+	border-color: #FF4D75;
+	background: linear-gradient(to right, #FF7BAC, #FF1E3D);
+}
+button:not(:disabled).button-primary:hover {
+	background-color: #FF5E82;
+	border-color: #FF5E82;
+	background: linear-gradient(to right, #ff88b4, #ff3450);
+}
+button:disabled {
+	background-color: #777;
+	border-color: #777;
+}
+button:disabled:hover {
+	background-color: #777;
+	border-color: #777;
+}
+.flex-row {
+	padding: 2rem 0;
+	display: flex;
+	flex-direction: row;
+	align-items: center;
+}
+.flex-row h4 {
+	margin: 0;
+}
+.centered {
+	text-align: center;
+}
+a {
+	color: #FF5E82;
+}
+
+a:hover {
+	color: #ff3450;
+}
+
+.back-icon {
+	margin-right: 1rem;
+}
+@media screen and (max-width: 760px) {
+	.header {
+		flex-direction: column;
+		text-align: center;
+		align-items: center;
+	}
+	.logo {
+		margin-bottom: 5rem;
+	}
+}
+
+input[type=text]:disabled {
+	background: #FFF5F5;
+}
+
+input[type=text].error,
+input[type=password].error {
+	border-color: red;
+}
\ No newline at end of file
diff --git a/public/js/reset-password.js b/public/js/reset-password.js
new file mode 100644
index 0000000000000000000000000000000000000000..3adedf3dcc142b97dfe67aa8966f871d6063644e
--- /dev/null
+++ b/public/js/reset-password.js
@@ -0,0 +1,71 @@
+(function () {
+	document.addEventListener('DOMContentLoaded', function() {
+		const form = document.getElementById('reset_password_form')
+		const tokenField = document.getElementById('reset_token')
+		const newField = document.getElementById('new_password')
+		const confirmField = document.getElementById('confirm_password')
+		const messageField = document.getElementById('formmessage')
+
+		function validateInput() {
+			const newValue = newField.value
+			const confirmValue = confirmField.value
+
+			if (newValue !== confirmValue) {
+				messageField.innerText = 'The passwords do not match'
+				confirmField.classList.toggle('error', true)
+				return false
+			} else {
+				messageField.innerText = ''
+				confirmField.classList.toggle('error', false)
+				return true
+			}
+		}
+
+		newField.addEventListener('input', validateInput)
+		confirmField.addEventListener('input', validateInput)
+
+		form.addEventListener('submit', async function (e) {
+			e.preventDefault()
+
+			if (!validateInput()) {
+				return
+			}
+
+			const payload = {
+				reset_token: tokenField.value,
+				new_password: newField.value,
+				confirm_password: confirmField.value,
+			}
+
+			const result = await fetch('/api/auth/reset-password', {
+				method: 'POST',
+				headers: {
+					'content-type': 'application/json',
+				},
+				body: JSON.stringify(payload),
+			})
+
+			if (!result.ok && result.status !== 400) {
+				messageField.innerText = 'There was a problem resetting your password. Please request a new password reset link and try again later.'
+				return
+			}
+
+			if (!result.ok && result.status === 400) {
+				const data = await result.clone().json()
+				const { error } = data
+				messageField.innerText = error.message
+				return
+			}
+
+			const parent = form.parentElement
+			parent.removeChild(form)
+			const success = document.createElement('div')
+			success.innerHTML = `<h3 class="centered">Password Reset Successful</h3>
+			<p class="centered">You successfully reset your password. You can log in to the Jetsam app with the password you just created. Now go and snap some pesky plastics!</p>
+			<div class="row centered">
+			\t<a href="https://jetsam.tech">Go to the Jetsam website</a>
+			</div>`
+			parent.appendChild(success)
+		})
+	})
+}())
\ No newline at end of file
diff --git a/src/app.js b/src/app.js
index 7da9984456a4e5663e5785963e9cfdd281c40ff3..ad107b6f8806350f67def75a9a70980139c66d7e 100644
--- a/src/app.js
+++ b/src/app.js
@@ -6,13 +6,14 @@ const routers = require('http/routes')
 const handlebars = require('./vendor/koa-handlebars')
 const { fs, config } = require('bootstrap')
 const { extractDevice } = require('./http/middleware/deviceProperties')
-
+const serve = require('koa-static')
 
 const app = new Koa()
 
 app.keys = [config('app.key')]
 
 app.use(logger())
+app.use(serve(fs.path('public')))
 app.use(body())
 app.use(session({
 	key: 'trash:sss',
diff --git a/src/config/app.js b/src/config/app.js
index e265bab63d3616057522dd84c9114185b8c78167..965bbc88c2dc8cb4748ca0b54078bed260bc59bc 100644
--- a/src/config/app.js
+++ b/src/config/app.js
@@ -5,4 +5,7 @@ module.exports = {
 	port: Number(env('PORT')) || 8000,
 	key: env('APP_KEY', () => require('core/utils/crypto').insecureHexString(32)),
 	dev: env('NODE_ENV', 'err') === 'development',
+	urls: {
+		web: env('WEB_URL', 'http://localhost'),
+	},
 }
diff --git a/src/config/mail.js b/src/config/mail.js
new file mode 100644
index 0000000000000000000000000000000000000000..a272a639a730fc89e3c3f34fd46041f77b7f636c
--- /dev/null
+++ b/src/config/mail.js
@@ -0,0 +1,26 @@
+const { env } = require('bootstrap')
+
+module.exports = {
+	driver: env('MAIL_DRIVER', 'log'),
+	log: {
+		templates: {
+			'reset-password': 'reset-password',
+		},
+	},
+	sendgrid: {
+		key: env('SENDGRID_KEY'),
+		opts: {
+			from: {
+				email: env('MAIL_FROM_ADDRESS'),
+				name: env('MAIL_FROM_NAME'),
+			},
+			replyTo: {
+				email: env('MAIL_FROM_ADDRESS'),
+				name: env('MAIL_FROM_NAME'),
+			},
+		},
+		templates: {
+			'reset-password': 'd-dd89d66ad75f40f5b3b0ed6849753cf7',
+		},
+	},
+}
diff --git a/src/database/models/User.js b/src/database/models/User.js
index 1834618c6940480949a7e8b90bf1ffed8d2102d0..11460d0523b20b629c9448f4d5df6b44c48e57c1 100644
--- a/src/database/models/User.js
+++ b/src/database/models/User.js
@@ -74,6 +74,24 @@ module.exports = (sequelize, DataTypes) => {
 		return await crypto.verify(this.password, password)
 	}
 
+	Model.prototype.generateResetToken = async function() {
+		const crypto = require('core/utils/crypto')
+		const moment = require('moment')
+
+		const id = this.id
+		const expires = moment.utc()
+			.add(1, 'hour')
+			.toISOString()
+
+		const token = await crypto.encrypt(JSON.stringify({ id, expires }))
+
+		this.reset_token = token
+
+		await this.save()
+
+		return token
+	}
+
 	Model.createSystemUser = async function() {
 		const crypto = require('core/utils/crypto')
 		const user = Model.build({
diff --git a/src/http/controllers/api/auth.js b/src/http/controllers/api/auth.js
index 84f27900270cdaca170013a1222c03ef2265e066..bd708c9f556e5923f1aa5ef3d456cbabac198b00 100644
--- a/src/http/controllers/api/auth.js
+++ b/src/http/controllers/api/auth.js
@@ -1,5 +1,10 @@
+const moment = require('moment')
+
 const { User } = require('database/models')
 const HttpError = require('core/errors/HttpError')
+const crypto = require('core/utils/crypto')
+const reporter = require('services/Reporter')
+const { config } = require('bootstrap')
 
 exports.register = async ctx => {
 	const { email, name, password, date_of_birth } = ctx.request.body
@@ -28,4 +33,134 @@ exports.login = async ctx => {
 	const token = await user.asToken()
 
 	ctx.body = { token }
+}
+
+exports.triggerPasswordReset = async ctx => {
+	const { email } = ctx.request.body
+	const user = await User.findOne({ where: { email } })
+	if (!user) {
+		throw new HttpError({ status: 404, title: 'No Such Email', description: 'The provided email address is not associated with an account' })
+	}
+
+	const token = await user.generateResetToken()
+
+	const name = user.name || 'Jetsam User (You haven\'t told us your name!)'
+	const reset_link = new URL(`/reset-password?token=${ token }`, config('app.urls.web'))
+
+	const { mail } = require('services')
+
+	try {
+		await mail.sendTemplate(email, 'Reset Your Jetsam password', config('mail.templates.reset-password'), {
+			name,
+			reset_link,
+		})
+	} catch (e) {
+		reporter.report(e)
+		console.log(e.response.body.errors)
+		throw new HttpError({ status: 500, title: 'Failed to send reset email', description: 'Could not send the password reset email' })
+	}
+
+	ctx.body = {
+		reset_token: token,
+	}
+}
+
+exports.handlePasswordReset = async ctx => {
+	const { reset_token, new_password, confirm_password } = ctx.request.body
+	let token = null
+
+	if (!reset_token) {
+		ctx.body = {
+			error: {
+				message: 'The reset token was missing',
+			},
+			params: {
+				new_password,
+				confirm_password,
+			},
+		}
+		ctx.status = 400
+		return
+	}
+
+	if (!new_password || !confirm_password) {
+		ctx.body = {
+			error: {
+				message: 'A new password and password confirmation must be supplied',
+			},
+			params: {
+				new_password,
+				confirm_password,
+			},
+		}
+		ctx.status = 400
+		return
+	}
+
+	if (new_password !== confirm_password) {
+		ctx.body = {
+			error: {
+				message: 'The new password must match the password confirmation',
+			},
+			params: {
+				new_password,
+				confirm_password,
+			},
+		}
+		ctx.status = 400
+		return
+	}
+
+	try {
+		token = JSON.parse(await crypto.decrypt(reset_token))
+	} catch (e) {
+		ctx.body = {
+			error: {
+				message: 'The reset token was invalid or expired',
+			},
+			params: {
+				new_password,
+				confirm_password,
+			},
+		}
+		ctx.status = 400
+		return
+	}
+
+	const expires = moment.utc(token.expires)
+	if (expires.isSameOrBefore(moment.utc())) {
+		ctx.body = {
+			error: {
+				message: 'The reset token was invalid or expired'
+			},
+			params: {
+				new_password,
+				confirm_password,
+			},
+		}
+		ctx.status = 400
+		return
+	}
+
+	const user = await User.findOne({ where: { reset_token, id: token.id } })
+	if (!user) {
+		ctx.body = {
+			error: {
+				message: 'The reset token was invalid or expired'
+			},
+			params: {
+				new_password,
+				confirm_password,
+			},
+		}
+		ctx.status = 400
+		return
+	}
+
+	user.reset_token = null
+	await user.setPlaintextPassword(new_password)
+	await user.save()
+
+	ctx.status = 200
+	ctx.body = { user }
 }
\ No newline at end of file
diff --git a/src/http/controllers/auth.js b/src/http/controllers/auth.js
index db61f1054ffc367e00dcd6310276f9c2354522fe..61d06da7c449a27a94f4f17739431050bfc998ac 100644
--- a/src/http/controllers/auth.js
+++ b/src/http/controllers/auth.js
@@ -1,4 +1,5 @@
 const crypto = require('core/utils/crypto')
+const moment = require('moment')
 
 exports.login = async ctx => {
 	const { email, password } = ctx.request.body
@@ -19,4 +20,73 @@ exports.handleLoginRedirect = async ctx => {
 	} else {
 		return ctx.redirect('/')
 	}
+}
+
+const resetErrorMessages = {
+	missing: 'No token was found in the URL. If you clicked a link to get here, please make sure that it contains a password reset token. You may need to request a new password reset in the Jetsam app.',
+	invalid: 'The link you clicked was invalid or has expired. Password reset links are valid for 1 hour from the time we send them to you; you may need to request a new password reset in the Jetsam app.',
+}
+
+exports.resetPassword = async ctx => {
+	const errorData = {
+		back_link: 'https://jetsam.tech',
+	}
+
+	const token = ctx.query.token
+
+	if (!token) {
+		errorData.message = resetErrorMessages.missing
+		await ctx.render('auth/reset-password-error', errorData)
+		ctx.status = 400
+		return
+	}
+
+	const { sequelize } = require('database/models')
+	const [ [ { exists } ] ] = await sequelize.query('SELECT exists(select reset_token from users where reset_token = :token AND deleted_at is null limit 1)', { replacements: { token } })
+
+	if (!exists) {
+		errorData.message = resetErrorMessages.invalid
+		await ctx.render('auth/reset-password-error', errorData)
+		ctx.status = 404
+		return
+	}
+
+	let data = null
+	try {
+		data = JSON.parse(await crypto.decrypt(token))
+	} catch (e) {
+		errorData.message = resetErrorMessages.invalid
+		await ctx.render('auth/reset-password-error', errorData)
+		ctx.status = 400
+		return
+	}
+
+	const time = moment.utc(data.expires)
+
+	if (data.id == null || data.expires == null || time.isSameOrBefore(moment.utc())) {
+		errorData.message = resetErrorMessages.invalid
+		await ctx.render('auth/reset-password-error', errorData)
+		ctx.status = 400
+		return
+	}
+
+	await ctx.render('auth/reset-password', {
+		token,
+	})
+}
+
+exports.handleResetPassword = async ctx => {
+	const defer = require('./api/auth').handlePasswordReset
+	await defer(ctx)
+
+	if (ctx.status >= 400) {
+		const { error } = ctx.body
+		await ctx.render('auth/reset-password-error', {
+			back_link: `/reset-password?token=${ ctx.request.body.reset_token }`,
+			message: error.message,
+		})
+		return
+	}
+
+	await ctx.render('auth/reset-password-success')
 }
\ No newline at end of file
diff --git a/src/http/routes.js b/src/http/routes.js
index 035f1a238357d343680642e166b8ea3748329656..18c3a8677173c2485d43a86115025259bba588fa 100644
--- a/src/http/routes.js
+++ b/src/http/routes.js
@@ -17,6 +17,9 @@ web.get('/login', ctx => {
 })
 web.post('/login', controller('auth', 'login'))
 
+web.get('/reset-password', controller('auth', 'resetPassword'))
+web.post('/reset-password', controller('auth', 'handleResetPassword'))
+
 web.get('/auth/authorize', AuthServer.authorize)
 web.post('/auth/authorize', AuthServer.authorize)
 web.post('/auth/token', AuthServer.token)
@@ -43,6 +46,9 @@ function mount(api) {
 	api.post('/register', controller('api/auth', 'register'))
 	api.post('/login', controller('api/auth', 'login'))
 
+	api.post('/auth/reset-token', controller('api/auth', 'triggerPasswordReset'))
+	api.post('/auth/reset-password', controller('api/auth', 'handlePasswordReset'))
+
 	api.get('/self', controller('api/user', 'self'))
 	api.put('/self/:property', controller('api/user', 'updateOne'))
 
diff --git a/src/services/index.js b/src/services/index.js
index 37f48b60f98d56416fd15de9618251ffb496fcb1..f9d714d8f3debc67e5cdd105145a08ea7052d51a 100644
--- a/src/services/index.js
+++ b/src/services/index.js
@@ -2,6 +2,7 @@ const { loadEnvService } = require('./utils')
 
 const SERVICES = [
 	['cache', 'CACHE_DRIVER', 'memory'],
+	['mail', 'MAIL_DRIVER', 'log'],
 ]
 
 const services = {}
diff --git a/src/services/mail/interface.js b/src/services/mail/interface.js
new file mode 100644
index 0000000000000000000000000000000000000000..b4db4fc9f0b1de430840820d308e55719d335062
--- /dev/null
+++ b/src/services/mail/interface.js
@@ -0,0 +1,6 @@
+const { notImplemented } = require('services/utils')
+
+module.exports = class Mail {
+	async send(to, subject, content, opts) { notImplemented('send', 'send') }
+	async sendTemplate(to, subject, templateId, data, opts) { notImplemented('send', 'sendTemplate') }
+}
diff --git a/src/services/mail/log.js b/src/services/mail/log.js
new file mode 100644
index 0000000000000000000000000000000000000000..3415c05a83951dfa59cbd63448176db17f6cb9bf
--- /dev/null
+++ b/src/services/mail/log.js
@@ -0,0 +1,13 @@
+const Mail = require('./interface')
+
+class LogMail extends Mail {
+	async send(to, subject, content, opts) {
+		console.log('Sending mail [%s] to %s with this content:\n\n%s', subject, to, content)
+	}
+
+	async sendTemplate(to, subject, templateId, data, opts) {
+		console.log('Sending template [%s] %s to %s', subject, templateId, to, data)
+	}
+}
+
+module.exports = new LogMail()
diff --git a/src/services/mail/sendgrid.js b/src/services/mail/sendgrid.js
new file mode 100644
index 0000000000000000000000000000000000000000..2ee4d7bf9224ea21b94acfa3b1487d5d403e097b
--- /dev/null
+++ b/src/services/mail/sendgrid.js
@@ -0,0 +1,32 @@
+const Mail = require('./interface')
+const { config } = require('bootstrap')
+const mailer = require('@sendgrid/mail')
+
+class SendgridMail extends Mail {
+	constructor() {
+		super();
+		mailer.setApiKey(config('mail.key'))
+		this._baseOpts = config('mail.opts')
+	}
+
+
+	async send(to, subject, content, opts = {}) {
+		super.send(to, subject, content, opts);
+	}
+
+	async sendTemplate(to, subject, templateId, data = {}, opts = {}) {
+		await mailer.send({
+			...this._baseOpts,
+			...opts,
+			to,
+			subject,
+			templateId,
+			dynamic_template_data: {
+				subject,
+				...data,
+			},
+		})
+	}
+}
+
+module.exports = new SendgridMail()
diff --git a/views/auth/reset-password-error.hbs b/views/auth/reset-password-error.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..c4af0693152653c2da9c7d90fd50e90f0174c9b0
--- /dev/null
+++ b/views/auth/reset-password-error.hbs
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+	<meta charset="utf-8">
+	<title>Password Reset | Jetsam</title>
+	<meta name="description" content="Jetsam is a social plastics logging app that puts the power of change in your hands!">
+	<meta property="og:description" content="Jetsam is a social plastics logging app that puts the power of change in your hands!">
+	<meta property="og:image" content="https://jetsam.tech/images/card-image.png">
+	<meta property="og:image:type" content="image/png">
+	<meta name="twitter:site" content="@jetsam_tech">
+	<meta name="twitter:creator" content="@louisdoesdev">
+
+	<meta name="viewport" content="width=device-width, initial-scale=1">
+
+	<link rel="shortcut icon" type="image/png" href="https://jetsam.tech/images/logo.png">
+	<link rel="stylesheet"
+		  href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css"
+		  integrity="sha256-l85OmPOjvil/SOvVt3HnSSjzF1TUMyT9eV0c2BzEGzU="
+		  crossorigin="anonymous" />
+	<link rel="stylesheet"
+		  href="https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css"
+		  integrity="sha256-2YQRJMXD7pIAPHiXr0s+vlRWA7GYJEK0ARns7k2sbHY="
+		  crossorigin="anonymous" />
+
+	<link rel="stylesheet" href="/css/main.css?v=2">
+</head>
+
+<body>
+<main class="container" style="max-width: 600px">
+	<header class="header">
+		<img class="logo" src="https://jetsam.tech/images/logo.png" width="128px" height="128px">
+		<div>
+			<h1>Jetsam</h1>
+			<h3>Your World; Cleaner</h3>
+		</div>
+	</header>
+
+	<h3 class="centered">Password Reset Error</h3>
+	<p class="centered">{{ message }}</p>
+	<div class="row centered">
+		<a href="{{ back_link }}">Go Back</a>
+	</div>
+</main>
+
+</body>
+</html>
diff --git a/views/auth/reset-password-success.hbs b/views/auth/reset-password-success.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..16dee630eabb53c88ca82323eef54442adfd270f
--- /dev/null
+++ b/views/auth/reset-password-success.hbs
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+	<meta charset="utf-8">
+	<title>Password Reset | Jetsam</title>
+	<meta name="description" content="Jetsam is a social plastics logging app that puts the power of change in your hands!">
+	<meta property="og:description" content="Jetsam is a social plastics logging app that puts the power of change in your hands!">
+	<meta property="og:image" content="https://jetsam.tech/images/card-image.png">
+	<meta property="og:image:type" content="image/png">
+	<meta name="twitter:site" content="@jetsam_tech">
+	<meta name="twitter:creator" content="@louisdoesdev">
+
+	<meta name="viewport" content="width=device-width, initial-scale=1">
+
+	<link rel="shortcut icon" type="image/png" href="https://jetsam.tech/images/logo.png">
+	<link rel="stylesheet"
+		  href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css"
+		  integrity="sha256-l85OmPOjvil/SOvVt3HnSSjzF1TUMyT9eV0c2BzEGzU="
+		  crossorigin="anonymous" />
+	<link rel="stylesheet"
+		  href="https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css"
+		  integrity="sha256-2YQRJMXD7pIAPHiXr0s+vlRWA7GYJEK0ARns7k2sbHY="
+		  crossorigin="anonymous" />
+
+	<link rel="stylesheet" href="/css/main.css?v=2">
+</head>
+
+<body>
+<main class="container" style="max-width: 600px">
+	<header class="header">
+		<img class="logo" src="https://jetsam.tech/images/logo.png" width="128px" height="128px">
+		<div>
+			<h1>Jetsam</h1>
+			<h3>Your World; Cleaner</h3>
+		</div>
+	</header>
+
+	<h3 class="centered">Password Reset Successful</h3>
+	<p class="centered">You successfully reset your password. You can log in to the Jetsam app with the password you just created. Now go and snap some pesky plastics!</p>
+	<div class="row centered">
+		<a href="https://jetsam.tech">Go to the Jetsam website</a>
+	</div>
+</main>
+
+</body>
+</html>
diff --git a/views/auth/reset-password.hbs b/views/auth/reset-password.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..e74337f344cc347175f04d64114baa10726633e8
--- /dev/null
+++ b/views/auth/reset-password.hbs
@@ -0,0 +1,80 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+	<meta charset="utf-8">
+	<title>Password Reset | Jetsam</title>
+	<meta name="description" content="Jetsam is a social plastics logging app that puts the power of change in your hands!">
+	<meta property="og:description" content="Jetsam is a social plastics logging app that puts the power of change in your hands!">
+	<meta property="og:image" content="https://jetsam.tech/images/card-image.png">
+	<meta property="og:image:type" content="image/png">
+	<meta name="twitter:site" content="@jetsam_tech">
+	<meta name="twitter:creator" content="@louisdoesdev">
+
+	<meta name="viewport" content="width=device-width, initial-scale=1">
+
+	<link rel="shortcut icon" type="image/png" href="https://jetsam.tech/images/logo.png">
+	<link rel="stylesheet"
+		  href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css"
+		  integrity="sha256-l85OmPOjvil/SOvVt3HnSSjzF1TUMyT9eV0c2BzEGzU="
+		  crossorigin="anonymous" />
+	<link rel="stylesheet"
+		  href="https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css"
+		  integrity="sha256-2YQRJMXD7pIAPHiXr0s+vlRWA7GYJEK0ARns7k2sbHY="
+		  crossorigin="anonymous" />
+
+	<link rel="stylesheet" href="/css/main.css?v=2">
+</head>
+
+<body>
+<main class="container" style="max-width: 600px">
+	<header class="header">
+		<img class="logo" src="https://jetsam.tech/images/logo.png" width="128px" height="128px">
+		<div>
+			<h1>Jetsam</h1>
+			<h3>Your World; Cleaner</h3>
+		</div>
+	</header>
+
+	<form method="POST" action="/reset-password" id="reset_password_form">
+		<h3>Reset Password</h3>
+		<p>
+			You've requested a reset of your password; please use this form to choose a new one. This link is valid for 1 hour from the time we send it to you,
+			so if you've gone to grab a cup of tea between requesting a new password and using this form, you may need to request another password reset!
+		</p>
+		<input id=reset_token name=reset_token required type=hidden class="u-full-width" value="{{token}}">
+		<div class="row">
+			<div class="twelve columns">
+				<label for=new_password><sup class="required" title="Required">*</sup>New Password:</label>
+				<input id=new_password name=new_password class="u-full-width" required type="password">
+			</div>
+		</div>
+		<div class="row">
+			<div class="twelve columns">
+				<label for=confirm_password><sup class="required" title="Required">*</sup>Confirm Password:</label>
+				<input id=confirm_password name=confirm_password class="u-full-width" required type="password">
+			</div>
+		</div>
+
+
+		<div class="row centered">
+			<button class="button-primary four columns" type="submit" id=submit>Reset Password</button>
+		</div>
+		<div id="formmessage" class="row" style="color: red"></div>
+	</form>
+</main>
+
+<script async>
+	(function() {
+		var script = document.createElement('script');
+		window.counter = 'https://jetsam.goatcounter.com/count'
+		script.async = 1;
+		script.src = '//gc.zgo.at/count.js';
+
+		var ins = document.getElementsByTagName('script')[0];
+		ins.parentNode.insertBefore(script, ins)
+	})();
+</script>
+
+<script type="application/javascript" src="/js/reset-password.js"></script>
+</body>
+</html>