diff --git a/database/migrations/20210000000004-add-oauth-client-name-description.js b/database/migrations/20210000000004-add-oauth-client-name-description.js
new file mode 100644
index 0000000000000000000000000000000000000000..202dc71cdd8fc7e82d295bd607605c1569a46543
--- /dev/null
+++ b/database/migrations/20210000000004-add-oauth-client-name-description.js
@@ -0,0 +1,30 @@
+module.exports = {
+	up: (migration, Types) => {
+		return migration.sequelize.transaction(async t => {
+			await migration.addColumn('oauth_clients', 'name', {
+				type: Types.TEXT,
+				defaultValue: '',
+				allowNull: false,
+			}, {
+				transaction: t
+			})
+			await migration.addColumn('oauth_clients', 'description', {
+				type: Types.TEXT,
+				defaultValue: '',
+				allowNull: false,
+			}, {
+				transaction: t
+			})
+		})
+	},
+	down: (migration, Types) => {
+		return migration.sequelize.transaction(async t => {
+			await migration.removeColumn('oauth_clients', 'description', {
+				transaction: t
+			})
+			await migration.removeColumn('oauth_clients', 'name', {
+				transaction: t
+			})
+		})
+	},
+}
diff --git a/public/camera.png b/public/camera.png
new file mode 100644
index 0000000000000000000000000000000000000000..5b26b4064f4a3e6f10d7b79cfb4608628fc2f234
--- /dev/null
+++ b/public/camera.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c0e7823340c620d32e041c7e707fe2f69bca2077906dfc7cd4ad520d51c20df2
+size 798959
diff --git a/public/css/jetsam.css b/public/css/jetsam.css
new file mode 100644
index 0000000000000000000000000000000000000000..17beca4d722f6a0aac94281da44160a75b6e970c
--- /dev/null
+++ b/public/css/jetsam.css
@@ -0,0 +1,200 @@
+@import url('https://fonts.googleapis.com/css2?family=Bungee&family=Rubik:ital,wght@0,400;0,500;0,700;1,400&display=swap');
+
+:root {
+	--font-headings: Bungee, cursive;
+	--font-body: Rubik, sans-serif;
+
+	--colour-jetsam: #FF5E82;
+	--colour-jetsam-light: #FF7BAC;
+	--colour-jetsam-dark: #FF1D3C;
+	--colour-black: #292425;
+	--colour-white: #FAF9F9;
+	--colour-dark-grey: #697077;
+	--colour-light-grey: #DDE1E6;
+
+	--padding-block: 4rem;
+
+	--gradient-jetsam: linear-gradient(135deg, var(--colour-jetsam-light) 0%, var(--colour-jetsam-dark) 100%);
+
+	--breakpoint-small: 720px;
+}
+
+html {
+	height: 100%;
+}
+
+body {
+	min-height: 100%;
+	background: var(--colour-white);
+	color: var(--colour-black);
+
+	font-style: normal;
+	font-weight: normal;
+	font-size: 16px;
+	line-height: 19px;
+}
+
+p {
+	margin: 0;
+}
+
+a {
+	color: var(--colour-jetsam);
+}
+a:hover {
+	color: var(--colour-jetsam-dark);
+}
+
+.bg-jetsam {
+	background: var(--colour-jetsam);
+}
+.bg-jetsam-gradient {
+	background: var(--gradient-jetsam);
+}
+
+.font-brand {
+	font-family: var(--font-headings);
+}
+
+body, .font-body {
+	font-family: var(--font-body);
+}
+.text {
+	font-size: 16px;
+	line-height: 19px;
+}
+.text.lg {
+	font-size: 24px;
+	line-height: 28px;
+}
+.text.sm {
+	font-size: 12px;
+	line-height: 14px;
+}
+.text.bold {
+	font-weight: 500;
+}
+
+.text.italic {
+	font-style: italic;
+}
+.text.caption {
+	font-weight: 700;
+	text-transform: uppercase;
+}
+.text.subtle {
+	color: var(--colour-dark-grey);
+}
+.text.light {
+	color: var(--colour-white);
+}
+.text.center {
+	text-align: center;
+}
+.split-container {
+	display: flex;
+	height: 100%;
+	align-items: stretch;
+}
+.split-container > .split-child {
+	flex: 1;
+	padding: var(--padding-block);
+	display: flex;
+	flex-direction: column;
+	align-items: flex-start;
+}
+.center-content {
+	justify-content: center;
+	align-items: center;
+}
+.row {
+	width: 100%;
+	display: flex;
+	align-items: center;
+}
+.row.space-1 > :not(:last-child) {
+	margin-right: 1rem;
+}
+.row.space-2 > :not(:last-child) {
+	margin-right: 2rem;
+}
+.row.space-4 > :not(:last-child) {
+	margin-right: 4rem;
+}
+.column {
+	display: flex;
+	flex-direction: column;
+	align-items: flex-start;
+}
+.column.space-1 > :not(:last-child) {
+	margin-bottom: 1rem;
+}
+.column.space-2 > :not(:last-child) {
+	margin-bottom: 2rem;
+}
+.column.space-4 > :not(:last-child) {
+	margin-bottom: 4rem;
+}
+
+.form {
+	width: 100%;
+}
+
+.form .control {
+	width: 100%;
+}
+
+.form .control label, .form .control input {
+	padding: 0.5rem 0;
+}
+.form .control input {
+	box-sizing: border-box;
+	padding: 0.75rem;
+	width: 100%;
+	border-radius: 6px;
+	outline: none;
+	border: transparent 2px solid;
+	display: block;
+	background-color: var(--colour-light-grey);
+	transition: background-color 0.1s linear;
+}
+.form .control input:focus {
+	border-color: var(--colour-jetsam);
+	background-color: var(--colour-white);
+}
+
+.button {
+	font-style: normal;
+	font-weight: 700;
+	font-size: 16px;
+	line-height: 19px;
+	text-transform: uppercase;
+
+	width: 100%;
+	outline: none;
+	border: none;
+	margin: 0;
+	border-radius: 6px;
+	padding: 0.75rem;
+
+	transition: background-color 0.1s linear;
+	cursor: pointer;
+}
+
+.button.primary {
+	color: var(--colour-white);
+	background-color: var(--colour-jetsam);
+}
+.button.primary:hover {
+	background-color: var(--colour-jetsam-dark);
+}
+
+@media (max-width: 720px) {
+	:root {
+		--padding-block: 2rem;
+	}
+
+	.split-container {
+		flex-direction: column;
+	}
+}
\ No newline at end of file
diff --git a/public/css/reset.css b/public/css/reset.css
new file mode 100644
index 0000000000000000000000000000000000000000..3895e8c460b199072e43693eb26eaeb8f5fb3984
--- /dev/null
+++ b/public/css/reset.css
@@ -0,0 +1,37 @@
+/*** The new CSS Reset - version 1.2.0 (last updated 23.7.2021) ***/
+
+/* Remove all the styles of the "User-Agent-Stylesheet", except for the 'display' property */
+*:where(:not(iframe, canvas, img, svg, video):not(svg *)) {
+	all: unset;
+	display: revert;
+}
+
+/* Preferred box-sizing value */
+*,
+*::before,
+*::after {
+	box-sizing: border-box;
+}
+
+/*
+  Remove list styles (bullets/numbers)
+  in case you use it with normalize.css
+*/
+ol, ul {
+	list-style: none;
+}
+
+/* For images to not be able to exceed their container */
+img {
+	max-width: 100%;
+}
+
+/* Removes spacing between cells in tables */
+table {
+	border-collapse: collapse;
+}
+
+/* Revert the 'white-space' property for textarea elements on Safari */
+textarea {
+	white-space: revert;
+}
\ No newline at end of file
diff --git a/public/ios-app-store-badge-2928664fe1fc6aca88583a6f606d60ba.svg b/public/ios-app-store-badge-2928664fe1fc6aca88583a6f606d60ba.svg
new file mode 100644
index 0000000000000000000000000000000000000000..072b425a1ab785ee7143f0f0fba305f55bfb9678
--- /dev/null
+++ b/public/ios-app-store-badge-2928664fe1fc6aca88583a6f606d60ba.svg
@@ -0,0 +1,46 @@
+<svg id="livetype" xmlns="http://www.w3.org/2000/svg" width="119.66407" height="40" viewBox="0 0 119.66407 40">
+  <title>Download_on_the_App_Store_Badge_US-UK_RGB_blk_4SVG_092917</title>
+  <g>
+    <g>
+      <g>
+        <path d="M110.13477,0H9.53468c-.3667,0-.729,0-1.09473.002-.30615.002-.60986.00781-.91895.0127A13.21476,13.21476,0,0,0,5.5171.19141a6.66509,6.66509,0,0,0-1.90088.627A6.43779,6.43779,0,0,0,1.99757,1.99707,6.25844,6.25844,0,0,0,.81935,3.61816a6.60119,6.60119,0,0,0-.625,1.90332,12.993,12.993,0,0,0-.1792,2.002C.00587,7.83008.00489,8.1377,0,8.44434V31.5586c.00489.3105.00587.6113.01515.9219a12.99232,12.99232,0,0,0,.1792,2.0019,6.58756,6.58756,0,0,0,.625,1.9043A6.20778,6.20778,0,0,0,1.99757,38.001a6.27445,6.27445,0,0,0,1.61865,1.1787,6.70082,6.70082,0,0,0,1.90088.6308,13.45514,13.45514,0,0,0,2.0039.1768c.30909.0068.6128.0107.91895.0107C8.80567,40,9.168,40,9.53468,40H110.13477c.3594,0,.7246,0,1.084-.002.3047,0,.6172-.0039.9219-.0107a13.279,13.279,0,0,0,2-.1768,6.80432,6.80432,0,0,0,1.9082-.6308,6.27742,6.27742,0,0,0,1.6172-1.1787,6.39482,6.39482,0,0,0,1.1816-1.6143,6.60413,6.60413,0,0,0,.6191-1.9043,13.50643,13.50643,0,0,0,.1856-2.0019c.0039-.3106.0039-.6114.0039-.9219.0078-.3633.0078-.7246.0078-1.0938V9.53613c0-.36621,0-.72949-.0078-1.09179,0-.30664,0-.61426-.0039-.9209a13.5071,13.5071,0,0,0-.1856-2.002,6.6177,6.6177,0,0,0-.6191-1.90332,6.46619,6.46619,0,0,0-2.7988-2.7998,6.76754,6.76754,0,0,0-1.9082-.627,13.04394,13.04394,0,0,0-2-.17676c-.3047-.00488-.6172-.01074-.9219-.01269-.3594-.002-.7246-.002-1.084-.002Z" style="fill: #a6a6a6"/>
+        <path d="M8.44483,39.125c-.30468,0-.602-.0039-.90429-.0107a12.68714,12.68714,0,0,1-1.86914-.1631,5.88381,5.88381,0,0,1-1.65674-.5479,5.40573,5.40573,0,0,1-1.397-1.0166,5.32082,5.32082,0,0,1-1.02051-1.3965,5.72186,5.72186,0,0,1-.543-1.6572,12.41351,12.41351,0,0,1-.1665-1.875c-.00634-.2109-.01464-.9131-.01464-.9131V8.44434S.88185,7.75293.8877,7.5498a12.37039,12.37039,0,0,1,.16553-1.87207,5.7555,5.7555,0,0,1,.54346-1.6621A5.37349,5.37349,0,0,1,2.61183,2.61768,5.56543,5.56543,0,0,1,4.01417,1.59521a5.82309,5.82309,0,0,1,1.65332-.54394A12.58589,12.58589,0,0,1,7.543.88721L8.44532.875H111.21387l.9131.0127a12.38493,12.38493,0,0,1,1.8584.16259,5.93833,5.93833,0,0,1,1.6709.54785,5.59374,5.59374,0,0,1,2.415,2.41993,5.76267,5.76267,0,0,1,.5352,1.64892,12.995,12.995,0,0,1,.1738,1.88721c.0029.2832.0029.5874.0029.89014.0079.375.0079.73193.0079,1.09179V30.4648c0,.3633,0,.7178-.0079,1.0752,0,.3252,0,.6231-.0039.9297a12.73126,12.73126,0,0,1-.1709,1.8535,5.739,5.739,0,0,1-.54,1.67,5.48029,5.48029,0,0,1-1.0156,1.3857,5.4129,5.4129,0,0,1-1.3994,1.0225,5.86168,5.86168,0,0,1-1.668.5498,12.54218,12.54218,0,0,1-1.8692.1631c-.2929.0068-.5996.0107-.8974.0107l-1.084.002Z"/>
+      </g>
+      <g id="_Group_" data-name="&lt;Group&gt;">
+        <g id="_Group_2" data-name="&lt;Group&gt;">
+          <g id="_Group_3" data-name="&lt;Group&gt;">
+            <path id="_Path_" data-name="&lt;Path&gt;" d="M24.76888,20.30068a4.94881,4.94881,0,0,1,2.35656-4.15206,5.06566,5.06566,0,0,0-3.99116-2.15768c-1.67924-.17626-3.30719,1.00483-4.1629,1.00483-.87227,0-2.18977-.98733-3.6085-.95814a5.31529,5.31529,0,0,0-4.47292,2.72787c-1.934,3.34842-.49141,8.26947,1.3612,10.97608.9269,1.32535,2.01018,2.8058,3.42763,2.7533,1.38706-.05753,1.9051-.88448,3.5794-.88448,1.65876,0,2.14479.88448,3.591.8511,1.48838-.02416,2.42613-1.33124,3.32051-2.66914a10.962,10.962,0,0,0,1.51842-3.09251A4.78205,4.78205,0,0,1,24.76888,20.30068Z" style="fill: #fff"/>
+            <path id="_Path_2" data-name="&lt;Path&gt;" d="M22.03725,12.21089a4.87248,4.87248,0,0,0,1.11452-3.49062,4.95746,4.95746,0,0,0-3.20758,1.65961,4.63634,4.63634,0,0,0-1.14371,3.36139A4.09905,4.09905,0,0,0,22.03725,12.21089Z" style="fill: #fff"/>
+          </g>
+        </g>
+        <g>
+          <path d="M42.30227,27.13965h-4.7334l-1.13672,3.35645H34.42727l4.4834-12.418h2.083l4.4834,12.418H43.438ZM38.0591,25.59082h3.752l-1.84961-5.44727h-.05176Z" style="fill: #fff"/>
+          <path d="M55.15969,25.96973c0,2.81348-1.50586,4.62109-3.77832,4.62109a3.0693,3.0693,0,0,1-2.84863-1.584h-.043v4.48438h-1.8584V21.44238H48.4302v1.50586h.03418a3.21162,3.21162,0,0,1,2.88281-1.60059C53.645,21.34766,55.15969,23.16406,55.15969,25.96973Zm-1.91016,0c0-1.833-.94727-3.03809-2.39258-3.03809-1.41992,0-2.375,1.23047-2.375,3.03809,0,1.82422.95508,3.0459,2.375,3.0459C52.30227,29.01563,53.24953,27.81934,53.24953,25.96973Z" style="fill: #fff"/>
+          <path d="M65.12453,25.96973c0,2.81348-1.50586,4.62109-3.77832,4.62109a3.0693,3.0693,0,0,1-2.84863-1.584h-.043v4.48438h-1.8584V21.44238H58.395v1.50586h.03418A3.21162,3.21162,0,0,1,61.312,21.34766C63.60988,21.34766,65.12453,23.16406,65.12453,25.96973Zm-1.91016,0c0-1.833-.94727-3.03809-2.39258-3.03809-1.41992,0-2.375,1.23047-2.375,3.03809,0,1.82422.95508,3.0459,2.375,3.0459C62.26711,29.01563,63.21438,27.81934,63.21438,25.96973Z" style="fill: #fff"/>
+          <path d="M71.71047,27.03613c.1377,1.23145,1.334,2.04,2.96875,2.04,1.56641,0,2.69336-.80859,2.69336-1.91895,0-.96387-.67969-1.541-2.28906-1.93652l-1.60937-.3877c-2.28027-.55078-3.33887-1.61719-3.33887-3.34766,0-2.14258,1.86719-3.61426,4.51855-3.61426,2.624,0,4.42285,1.47168,4.4834,3.61426h-1.876c-.1123-1.23926-1.13672-1.9873-2.63379-1.9873s-2.52148.75684-2.52148,1.8584c0,.87793.6543,1.39453,2.25488,1.79l1.36816.33594c2.54785.60254,3.60645,1.626,3.60645,3.44238,0,2.32324-1.85059,3.77832-4.79395,3.77832-2.75391,0-4.61328-1.4209-4.7334-3.667Z" style="fill: #fff"/>
+          <path d="M83.34621,19.2998v2.14258h1.72168v1.47168H83.34621v4.99121c0,.77539.34473,1.13672,1.10156,1.13672a5.80752,5.80752,0,0,0,.61133-.043v1.46289a5.10351,5.10351,0,0,1-1.03223.08594c-1.833,0-2.54785-.68848-2.54785-2.44434V22.91406H80.16262V21.44238H81.479V19.2998Z" style="fill: #fff"/>
+          <path d="M86.065,25.96973c0-2.84863,1.67773-4.63867,4.29395-4.63867,2.625,0,4.29492,1.79,4.29492,4.63867,0,2.85645-1.66113,4.63867-4.29492,4.63867C87.72609,30.6084,86.065,28.82617,86.065,25.96973Zm6.69531,0c0-1.9541-.89551-3.10742-2.40137-3.10742s-2.40039,1.16211-2.40039,3.10742c0,1.96191.89453,3.10645,2.40039,3.10645S92.76027,27.93164,92.76027,25.96973Z" style="fill: #fff"/>
+          <path d="M96.18606,21.44238h1.77246v1.541h.043a2.1594,2.1594,0,0,1,2.17773-1.63574,2.86616,2.86616,0,0,1,.63672.06934v1.73828a2.59794,2.59794,0,0,0-.835-.1123,1.87264,1.87264,0,0,0-1.93652,2.083v5.37012h-1.8584Z" style="fill: #fff"/>
+          <path d="M109.3843,27.83691c-.25,1.64355-1.85059,2.77148-3.89844,2.77148-2.63379,0-4.26855-1.76465-4.26855-4.5957,0-2.83984,1.64355-4.68164,4.19043-4.68164,2.50488,0,4.08008,1.7207,4.08008,4.46582v.63672h-6.39453v.1123a2.358,2.358,0,0,0,2.43555,2.56445,2.04834,2.04834,0,0,0,2.09082-1.27344Zm-6.28223-2.70215h4.52637a2.1773,2.1773,0,0,0-2.2207-2.29785A2.292,2.292,0,0,0,103.10207,25.13477Z" style="fill: #fff"/>
+        </g>
+      </g>
+    </g>
+    <g id="_Group_4" data-name="&lt;Group&gt;">
+      <g>
+        <path d="M37.82619,8.731a2.63964,2.63964,0,0,1,2.80762,2.96484c0,1.90625-1.03027,3.002-2.80762,3.002H35.67092V8.731Zm-1.22852,5.123h1.125a1.87588,1.87588,0,0,0,1.96777-2.146,1.881,1.881,0,0,0-1.96777-2.13379h-1.125Z" style="fill: #fff"/>
+        <path d="M41.68068,12.44434a2.13323,2.13323,0,1,1,4.24707,0,2.13358,2.13358,0,1,1-4.24707,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C44.57522,13.99463,45.01369,13.42432,45.01369,12.44434Z" style="fill: #fff"/>
+        <path d="M51.57326,14.69775h-.92187l-.93066-3.31641h-.07031l-.92676,3.31641h-.91309l-1.24121-4.50293h.90137l.80664,3.436h.06641l.92578-3.436h.85254l.92578,3.436h.07031l.80273-3.436h.88867Z" style="fill: #fff"/>
+        <path d="M53.85354,10.19482H54.709v.71533h.06641a1.348,1.348,0,0,1,1.34375-.80225,1.46456,1.46456,0,0,1,1.55859,1.6748v2.915h-.88867V12.00586c0-.72363-.31445-1.0835-.97168-1.0835a1.03294,1.03294,0,0,0-1.0752,1.14111v2.63428h-.88867Z" style="fill: #fff"/>
+        <path d="M59.09377,8.437h.88867v6.26074h-.88867Z" style="fill: #fff"/>
+        <path d="M61.21779,12.44434a2.13346,2.13346,0,1,1,4.24756,0,2.1338,2.1338,0,1,1-4.24756,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C64.11232,13.99463,64.5508,13.42432,64.5508,12.44434Z" style="fill: #fff"/>
+        <path d="M66.4009,13.42432c0-.81055.60352-1.27783,1.6748-1.34424l1.21973-.07031v-.38867c0-.47559-.31445-.74414-.92187-.74414-.49609,0-.83984.18213-.93848.50049h-.86035c.09082-.77344.81836-1.26953,1.83984-1.26953,1.12891,0,1.76563.562,1.76563,1.51318v3.07666h-.85547v-.63281h-.07031a1.515,1.515,0,0,1-1.35254.707A1.36026,1.36026,0,0,1,66.4009,13.42432Zm2.89453-.38477v-.37646l-1.09961.07031c-.62012.0415-.90137.25244-.90137.64941,0,.40527.35156.64111.835.64111A1.0615,1.0615,0,0,0,69.29543,13.03955Z" style="fill: #fff"/>
+        <path d="M71.34816,12.44434c0-1.42285.73145-2.32422,1.86914-2.32422a1.484,1.484,0,0,1,1.38086.79h.06641V8.437h.88867v6.26074h-.85156v-.71143h-.07031a1.56284,1.56284,0,0,1-1.41406.78564C72.0718,14.772,71.34816,13.87061,71.34816,12.44434Zm.918,0c0,.95508.4502,1.52979,1.20313,1.52979.749,0,1.21191-.583,1.21191-1.52588,0-.93848-.46777-1.52979-1.21191-1.52979C72.72121,10.91846,72.26613,11.49707,72.26613,12.44434Z" style="fill: #fff"/>
+        <path d="M79.23,12.44434a2.13323,2.13323,0,1,1,4.24707,0,2.13358,2.13358,0,1,1-4.24707,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C82.12453,13.99463,82.563,13.42432,82.563,12.44434Z" style="fill: #fff"/>
+        <path d="M84.66945,10.19482h.85547v.71533h.06641a1.348,1.348,0,0,1,1.34375-.80225,1.46456,1.46456,0,0,1,1.55859,1.6748v2.915H87.605V12.00586c0-.72363-.31445-1.0835-.97168-1.0835a1.03294,1.03294,0,0,0-1.0752,1.14111v2.63428h-.88867Z" style="fill: #fff"/>
+        <path d="M93.51516,9.07373v1.1416h.97559v.74854h-.97559V13.2793c0,.47168.19434.67822.63672.67822a2.96657,2.96657,0,0,0,.33887-.02051v.74023a2.9155,2.9155,0,0,1-.4834.04541c-.98828,0-1.38184-.34766-1.38184-1.21582v-2.543h-.71484v-.74854h.71484V9.07373Z" style="fill: #fff"/>
+        <path d="M95.70461,8.437h.88086v2.48145h.07031a1.3856,1.3856,0,0,1,1.373-.80664,1.48339,1.48339,0,0,1,1.55078,1.67871v2.90723H98.69v-2.688c0-.71924-.335-1.0835-.96289-1.0835a1.05194,1.05194,0,0,0-1.13379,1.1416v2.62988h-.88867Z" style="fill: #fff"/>
+        <path d="M104.76125,13.48193a1.828,1.828,0,0,1-1.95117,1.30273A2.04531,2.04531,0,0,1,100.73,12.46045a2.07685,2.07685,0,0,1,2.07617-2.35254c1.25293,0,2.00879.856,2.00879,2.27V12.688h-3.17969v.0498a1.1902,1.1902,0,0,0,1.19922,1.29,1.07934,1.07934,0,0,0,1.07129-.5459Zm-3.126-1.45117h2.27441a1.08647,1.08647,0,0,0-1.1084-1.1665A1.15162,1.15162,0,0,0,101.63527,12.03076Z" style="fill: #fff"/>
+      </g>
+    </g>
+  </g>
+</svg>
diff --git a/src/config/app.js b/src/config/app.js
index f10841c43820e5f6203cc738610f140462acfee8..28b9fe507d3a5c6da03213a893a68da93ddc16fb 100644
--- a/src/config/app.js
+++ b/src/config/app.js
@@ -3,7 +3,7 @@ const { env } = require('bootstrap')
 module.exports = {
 	env: env('NODE_ENV', 'development'),
 	key: env('APP_KEY', new Error('No App Key Set')),
-	session_key: env('SESSION_IDENTIFIER', 'hf.sid'),
+	session_key: env('SESSION_IDENTIFIER', 'jetsam.sid'),
 	port: Number(env('PORT', 4000)),
 	name: env('APP_NAME', 'application'),
 	host: {
diff --git a/src/database/models/OAuthClient.js b/src/database/models/OAuthClient.js
index 2cfb54a2859a14affcac9e07a14a2c8cbf6e48bd..dd6ad12764961561f9f35212fa03d3bfafc2a5ab 100644
--- a/src/database/models/OAuthClient.js
+++ b/src/database/models/OAuthClient.js
@@ -37,6 +37,8 @@ class OAuthClient extends BaseModel {
 	toJSON() {
 		return {
 			id: this.id,
+			name: this.name,
+			description: this.description,
 			secret: this.secret,
 			redirect_uris: this.redirect_uris,
 			grant_types: this.grant_types,
@@ -59,6 +61,12 @@ module.exports = (sequelize, DataTypes) => {
 						isUUID: 4,
 					},
 				},
+				name: {
+					type: DataTypes.TEXT,
+				},
+				description: {
+					type: DataTypes.TEXT,
+				},
 				secret: {
 					type: DataTypes.TEXT,
 				},
diff --git a/src/database/models/Survey.js b/src/database/models/Survey.js
index 670325ae04b0ccecc314ffee565308cb0b251fd0..054fd31ecc0b59f3fe0e45cc3da4ee564b1f748b 100644
--- a/src/database/models/Survey.js
+++ b/src/database/models/Survey.js
@@ -3,7 +3,8 @@ const BaseModel = require('./BaseModel')
 
 class Survey extends BaseModel {
 	static associate(models) {
-		// this.belongsTo(models.User, {foreignKey: 'user_id'})
+		this.hasMany(models.SurveyUser, { foreignKey: 'survey_id' })
+		this.belongsToMany(models.User, { through: models.SurveyUser, timestamps: false })
 	}
 
 	toJSON() {
diff --git a/src/database/models/SurveyUsers.js b/src/database/models/SurveyUsers.js
new file mode 100644
index 0000000000000000000000000000000000000000..cccb463dc8819a797b39174d71f452fad31e3d1e
--- /dev/null
+++ b/src/database/models/SurveyUsers.js
@@ -0,0 +1,50 @@
+const timestamps = require('./properties/timestamps')
+const BaseModel = require('./BaseModel')
+
+class SurveyUser extends BaseModel {
+	static associate(models) {
+		this.belongsTo(models.User, {foreignKey: 'user_id'})
+		this.belongsTo(models.Survey, {foreignKey: 'survey_id'})
+	}
+
+	toJSON() {
+		return {
+			properties: this.properties,
+			created_at: this.created_at,
+		}
+	}
+}
+
+module.exports = (sequelize, DataTypes) => {
+	SurveyUser.init(
+		Object.assign(
+			{
+				user_id: {
+					type: DataTypes.UUID,
+				},
+				survey_id: {
+					type: DataTypes.UUID,
+				},
+				properties: {
+					type: DataTypes.JSONB,
+					defaultValue: {},
+					allowNull: false,
+				},
+				created_at: {
+					type: DataTypes.DATE,
+					validate: {
+						isDate: true,
+					},
+				},
+			},
+		),
+		{
+			sequelize,
+			paranoid: false,
+			timestamps: false,
+			tableName: 'survey_users',
+		},
+	)
+
+	return SurveyUser
+}
diff --git a/src/database/models/User.js b/src/database/models/User.js
index 8b9f3b0f9a64c303bab16483e57c3320d26672f5..03e22efa8aec64d735c027970cff92211716dc37 100644
--- a/src/database/models/User.js
+++ b/src/database/models/User.js
@@ -9,6 +9,7 @@ class User extends BaseModel {
 		this.hasMany(models.RefreshToken, { foreignKey: 'user_id' })
 		this.hasMany(models.File, { foreignKey: 'user_id' })
 		this.hasMany(models.Upload, { foreignKey: 'user_id' })
+		this.hasMany(models.SurveyUser, { as: 'SurveyMembership', foreignKey: 'user_id' })
 		this.belongsToMany(models.File, {
 			as: 'likes',
 			through: 'user_file_likes',
@@ -21,6 +22,7 @@ class User extends BaseModel {
 			otherKey: 'bundle_code_id',
 			timestamps: false,
 		})
+		this.belongsToMany(models.Survey, { through: models.SurveyUser, timestamps: false })
 	}
 
 	static async fromToken(token, type) {
diff --git a/src/domain/auth/AuthenticationService.js b/src/domain/auth/AuthenticationService.js
index e4995a2998cb1aec30f6495ad0be90ea5ee9f3b6..78a5b5c579134e293f06b349c5715958fcf1d48f 100644
--- a/src/domain/auth/AuthenticationService.js
+++ b/src/domain/auth/AuthenticationService.js
@@ -100,6 +100,7 @@ module.exports = class AuthenticationService extends ContextualModule {
 	}
 
 	async clearSessionAuth() {
-		this.ctx.session.token = null
+		delete this.ctx.session.token
+		delete this.ctx.session.user
 	}
 }
diff --git a/src/domain/auth/OAuthFlow.js b/src/domain/auth/OAuthFlow.js
index 6ae94fa4ebc810dd1dcd1dbb51fa56bbcaf8de3b..fa6af3032a4fedb67679f42c08be9ab2b04dd749 100644
--- a/src/domain/auth/OAuthFlow.js
+++ b/src/domain/auth/OAuthFlow.js
@@ -24,6 +24,8 @@ exports.initialiseFlow = async ctx => {
 
 	const action = baseQuery.action
 
+	console.log({ redirect: 'authorize', query: baseQuery })
+
 	const redirectState = await crypto.encrypt(
 		JSON.stringify({ redirect: 'authorize', query: baseQuery }),
 	)
@@ -47,9 +49,11 @@ exports.showOAuthConsent = async (ctx, queryState) => {
 
 	const scopes = describeScopeRequest(query.scope)
 
+	console.log(client)
+
 	return ctx.render('auth/accept-oauth', {
 		user,
-		client,
+		client: client.toJSON(),
 		scopes,
 		redirect,
 	})
diff --git a/src/http/controllers/api/v2/surveys.js b/src/http/controllers/api/v2/surveys.js
index ab5df30193423ec64d8e3052f8fd13593f40a919..76ceefef0a829be754eb04875f7124339f004866 100644
--- a/src/http/controllers/api/v2/surveys.js
+++ b/src/http/controllers/api/v2/surveys.js
@@ -2,6 +2,8 @@ const moment = require('moment')
 const UnauthorizedError = require("../../../../core/errors/UnauthorizedError");
 const NotFoundError = require("../../../../core/errors/NotFoundError");
 const HttpError = require("../../../../core/errors/HttpError");
+const { Survey, SurveyUser, User } = require('database/models')
+
 
 exports.list = async ctx => {
 	const { Sequelize, Survey } = require('database/models')
@@ -25,6 +27,19 @@ exports.list = async ctx => {
 	}
 }
 
+exports.joined = async ctx => {
+	const user = await ctx.services['core.auth'].getUser()
+	if (user == null) {
+		ctx.body = {
+			surveys: []
+		}
+		return
+	}
+
+	const surveys = await user.getSurveys({ include: [SurveyUser] })
+	console.log(surveys)
+}
+
 exports.join = async ctx => {
 	if (ctx.models.survey == null) {
 		throw new NotFoundError('survey')
@@ -36,15 +51,43 @@ exports.join = async ctx => {
 	}
 
 	const { survey } = ctx.models
-	const { properties } = ctx.request.body
+	const { properties = {} } = ctx.request.body
 
 	if (Object.values(survey.properties).length !== Object.values(properties).length) {
 		throw new HttpError(400, 'Must provide values for all properties')
 	}
 
-
+	const surveyUserProperties = {}
+	Object.entries(survey.properties).forEach(([key, type]) => {
+		if (properties[key] == null) {
+			throw new HttpError(400, `Must provide value for property ${ key }`)
+		}
+		surveyUserProperties[key] = properties[key]
+	})
 
 	console.log(survey)
+
+	const surveyUser = await user.createSurveyMembership({
+		survey,
+		properties: surveyUserProperties,
+	}, {
+		include: [Survey]
+	})
+
+	// const surveyUser = await SurveyUser.create({
+	// 	survey_id: survey.id,
+	// 	user_id: user.id,
+	// 	properties: surveyUserProperties,
+	// }, {
+	// 	include: [Survey, User]
+	// })
+
+	console.log(surveyUser)
+
+	ctx.body = {
+		survey,
+		answers: surveyUser,
+	}
 }
 
 exports.leave = async ctx => {
diff --git a/src/http/controllers/auth.js b/src/http/controllers/auth.js
index c096b26cec1a1569e499e16345e73ea7d45f9438..2b5b170be0d90deb20076e6a206873f4e7536293 100644
--- a/src/http/controllers/auth.js
+++ b/src/http/controllers/auth.js
@@ -7,6 +7,10 @@ exports.login = async ctx => {
 	await ctx.services['core.auth'].saveToSession()
 	return exports.handleLoginRedirect(ctx)
 }
+exports.logout = async ctx => {
+	await ctx.services['core.auth'].clearSessionAuth()
+	return ctx.redirect('/')
+}
 
 exports.handleLoginRedirect = async ctx => {
 	const query = ctx.request.query
diff --git a/src/http/routers/routes_v2.js b/src/http/routers/routes_v2.js
index 45e3ab414f885f3c8ad0e1e763cc77031adecf4f..be47d9492453703a9cf3ee9eb1d108644266dce0 100644
--- a/src/http/routers/routes_v2.js
+++ b/src/http/routers/routes_v2.js
@@ -33,6 +33,7 @@ router.post(
 )
 
 router.get('/self', controller('api/user', 'self'))
+router.get('/self/surveys', controller('api/v2/surveys', 'joined'))
 router.get('/self/bundles', controller('api/app', 'getBundles'))
 router.put('/self/:property', controller('api/user', 'updateOne'))
 
diff --git a/src/http/routes.js b/src/http/routes.js
index 9573e0028dea83887d4c8e45d665d8ed6c195114..f88661129bf8c783e9d09654db50b1619b94a48d 100644
--- a/src/http/routes.js
+++ b/src/http/routes.js
@@ -49,11 +49,18 @@ web.use(well_known.routes())
 
 web.get('/login', ctx => {
 	const data = {}
+	const state = new URLSearchParams()
 	if (ctx.query.login_state) {
-		data.login_state = ctx.query.login_state
+		state.set('login_state',  ctx.query.login_state)
 	}
+	if (ctx.query.auth_state) {
+		state.set('auth_state', ctx.query.auth_state)
+	}
+
+	data.state_string = `?${ state.toString() }`
 	return ctx.render('auth/login', data)
 })
+web.get('/logout', controller('auth', 'logout'))
 web.post('/login', controller('auth', 'login'))
 
 web.get('/reset-password', controller('auth', 'resetPassword'))
diff --git a/views/auth/accept-oauth.hbs b/views/auth/accept-oauth.hbs
index 5b73a73422048a9ef8a18dfd3032cd876192a5ee..fcc291fc304ef4e10cef8677e07291980f76c300 100644
--- a/views/auth/accept-oauth.hbs
+++ b/views/auth/accept-oauth.hbs
@@ -1,17 +1,26 @@
 <!DOCTYPE html>
 <html lang="en">
-	<head>
-		<meta charset="utf-8">
-		<title>Authenticate Your Account</title>
-        <link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet">
-	</head>
+<head>
+	<title>Authorise {{ client.name }} | Jetsam</title>
+	<meta charset="utf-8">
+	<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+	<meta name="description" content="">
+	<meta name="author" content="">
+	<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css">
+	<link rel="stylesheet" href="/css/jetsam.css">
 
-	<body class="bg-white rounded-t-lg overflow-hidden border-t border-l border-r border-gray-400 p-4 px-3 py-10 bg-gray-200 flex justify-center">
+	<style>
+		body, body > div {
+			height: 100%;
+		}
+	</style>
+</head>
+<body>
 	<div class="max-w-lg rounded overflow-hidden bg-white shadow">
-		{{#if client.meta.name }}<h1 class="p-2 text-xl">{{ client.meta.name }}</h1>{{/if}}
-		{{#if client.meta.description }}<p class="p-2">{{ client.meta.description }}</p>{{/if}}
+		{{#if client.name }}<h1 class="p-2 text-xl">{{ client.name }}</h1>{{/if}}
+		{{#if client.description }}<p class="p-2">{{ client.description }}</p>{{/if}}
 		<p class="p-2 border-t border-b border-solid border-gray-400">
-			{{#if client.meta.name}}{{client.meta.name}}{{else}}An application{{/if}} is requesting access to the following
+			{{#if client.name}}{{client.name}}{{else}}An application{{/if}} is requesting access to the following
 			permissions. Please carefully read what each does before accepting or rejecting this request. Only accept this
 			request for access if you trust the application.
 		</p>
diff --git a/views/auth/login-tailwind.hbs b/views/auth/login-tailwind.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..b4be939dad495c9c5ba254b6db298f215d952a7e
--- /dev/null
+++ b/views/auth/login-tailwind.hbs
@@ -0,0 +1,86 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+	<title>Login | Jetsam</title>
+	<meta charset="utf-8">
+	<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+	<meta name="description" content="">
+	<meta name="author" content="">
+	<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&amp;display=swap" rel="stylesheet">
+	<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet">
+	<link rel="icon" type="image/png" sizes="16x16" href="favicon-tailwind.png">
+	<script src="js/main.js"></script>
+
+	<style>
+		body, body > div {
+			height: 100%;
+		}
+	</style>
+</head>
+<body class="antialiased bg-body text-body font-body">
+<div class="">
+
+	<section>
+		<div class="flex flex-wrap">
+			<div class="pt-6 lg:pt-16 pb-6 w-full lg:w-1/2">
+				<div class="max-w-md mx-auto">
+					<div class="mb-6 lg:mb-20 w-full px-3 flex items-center justify-between">
+						<a class="text-3xl font-bold leading-none" href="#">
+							<img class="h-12" src="atis-assets/logo/atis/atis-mono-black.svg" alt="" width="auto"></a>
+						<a class="py-2 px-6 text-xs rounded-l-xl rounded-t-xl bg-green-100 hover:bg-green-200 text-green-600 font-bold transition duration-200"
+						   href="#">Sign Up</a>
+					</div>
+					<div>
+						<div class="mb-6 px-3">
+							<span class="text-gray-500">Sign In</span>
+							<h3 class="text-2xl font-bold">Join our community</h3>
+						</div>
+						<form action="">
+							<div class="mb-3 flex p-4 mx-3 bg-gray-100 rounded">
+								<input class="w-full text-xs bg-gray-100 outline-none" type="email"
+									   placeholder="name@email.com">
+							</div>
+							<div class="mb-6 flex p-4 mx-3 bg-gray-100 rounded">
+								<input class="w-full text-xs bg-gray-100 outline-none" type="password"
+									   placeholder="Enter your password">
+							</div>
+							<div class="px-3 text-center">
+								<button class="mb-4 w-full py-4 bg-pink-400 hover:bg-pink-600 rounded text-sm font-bold text-gray-50 transition duration-200">
+									Sign In
+								</button>
+								<a class="text-xs text-green-600 hover:underline" href="#">Forgot password?</a>
+							</div>
+						</form>
+					</div>
+				</div>
+			</div>
+			<div class="hidden lg:block relative w-full lg:w-1/2 bg-pink-400">
+				<div class="absolute bottom-0 inset-x-0 mx-auto mb-12 max-w-xl text-center" style="z-index: 10;">
+					<img class="max-w-xl" src="atis-assets/illustrations/pablo-coming-soon-dark-mono.png" alt="">
+					<h2 class="mb-2 text-2xl text-white font-bold">Track Plastics; Make Change Happen</h2>
+					<div class="max-w-lg mx-auto">
+						<p class="mb-6 text-gray-50 leading-loose">Lorem ipsum dolor sit amet, consectetur adipiscing
+							elit. Pellentesque efficitur nisl sodales egestas lobortis.</p>
+					</div>
+					<a class="inline-block py-2 px-6 leading-loose rounded-t-xl rounded-l-xl bg-white hover:bg-gray-500 text-gray-900 hover:text-white transition duration-200 font-bold"
+					   href="#">Get Started</a>
+				</div>
+			</div>
+			<div class="lg:hidden bg-green-600">
+				<div class="relative w-full">
+					<img class="relative mx-auto max-w-sm mt-4 mb-4 block"
+						 src="atis-assets/illustrations/pablo-coming-soon-dark-mono.png" alt="">
+				</div>
+				<div class="py-10 px-3 text-center" style="z-index: 10;">
+					<h2 class="mb-2 text-2xl text-white font-bold">So much more than a business analytics tool</h2>
+					<p class="mb-6 text-gray-50 leading-loose">Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+						Pellentesque efficitur nisl sodales egestas lobortis.</p>
+					<a class="py-2 px-6 leading-loose rounded-t-xl rounded-l-xl rounded-l-xl bg-white hover:bg-gray-500 text-gray-900 hover:text-white transition duration-200 font-bold"
+					   href="#">Get Started</a>
+				</div>
+			</div>
+		</div>
+	</section>
+</div>
+</body>
+</html>
diff --git a/views/auth/login.hbs b/views/auth/login.hbs
index 249595d202199330e1f45b63d6161a60b70b7629..e2d1145923d3fe7ea8eee25470e84ac2601926be 100644
--- a/views/auth/login.hbs
+++ b/views/auth/login.hbs
@@ -1,35 +1,77 @@
 <!DOCTYPE html>
 <html lang="en">
-	<head>
-		<meta charset="utf-8">
-		<title>Log In To Your Account</title>
-		<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet">
-	</head>
+<head>
+	<title>Login | Jetsam</title>
+	<meta charset="utf-8">
+	<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+	<meta name="description" content="">
+	<meta name="author" content="">
+	<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css">
+	<link rel="stylesheet" href="/css/jetsam.css">
 
-	<body class="bg-white rounded-t-lg overflow-hidden border-t border-l border-r border-gray-400 p-4 px-3 py-10 bg-gray-200 flex justify-center">
-        <div class="w-full max-w-xs">
-            <form class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4" method="post" action="/login{{#if login_state}}?login_state={{login_state}}{{/if}}">
-                <div class="mb-4">
-                    <label class="block text-gray-700 text-sm font-bold mb-2" for="username">
-                        Email
-                    </label>
-                    <input class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" id="email" name="email" placeholder="Email" type="email">
-                </div>
-                <div class="mb-6">
-                    <label class="block text-gray-700 text-sm font-bold mb-2" for="password">
-                        Password
-                    </label>
-                    <input class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline" id="password" name="password" type="password" placeholder="Password">
-                </div>
-                <div class="flex items-center justify-between">
-                    <button class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline" type="submit">
-                        Sign In
-                    </button>
-<!--                    <a class="inline-block align-baseline font-bold text-sm text-blue-500 hover:text-blue-800" href="#">-->
-<!--                        Forgot Password?-->
-<!--                    </a>-->
-                </div>
-            </form>
-        </div>
-	</body>
-</html>
\ No newline at end of file
+	<style>
+		body, body > div {
+			height: 100%;
+		}
+	</style>
+</head>
+<body>
+<div class="split-container">
+	<div class="split-child center-content column space-2">
+		<div class="row space-1">
+			<img src="/logo.png" width="75px">
+			<h1 class="font-brand">Jetsam</h1>
+		</div>
+
+		<div class="spacer"></div>
+
+		<div class="column space-1">
+			<p class="text sm caption subtle">
+				Log In
+			</p>
+			<p class="text lg bold">
+				Join The Community
+			</p>
+		</div>
+
+			<form class="form column space-1" method="post" action="/login{{#if state_string}}{{state_string}}{{/if}}">
+		<div class="control column">
+			<label for="email">Email Address</label>
+			<input id="email" name="email" type="email" placeholder="Email Address"/>
+		</div>
+		<div class="control column">
+			<label for="password">Password</label>
+			<input id="password" name="password" type="password" placeholder="Password"/>
+		</div>
+
+		<button type="submit" class="button primary">
+			Log In
+		</button>
+		<div class="row center-content">
+			<a href="/reset-password">Forgotten Password?</a>
+		</div>
+		</form>
+	</div>
+	<div class="split-child bg-jetsam center-content column space-2">
+		<img src="/camera.png" width="50%" style="margin-left: auto; margin-right: auto"/>
+		<p class="text lg bold light center" style="margin-left: auto; margin-right: auto">
+			Photograph Plastic; Make Change Happen
+		</p>
+		<p class="text light center">
+			Download Jetsam on your phone and start snapping pictures of pesky plastics - every photo makes a difference
+		</p>
+		<div class="row center-content">
+			<a href="https://apps.apple.com/gb/app/jetsam/id1494342033?ls=1"><img height="50"
+																				  alt="Download on the App Store"
+																				  src="/ios-app-store-badge-2928664fe1fc6aca88583a6f606d60ba.svg"
+																				  style="height: 50px;"></a>
+			<a class="centered"
+			   href="https://play.google.com/store/apps/details?id=tech.jetsam.catalog&amp;pcampaignid=pcampaignidMKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1"><img
+					height="75" alt="Get it on Google Play"
+					src="https://play.google.com/intl/en_gb/badges/static/images/badges/en_badge_web_generic.png"
+					style="height: 74px;"></a>
+		</div>
+	</div>
+</div>
+</body>
+</html>