diff --git a/.gitea/workflows/tests.yaml b/.gitea/workflows/tests.yaml
index f25a07c..a906e41 100644
--- a/.gitea/workflows/tests.yaml
+++ b/.gitea/workflows/tests.yaml
@@ -68,7 +68,7 @@ jobs:
           POSTGRES_PASSWORD: ${{ secrets.DB_PASS }}
           PGUSER: ${{ vars.DB_USER }}
         ports:
-          - 5433:5432
+          - 5432:5432
         options: >-
           --health-cmd pg_isready
           --health-interval 10s
diff --git a/apps/bright/assets/css/app.scss b/apps/bright/assets/css/app.scss
index 794863f..7fa05a6 100644
--- a/apps/bright/assets/css/app.scss
+++ b/apps/bright/assets/css/app.scss
@@ -1,11 +1,15 @@
 @import "bulma";
 
 @import "variables";
-@import "@fortawesome/fontawesome-free/scss/brands";
-@import "@fortawesome/fontawesome-free/scss/regular";
-@import "@fortawesome/fontawesome-free/scss/solid";
-@import "@fortawesome/fontawesome-free/scss/fontawesome";
+// @import "@fortawesome/fontawesome-free/scss/brands";
+// @import "@fortawesome/fontawesome-free/scss/regular";
+// @import "@fortawesome/fontawesome-free/scss/solid";
+// @import "@fortawesome/fontawesome-free/scss/fontawesome";
 
+// @import "dropzone";
+
+// @import "@uppy/core/dist/style.min.css";
+// @import "@uppy/dashboard/dist/style.min.css";
 
 
 .is-unclickable {
diff --git a/apps/bright/assets/css/dropzone.scss b/apps/bright/assets/css/dropzone.scss
new file mode 100644
index 0000000..e112eef
--- /dev/null
+++ b/apps/bright/assets/css/dropzone.scss
@@ -0,0 +1,542 @@
+/* dropzone.css */
+@-webkit-keyframes passing-through {
+  0% {
+    opacity: 0;
+    -webkit-transform: translateY(40px);
+    -moz-transform: translateY(40px);
+    -ms-transform: translateY(40px);
+    -o-transform: translateY(40px);
+    transform: translateY(40px)
+  }
+
+  30%,
+  70% {
+    opacity: 1;
+    -webkit-transform: translateY(0px);
+    -moz-transform: translateY(0px);
+    -ms-transform: translateY(0px);
+    -o-transform: translateY(0px);
+    transform: translateY(0px)
+  }
+
+  100% {
+    opacity: 0;
+    -webkit-transform: translateY(-40px);
+    -moz-transform: translateY(-40px);
+    -ms-transform: translateY(-40px);
+    -o-transform: translateY(-40px);
+    transform: translateY(-40px)
+  }
+}
+
+@-moz-keyframes passing-through {
+  0% {
+    opacity: 0;
+    -webkit-transform: translateY(40px);
+    -moz-transform: translateY(40px);
+    -ms-transform: translateY(40px);
+    -o-transform: translateY(40px);
+    transform: translateY(40px)
+  }
+
+  30%,
+  70% {
+    opacity: 1;
+    -webkit-transform: translateY(0px);
+    -moz-transform: translateY(0px);
+    -ms-transform: translateY(0px);
+    -o-transform: translateY(0px);
+    transform: translateY(0px)
+  }
+
+  100% {
+    opacity: 0;
+    -webkit-transform: translateY(-40px);
+    -moz-transform: translateY(-40px);
+    -ms-transform: translateY(-40px);
+    -o-transform: translateY(-40px);
+    transform: translateY(-40px)
+  }
+}
+
+@keyframes passing-through {
+  0% {
+    opacity: 0;
+    -webkit-transform: translateY(40px);
+    -moz-transform: translateY(40px);
+    -ms-transform: translateY(40px);
+    -o-transform: translateY(40px);
+    transform: translateY(40px)
+  }
+
+  30%,
+  70% {
+    opacity: 1;
+    -webkit-transform: translateY(0px);
+    -moz-transform: translateY(0px);
+    -ms-transform: translateY(0px);
+    -o-transform: translateY(0px);
+    transform: translateY(0px)
+  }
+
+  100% {
+    opacity: 0;
+    -webkit-transform: translateY(-40px);
+    -moz-transform: translateY(-40px);
+    -ms-transform: translateY(-40px);
+    -o-transform: translateY(-40px);
+    transform: translateY(-40px)
+  }
+}
+
+@-webkit-keyframes slide-in {
+  0% {
+    opacity: 0;
+    -webkit-transform: translateY(40px);
+    -moz-transform: translateY(40px);
+    -ms-transform: translateY(40px);
+    -o-transform: translateY(40px);
+    transform: translateY(40px)
+  }
+
+  30% {
+    opacity: 1;
+    -webkit-transform: translateY(0px);
+    -moz-transform: translateY(0px);
+    -ms-transform: translateY(0px);
+    -o-transform: translateY(0px);
+    transform: translateY(0px)
+  }
+}
+
+@-moz-keyframes slide-in {
+  0% {
+    opacity: 0;
+    -webkit-transform: translateY(40px);
+    -moz-transform: translateY(40px);
+    -ms-transform: translateY(40px);
+    -o-transform: translateY(40px);
+    transform: translateY(40px)
+  }
+
+  30% {
+    opacity: 1;
+    -webkit-transform: translateY(0px);
+    -moz-transform: translateY(0px);
+    -ms-transform: translateY(0px);
+    -o-transform: translateY(0px);
+    transform: translateY(0px)
+  }
+}
+
+@keyframes slide-in {
+  0% {
+    opacity: 0;
+    -webkit-transform: translateY(40px);
+    -moz-transform: translateY(40px);
+    -ms-transform: translateY(40px);
+    -o-transform: translateY(40px);
+    transform: translateY(40px)
+  }
+
+  30% {
+    opacity: 1;
+    -webkit-transform: translateY(0px);
+    -moz-transform: translateY(0px);
+    -ms-transform: translateY(0px);
+    -o-transform: translateY(0px);
+    transform: translateY(0px)
+  }
+}
+
+@-webkit-keyframes pulse {
+  0% {
+    -webkit-transform: scale(1);
+    -moz-transform: scale(1);
+    -ms-transform: scale(1);
+    -o-transform: scale(1);
+    transform: scale(1)
+  }
+
+  10% {
+    -webkit-transform: scale(1.1);
+    -moz-transform: scale(1.1);
+    -ms-transform: scale(1.1);
+    -o-transform: scale(1.1);
+    transform: scale(1.1)
+  }
+
+  20% {
+    -webkit-transform: scale(1);
+    -moz-transform: scale(1);
+    -ms-transform: scale(1);
+    -o-transform: scale(1);
+    transform: scale(1)
+  }
+}
+
+@-moz-keyframes pulse {
+  0% {
+    -webkit-transform: scale(1);
+    -moz-transform: scale(1);
+    -ms-transform: scale(1);
+    -o-transform: scale(1);
+    transform: scale(1)
+  }
+
+  10% {
+    -webkit-transform: scale(1.1);
+    -moz-transform: scale(1.1);
+    -ms-transform: scale(1.1);
+    -o-transform: scale(1.1);
+    transform: scale(1.1)
+  }
+
+  20% {
+    -webkit-transform: scale(1);
+    -moz-transform: scale(1);
+    -ms-transform: scale(1);
+    -o-transform: scale(1);
+    transform: scale(1)
+  }
+}
+
+@keyframes pulse {
+  0% {
+    -webkit-transform: scale(1);
+    -moz-transform: scale(1);
+    -ms-transform: scale(1);
+    -o-transform: scale(1);
+    transform: scale(1)
+  }
+
+  10% {
+    -webkit-transform: scale(1.1);
+    -moz-transform: scale(1.1);
+    -ms-transform: scale(1.1);
+    -o-transform: scale(1.1);
+    transform: scale(1.1)
+  }
+
+  20% {
+    -webkit-transform: scale(1);
+    -moz-transform: scale(1);
+    -ms-transform: scale(1);
+    -o-transform: scale(1);
+    transform: scale(1)
+  }
+}
+
+.dropzone,
+.dropzone * {
+  box-sizing: border-box
+}
+
+.dropzone {
+  min-height: 150px;
+  border: 2px solid rgba(0, 0, 0, .3);
+  background: rgb(24, 26, 27);
+  padding: 20px 20px
+}
+
+.dropzone.dz-clickable {
+  cursor: pointer
+}
+
+.dropzone.dz-clickable * {
+  cursor: default
+}
+
+.dropzone.dz-clickable .dz-message,
+.dropzone.dz-clickable .dz-message * {
+  cursor: pointer
+}
+
+.dropzone.dz-started .dz-message {
+  display: none
+}
+
+.dropzone.dz-drag-hover {
+  border-style: solid
+}
+
+.dropzone.dz-drag-hover .dz-message {
+  opacity: .5
+}
+
+.dropzone .dz-message {
+  text-align: center;
+  margin: 2em 0
+}
+
+.dropzone .dz-message .dz-button {
+  background: none;
+  color: inherit;
+  border: none;
+  padding: 0;
+  font: inherit;
+  cursor: pointer;
+  outline: inherit
+}
+
+.dropzone .dz-preview {
+  position: relative;
+  display: inline-block;
+  vertical-align: top;
+  margin: 16px;
+  min-height: 100px
+}
+
+.dropzone .dz-preview:hover {
+  z-index: 1000
+}
+
+.dropzone .dz-preview:hover .dz-details {
+  opacity: 1
+}
+
+.dropzone .dz-preview.dz-file-preview .dz-image {
+  border-radius: 20px;
+  background: #999;
+  background: linear-gradient(to bottom, #eee, #ddd)
+}
+
+.dropzone .dz-preview.dz-file-preview .dz-details {
+  opacity: 1
+}
+
+.dropzone .dz-preview.dz-image-preview {
+  background: #fff
+}
+
+.dropzone .dz-preview.dz-image-preview .dz-details {
+  -webkit-transition: opacity 0.2s linear;
+  -moz-transition: opacity 0.2s linear;
+  -ms-transition: opacity 0.2s linear;
+  -o-transition: opacity 0.2s linear;
+  transition: opacity 0.2s linear
+}
+
+.dropzone .dz-preview .dz-remove {
+  font-size: 14px;
+  text-align: center;
+  display: block;
+  cursor: pointer;
+  border: none
+}
+
+.dropzone .dz-preview .dz-remove:hover {
+  text-decoration: underline
+}
+
+.dropzone .dz-preview:hover .dz-details {
+  opacity: 1
+}
+
+.dropzone .dz-preview .dz-details {
+  z-index: 20;
+  position: absolute;
+  top: 0;
+  left: 0;
+  opacity: 0;
+  font-size: 13px;
+  min-width: 100%;
+  max-width: 100%;
+  padding: 2em 1em;
+  text-align: center;
+  color: rgba(0, 0, 0, .9);
+  line-height: 150%
+}
+
+.dropzone .dz-preview .dz-details .dz-size {
+  margin-bottom: 1em;
+  font-size: 16px
+}
+
+.dropzone .dz-preview .dz-details .dz-filename {
+  white-space: nowrap
+}
+
+.dropzone .dz-preview .dz-details .dz-filename:hover span {
+  border: 1px solid rgba(200, 200, 200, .8);
+  background-color: rgba(255, 255, 255, .8)
+}
+
+.dropzone .dz-preview .dz-details .dz-filename:not(:hover) {
+  overflow: hidden;
+  text-overflow: ellipsis
+}
+
+.dropzone .dz-preview .dz-details .dz-filename:not(:hover) span {
+  border: 1px solid transparent
+}
+
+.dropzone .dz-preview .dz-details .dz-filename span,
+.dropzone .dz-preview .dz-details .dz-size span {
+  background-color: rgba(255, 255, 255, .4);
+  padding: 0 .4em;
+  border-radius: 3px
+}
+
+.dropzone .dz-preview:hover .dz-image img {
+  -webkit-transform: scale(1.05, 1.05);
+  -moz-transform: scale(1.05, 1.05);
+  -ms-transform: scale(1.05, 1.05);
+  -o-transform: scale(1.05, 1.05);
+  transform: scale(1.05, 1.05);
+  -webkit-filter: blur(8px);
+  filter: blur(8px)
+}
+
+.dropzone .dz-preview .dz-image {
+  border-radius: 20px;
+  overflow: hidden;
+  width: 120px;
+  height: 120px;
+  position: relative;
+  display: block;
+  z-index: 10
+}
+
+.dropzone .dz-preview .dz-image img {
+  display: block
+}
+
+.dropzone .dz-preview.dz-success .dz-success-mark {
+  -webkit-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);
+  -moz-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);
+  -ms-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);
+  -o-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);
+  animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1)
+}
+
+.dropzone .dz-preview.dz-error .dz-error-mark {
+  opacity: 1;
+  -webkit-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);
+  -moz-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);
+  -ms-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);
+  -o-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);
+  animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1)
+}
+
+.dropzone .dz-preview .dz-success-mark,
+.dropzone .dz-preview .dz-error-mark {
+  pointer-events: none;
+  opacity: 0;
+  z-index: 500;
+  position: absolute;
+  display: block;
+  top: 50%;
+  left: 50%;
+  margin-left: -27px;
+  margin-top: -27px
+}
+
+.dropzone .dz-preview .dz-success-mark svg,
+.dropzone .dz-preview .dz-error-mark svg {
+  display: block;
+  width: 54px;
+  height: 54px
+}
+
+.dropzone .dz-preview.dz-processing .dz-progress {
+  opacity: 1;
+  -webkit-transition: all 0.2s linear;
+  -moz-transition: all 0.2s linear;
+  -ms-transition: all 0.2s linear;
+  -o-transition: all 0.2s linear;
+  transition: all 0.2s linear
+}
+
+.dropzone .dz-preview.dz-complete .dz-progress {
+  opacity: 0;
+  -webkit-transition: opacity 0.4s ease-in;
+  -moz-transition: opacity 0.4s ease-in;
+  -ms-transition: opacity 0.4s ease-in;
+  -o-transition: opacity 0.4s ease-in;
+  transition: opacity 0.4s ease-in
+}
+
+.dropzone .dz-preview:not(.dz-processing) .dz-progress {
+  -webkit-animation: pulse 6s ease infinite;
+  -moz-animation: pulse 6s ease infinite;
+  -ms-animation: pulse 6s ease infinite;
+  -o-animation: pulse 6s ease infinite;
+  animation: pulse 6s ease infinite
+}
+
+.dropzone .dz-preview .dz-progress {
+  opacity: 1;
+  z-index: 1000;
+  pointer-events: none;
+  position: absolute;
+  height: 16px;
+  left: 50%;
+  top: 50%;
+  margin-top: -8px;
+  width: 80px;
+  margin-left: -40px;
+  background: rgba(255, 255, 255, .9);
+  -webkit-transform: scale(1);
+  border-radius: 8px;
+  overflow: hidden
+}
+
+.dropzone .dz-preview .dz-progress .dz-upload {
+  background: #333;
+  background: linear-gradient(to bottom, #666, #444);
+  position: absolute;
+  top: 0;
+  left: 0;
+  bottom: 0;
+  width: 0;
+  -webkit-transition: width 300ms ease-in-out;
+  -moz-transition: width 300ms ease-in-out;
+  -ms-transition: width 300ms ease-in-out;
+  -o-transition: width 300ms ease-in-out;
+  transition: width 300ms ease-in-out
+}
+
+.dropzone .dz-preview.dz-error .dz-error-message {
+  display: block
+}
+
+.dropzone .dz-preview.dz-error:hover .dz-error-message {
+  opacity: 1;
+  pointer-events: auto
+}
+
+.dropzone .dz-preview .dz-error-message {
+  pointer-events: none;
+  z-index: 1000;
+  position: absolute;
+  display: block;
+  display: none;
+  opacity: 0;
+  -webkit-transition: opacity 0.3s ease;
+  -moz-transition: opacity 0.3s ease;
+  -ms-transition: opacity 0.3s ease;
+  -o-transition: opacity 0.3s ease;
+  transition: opacity 0.3s ease;
+  border-radius: 8px;
+  font-size: 13px;
+  top: 130px;
+  left: -10px;
+  width: 140px;
+  background: #be2626;
+  background: linear-gradient(to bottom, #be2626, #a92222);
+  padding: .5em 1.2em;
+  color: #454a4d #202324
+}
+
+.dropzone .dz-preview .dz-error-message:after {
+  content: "";
+  position: absolute;
+  top: -6px;
+  left: 64px;
+  width: 0;
+  height: 0;
+  border-left: 6px solid transparent;
+  border-right: 6px solid transparent;
+  border-bottom: 6px solid #be2626
+}
\ No newline at end of file
diff --git a/apps/bright/assets/js/app.js b/apps/bright/assets/js/app.js
index 3650eb4..79cfff9 100644
--- a/apps/bright/assets/js/app.js
+++ b/apps/bright/assets/js/app.js
@@ -18,21 +18,24 @@
 // Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
 import "phoenix_html"
 // Establish Phoenix Socket and LiveView configuration.
-import {Socket} from "phoenix"
-import {LiveSocket} from "phoenix_live_view"
+import { Socket } from "phoenix"
+import { LiveSocket } from "phoenix_live_view"
 import topbar from "../vendor/topbar"
 import Hooks from './hooks/index.js'
-
+import Uploaders from "./uploaders.js"
 
 let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
 let liveSocket = new LiveSocket("/live", Socket, {
-  longPollFallbackMs: 2500,
-  params: {_csrf_token: csrfToken},
-  hooks: Hooks
+  longPollFallbackMs: 3000,
+  params: { _csrf_token: csrfToken },
+  hooks: Hooks,
+  uploaders: Uploaders
 })
 
+
+
 // Show progress bar on live navigation and form submits
-topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
+topbar.config({ barColors: { 0: "#29d" }, shadowColor: "rgba(0, 0, 0, .3)" })
 window.addEventListener("phx:page-loading-start", _info => topbar.show(300))
 window.addEventListener("phx:page-loading-stop", _info => topbar.hide())
 
diff --git a/apps/bright/assets/js/hooks/index.js b/apps/bright/assets/js/hooks/index.js
index 5709c05..febeb99 100644
--- a/apps/bright/assets/js/hooks/index.js
+++ b/apps/bright/assets/js/hooks/index.js
@@ -1,7 +1,9 @@
 import VideojsHook from "./videojs_hook.js"
+import UppyHook from './uppy_hook.js'
 
 let Hooks = {}
 
 Hooks.VideojsHook = VideojsHook
+Hooks.UppyHook = UppyHook
 
 export default Hooks
\ No newline at end of file
diff --git a/apps/bright/assets/js/hooks/uppy_hook.js b/apps/bright/assets/js/hooks/uppy_hook.js
new file mode 100644
index 0000000..30a92e6
--- /dev/null
+++ b/apps/bright/assets/js/hooks/uppy_hook.js
@@ -0,0 +1,139 @@
+
+import Uppy from '@uppy/core'
+import Dashboard from '@uppy/dashboard'
+import GoldenRetriever from '@uppy/golden-retriever'
+import AwsS3 from '@uppy/aws-s3'
+
+// @see https://github.com/discourse/discourse/blob/main/app/assets/javascripts/discourse/app/lib/uppy/s3-multipart.js
+
+const UppyHook = {
+
+  mounted() {
+    let live = this
+
+
+
+    const element = live.el;
+
+    // Get the value of the `data-endpoint` attribute
+    // const endpoint = element.getAttribute('data-endpoint');
+
+    new Uppy()
+      .use(Dashboard, {
+        inline: true,
+        target: '#uppy-dashboard',
+        theme: 'dark',
+        proudlyDisplayPoweredByUppy: false
+      })
+      .use(AwsS3, {
+
+
+        signPart(file, partData) {
+
+          return new Promise((resolve) => {
+            live.pushEvent('sign_part', partData, (reply, ref) => {
+              // console.log(`We got a response from sign_part pushEvent. reply=${JSON.stringify(reply)}`)
+              let { url, headers } = reply
+              // console.log(`got url=${url} and headers=${headers} from reply`)
+              resolve({
+                url,
+                headers
+              })
+            })
+
+          })
+        },
+        createMultipartUpload(file) {
+          // console.log(`createMultipartUpload with file ${JSON.stringify(file)}`)
+          let { name, type } = file.meta
+          let payload = { name, type }
+          return new Promise((resolve) => {
+            live.pushEvent('initiate_multipart_upload', payload, (reply, ref) => {
+              // console.log(`payload=${JSON.stringify(payload)}`)
+              // console.log(`initiate_multipart_upload pushEvent response callback.`)
+              // console.log(`got reply=${JSON.stringify(reply)}`)
+              // console.log(`got ref=${JSON.stringify(ref)}`)
+
+              let output = {
+                uploadId: reply?.upload_id,
+                key: reply?.key
+              }
+              resolve(output)
+            })
+          })
+        },
+        completeMultipartUpload(file, { key, uploadId, parts }) {
+          // console.log(`completeMultipartUpload with file ${JSON.stringify(file)}, parts=${JSON.stringify(parts)}`)
+          // parts = parts.map((part) => Object.assign({}, part, { etag: part.etag.replace(/^"|"$/, "") } ))
+          parts = parts.map((part) => ({
+            etag: part.ETag.replace(/"/g, ""),
+            part_number: part.PartNumber
+          }));
+          // console.log(parts)
+
+          let payload = { key, uploadId, parts }
+          return new Promise((resolve) => {
+            live.pushEvent('complete_multipart_upload', payload, (reply, ref) => {
+              // console.log(`payload=${JSON.stringify(payload)}`)
+              // console.log(`complete_multipart_upload pushEvent response callback.`)
+              // console.log(`got reply=${JSON.stringify(reply)}`)
+              // console.log(`got ref=${JSON.stringify(ref)}`)
+
+              let output = {
+                location
+              }
+              resolve(output)
+            })
+          })
+        },
+        getUploadParameters(file, options) {
+          // console.log(`getUploadParameters with file=${JSON.stringify(file)}, options=${JSON.stringify(options)}`)
+          return new Promise((resolve) => {
+            let payload = { type: file.type, name: file.name }
+            live.pushEvent('get_upload_parameters', payload, (reply, ref) => {
+              // console.log(`reply=${JSON.stringify(reply)}`)
+              let { type, method, url } = reply
+              let output = {
+                type, method, url
+              }
+              resolve(output)
+            })
+          })
+        },
+        listParts(file, { uploadId, key }) {
+          // console.log('pushing list_parts event')
+          let payload = {
+            uploadId,
+            key
+          }
+          live.pushEvent('list_parts', payload, (reply) => {
+            return reply
+          })
+        },
+        shouldUseMultipart(file) {
+          // Use multipart only for files larger than 100MiB.
+          // return file.size > 100 * 2 ** 20;
+          return file.size > 100 * 2;
+        },
+      });
+  },
+
+
+
+  beforeUpdate() { },
+
+  updated() {
+    // console.log("UppyHook updated");
+
+  },
+
+  destroyed() { },
+
+  disconnected() { },
+
+  reconnected() { }
+
+}
+
+
+export default UppyHook
\ No newline at end of file
diff --git a/apps/bright/assets/js/uploaders.js b/apps/bright/assets/js/uploaders.js
new file mode 100644
index 0000000..9ec78ce
--- /dev/null
+++ b/apps/bright/assets/js/uploaders.js
@@ -0,0 +1,26 @@
+let Uploaders = {}
+
+Uploaders.S3 = function (entries, onViewError) {
+  entries.forEach(entry => {
+    let xhr = new XMLHttpRequest()
+    onViewError(() => xhr.abort())
+    xhr.onload = () => xhr.status === 200 ? entry.progress(100) : entry.error()
+    xhr.onerror = () => entry.error()
+
+    xhr.upload.addEventListener("progress", (event) => {
+      if (event.lengthComputable) {
+        let percent = Math.round((event.loaded / event.total) * 100)
+        if (percent < 100) { entry.progress(percent) }
+      }
+    })
+
+    let url = entry.meta.url
+    xhr.open("PUT", url, true)
+    xhr.send(entry.file)
+  })
+}
+
+
+
+
+export default Uploaders;
\ No newline at end of file
diff --git a/apps/bright/assets/package-lock.json b/apps/bright/assets/package-lock.json
index 4f04720..e2edf3b 100644
--- a/apps/bright/assets/package-lock.json
+++ b/apps/bright/assets/package-lock.json
@@ -6,6 +6,12 @@
     "": {
       "dependencies": {
         "@fortawesome/fontawesome-free": "^6.7.2",
+        "@mux/upchunk": "^3.5.0",
+        "@uppy/aws-s3": "^4.2.3",
+        "@uppy/core": "^4.4.3",
+        "@uppy/dashboard": "^4.3.2",
+        "@uppy/golden-retriever": "^4.1.1",
+        "dropzone": "^6.0.0-beta.2",
         "hls.js": "^1.5.18",
         "vidstack": "^1.12.12"
       }
@@ -44,12 +50,192 @@
         "node": ">=6"
       }
     },
+    "node_modules/@mux/upchunk": {
+      "version": "3.5.0",
+      "resolved": "https://registry.npmjs.org/@mux/upchunk/-/upchunk-3.5.0.tgz",
+      "integrity": "sha512-D+TtvlujlZQjh5I+vFzJ31h5E1uVpEaLdR8M3BNaCFbVLnFMZs8J/L/fYSUyVGnyHT/yDtPHn/IHKdo3G6oSjA==",
+      "license": "MIT",
+      "dependencies": {
+        "event-target-shim": "^6.0.2",
+        "xhr": "^2.6.0"
+      }
+    },
+    "node_modules/@swc/helpers": {
+      "version": "0.2.14",
+      "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.2.14.tgz",
+      "integrity": "sha512-wpCQMhf5p5GhNg2MmGKXzUNwxe7zRiCsmqYsamez2beP7mKPCSiu+BjZcdN95yYSzO857kr0VfQewmGpS77nqA==",
+      "license": "MIT"
+    },
+    "node_modules/@transloadit/prettier-bytes": {
+      "version": "0.3.5",
+      "resolved": "https://registry.npmjs.org/@transloadit/prettier-bytes/-/prettier-bytes-0.3.5.tgz",
+      "integrity": "sha512-xF4A3d/ZyX2LJWeQZREZQw+qFX4TGQ8bGVP97OLRt6sPO6T0TNHBFTuRHOJh7RNmYOBmQ9MHxpolD9bXihpuVA==",
+      "license": "MIT"
+    },
+    "node_modules/@types/retry": {
+      "version": "0.12.2",
+      "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz",
+      "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==",
+      "license": "MIT"
+    },
     "node_modules/@types/trusted-types": {
       "version": "2.0.7",
       "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
       "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
       "license": "MIT"
     },
+    "node_modules/@uppy/aws-s3": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/@uppy/aws-s3/-/aws-s3-4.2.3.tgz",
+      "integrity": "sha512-5vNgTE85DLujOXpzC6KEwJHLSi8o96v4rwZxMvDWQuikvX4sGcGflYjBCsPaVDYUCiiDXuhI8f93zfwCUEwQ/Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@uppy/companion-client": "^4.4.1",
+        "@uppy/utils": "^6.1.1"
+      },
+      "peerDependencies": {
+        "@uppy/core": "^4.4.1"
+      }
+    },
+    "node_modules/@uppy/companion-client": {
+      "version": "4.4.1",
+      "resolved": "https://registry.npmjs.org/@uppy/companion-client/-/companion-client-4.4.1.tgz",
+      "integrity": "sha512-ardMacShsfzaIbqHEH48YlpzWZkBj1qhAj0Dvn3r31p9d0HA5xFUvAdLYrZ6ezKvZ0RcDbf0SB5qCrQMkjscXQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@uppy/utils": "^6.1.1",
+        "namespace-emitter": "^2.0.1",
+        "p-retry": "^6.1.0"
+      },
+      "peerDependencies": {
+        "@uppy/core": "^4.4.1"
+      }
+    },
+    "node_modules/@uppy/core": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/@uppy/core/-/core-4.4.3.tgz",
+      "integrity": "sha512-Ma/v9+u0xYoxFcTajBpe0TUHI0Vjw2IKgB0AUNevhgFsBRgA03nL5n8Fac8TrC0QjPkYu7h0n2xf2EgzvyxAQA==",
+      "license": "MIT",
+      "dependencies": {
+        "@transloadit/prettier-bytes": "^0.3.4",
+        "@uppy/store-default": "^4.2.0",
+        "@uppy/utils": "^6.1.2",
+        "lodash": "^4.17.21",
+        "mime-match": "^1.0.2",
+        "namespace-emitter": "^2.0.1",
+        "nanoid": "^5.0.9",
+        "preact": "^10.5.13"
+      }
+    },
+    "node_modules/@uppy/dashboard": {
+      "version": "4.3.2",
+      "resolved": "https://registry.npmjs.org/@uppy/dashboard/-/dashboard-4.3.2.tgz",
+      "integrity": "sha512-6cikgcY/TMy+Fq/v03QI1BNocfm1kOxii3kuUaxnz1SFGeuZ/55+C7KKL7SP/IdeoVwH7KV+550HThT0uwIQEw==",
+      "license": "MIT",
+      "dependencies": {
+        "@transloadit/prettier-bytes": "^0.3.4",
+        "@uppy/informer": "^4.2.1",
+        "@uppy/provider-views": "^4.4.2",
+        "@uppy/status-bar": "^4.1.2",
+        "@uppy/thumbnail-generator": "^4.1.1",
+        "@uppy/utils": "^6.1.2",
+        "classnames": "^2.2.6",
+        "lodash": "^4.17.21",
+        "memoize-one": "^6.0.0",
+        "nanoid": "^5.0.9",
+        "preact": "^10.5.13",
+        "shallow-equal": "^3.0.0"
+      },
+      "peerDependencies": {
+        "@uppy/core": "^4.4.2"
+      }
+    },
+    "node_modules/@uppy/golden-retriever": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/@uppy/golden-retriever/-/golden-retriever-4.1.1.tgz",
+      "integrity": "sha512-ZzgG2p0iS/4xAOVQjckIOO29otZpxJEaZr6aDvNvc67eW0VhRMqWQhq/1X4bULmKg2TVIW06vaECd71DucUsQw==",
+      "license": "MIT",
+      "dependencies": {
+        "@uppy/utils": "^6.1.1",
+        "lodash": "^4.17.21"
+      },
+      "peerDependencies": {
+        "@uppy/core": "^4.4.1"
+      }
+    },
+    "node_modules/@uppy/informer": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/@uppy/informer/-/informer-4.2.1.tgz",
+      "integrity": "sha512-0en8Py47pl6RMDrgUfqFoF807W5kK5AKVJNT1SkTsLiGg5anmTIMuvmNG3k6LN4cn9P/rKyEHSdGcoBBUj9u7Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@uppy/utils": "^6.1.1",
+        "preact": "^10.5.13"
+      },
+      "peerDependencies": {
+        "@uppy/core": "^4.4.1"
+      }
+    },
+    "node_modules/@uppy/provider-views": {
+      "version": "4.4.2",
+      "resolved": "https://registry.npmjs.org/@uppy/provider-views/-/provider-views-4.4.2.tgz",
+      "integrity": "sha512-YGrPJuydrksmMCjvo7Ty7/lDLNo/Y8zsOgWgWmVbXB0V5aRvqY49LeKY8HDlOXclKmn6dl5CeQFf7p46txRNGQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@uppy/utils": "^6.1.2",
+        "classnames": "^2.2.6",
+        "nanoid": "^5.0.9",
+        "p-queue": "^8.0.0",
+        "preact": "^10.5.13"
+      },
+      "peerDependencies": {
+        "@uppy/core": "^4.4.2"
+      }
+    },
+    "node_modules/@uppy/status-bar": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/@uppy/status-bar/-/status-bar-4.1.2.tgz",
+      "integrity": "sha512-Z2fDXItoE940uMo3kwdDo4ZFPjTk5GY6y/C/G5+tSl6nL/IaDtWo5iVbAKHIH4s9SIRwCBgllEhxbmEPhuK7eA==",
+      "license": "MIT",
+      "dependencies": {
+        "@transloadit/prettier-bytes": "^0.3.4",
+        "@uppy/utils": "^6.1.2",
+        "classnames": "^2.2.6",
+        "preact": "^10.5.13"
+      },
+      "peerDependencies": {
+        "@uppy/core": "^4.4.2"
+      }
+    },
+    "node_modules/@uppy/store-default": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/@uppy/store-default/-/store-default-4.2.0.tgz",
+      "integrity": "sha512-PieFVa8yTvRHIqsNKfpO/yaJw5Ae/hT7uT58ryw7gvCBY5bHrNWxH5N0XFe8PFHMpLpLn8v3UXGx9ib9QkB6+Q==",
+      "license": "MIT"
+    },
+    "node_modules/@uppy/thumbnail-generator": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/@uppy/thumbnail-generator/-/thumbnail-generator-4.1.1.tgz",
+      "integrity": "sha512-65znkGNgVTbVte51IKOhgxOpHGSwYj9Qik2jF2ZBocMbhBY4gPkWFwqMrKQBfddA9KbUa4jVe1psxhAQTzYgiA==",
+      "license": "MIT",
+      "dependencies": {
+        "@uppy/utils": "^6.1.1",
+        "exifr": "^7.0.0"
+      },
+      "peerDependencies": {
+        "@uppy/core": "^4.4.1"
+      }
+    },
+    "node_modules/@uppy/utils": {
+      "version": "6.1.2",
+      "resolved": "https://registry.npmjs.org/@uppy/utils/-/utils-6.1.2.tgz",
+      "integrity": "sha512-PCrw6v51M6p3hlrlB2INmcocen4Dyjun1SobjVZRBkg4wutQE8ihZfSrH5ZE8UXFelufhtO16wlaZMi0EHk84w==",
+      "license": "MIT",
+      "dependencies": {
+        "lodash": "^4.17.21",
+        "preact": "^10.5.13"
+      }
+    },
     "node_modules/acorn": {
       "version": "8.14.0",
       "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
@@ -62,12 +248,91 @@
         "node": ">=0.4.0"
       }
     },
+    "node_modules/classnames": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
+      "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
+      "license": "MIT"
+    },
+    "node_modules/dom-walk": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz",
+      "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w=="
+    },
+    "node_modules/dropzone": {
+      "version": "6.0.0-beta.2",
+      "resolved": "https://registry.npmjs.org/dropzone/-/dropzone-6.0.0-beta.2.tgz",
+      "integrity": "sha512-k44yLuFFhRk53M8zP71FaaNzJYIzr99SKmpbO/oZKNslDjNXQsBTdfLs+iONd0U0L94zzlFzRnFdqbLcs7h9fQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@swc/helpers": "^0.2.13",
+        "just-extend": "^5.0.0"
+      }
+    },
+    "node_modules/event-target-shim": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-6.0.2.tgz",
+      "integrity": "sha512-8q3LsZjRezbFZ2PN+uP+Q7pnHUMmAOziU2vA2OwoFaKIXxlxl38IylhSSgUorWu/rf4er67w0ikBqjBFk/pomA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.13.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/mysticatea"
+      }
+    },
+    "node_modules/eventemitter3": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
+      "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
+      "license": "MIT"
+    },
+    "node_modules/exifr": {
+      "version": "7.1.3",
+      "resolved": "https://registry.npmjs.org/exifr/-/exifr-7.1.3.tgz",
+      "integrity": "sha512-g/aje2noHivrRSLbAUtBPWFbxKdKhgj/xr1vATDdUXPOFYJlQ62Ft0oy+72V6XLIpDJfHs6gXLbBLAolqOXYRw==",
+      "license": "MIT"
+    },
+    "node_modules/global": {
+      "version": "4.4.0",
+      "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz",
+      "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==",
+      "license": "MIT",
+      "dependencies": {
+        "min-document": "^2.19.0",
+        "process": "^0.11.10"
+      }
+    },
     "node_modules/hls.js": {
       "version": "1.5.20",
       "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.5.20.tgz",
       "integrity": "sha512-uu0VXUK52JhihhnN/MVVo1lvqNNuhoxkonqgO3IpjvQiGpJBdIXMGkofjQb/j9zvV7a1SW8U9g1FslWx/1HOiQ==",
       "license": "Apache-2.0"
     },
+    "node_modules/is-function": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz",
+      "integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==",
+      "license": "MIT"
+    },
+    "node_modules/is-network-error": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.1.0.tgz",
+      "integrity": "sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/just-extend": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-5.1.1.tgz",
+      "integrity": "sha512-b+z6yF1d4EOyDgylzQo5IminlUmzSeqR1hs/bzjBNjuGras4FXq/6TrzjxfN0j+TmI0ltJzTNlqXUMCniciwKQ==",
+      "license": "MIT"
+    },
     "node_modules/lit-html": {
       "version": "2.8.0",
       "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.8.0.tgz",
@@ -77,6 +342,12 @@
         "@types/trusted-types": "^2.0.2"
       }
     },
+    "node_modules/lodash": {
+      "version": "4.17.21",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+      "license": "MIT"
+    },
     "node_modules/media-captions": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/media-captions/-/media-captions-1.0.4.tgz",
@@ -86,6 +357,138 @@
         "node": ">=16"
       }
     },
+    "node_modules/memoize-one": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
+      "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
+      "license": "MIT"
+    },
+    "node_modules/mime-match": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/mime-match/-/mime-match-1.0.2.tgz",
+      "integrity": "sha512-VXp/ugGDVh3eCLOBCiHZMYWQaTNUHv2IJrut+yXA6+JbLPXHglHwfS/5A5L0ll+jkCY7fIzRJcH6OIunF+c6Cg==",
+      "license": "ISC",
+      "dependencies": {
+        "wildcard": "^1.1.0"
+      }
+    },
+    "node_modules/min-document": {
+      "version": "2.19.0",
+      "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz",
+      "integrity": "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==",
+      "dependencies": {
+        "dom-walk": "^0.1.0"
+      }
+    },
+    "node_modules/namespace-emitter": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/namespace-emitter/-/namespace-emitter-2.0.1.tgz",
+      "integrity": "sha512-N/sMKHniSDJBjfrkbS/tpkPj4RAbvW3mr8UAzvlMHyun93XEm83IAvhWtJVHo+RHn/oO8Job5YN4b+wRjSVp5g==",
+      "license": "MIT"
+    },
+    "node_modules/nanoid": {
+      "version": "5.1.5",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.5.tgz",
+      "integrity": "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "bin": {
+        "nanoid": "bin/nanoid.js"
+      },
+      "engines": {
+        "node": "^18 || >=20"
+      }
+    },
+    "node_modules/p-queue": {
+      "version": "8.1.0",
+      "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-8.1.0.tgz",
+      "integrity": "sha512-mxLDbbGIBEXTJL0zEx8JIylaj3xQ7Z/7eEVjcF9fJX4DBiH9oqe+oahYnlKKxm0Ci9TlWTyhSHgygxMxjIB2jw==",
+      "license": "MIT",
+      "dependencies": {
+        "eventemitter3": "^5.0.1",
+        "p-timeout": "^6.1.2"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/p-retry": {
+      "version": "6.2.1",
+      "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz",
+      "integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/retry": "0.12.2",
+        "is-network-error": "^1.0.0",
+        "retry": "^0.13.1"
+      },
+      "engines": {
+        "node": ">=16.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/p-timeout": {
+      "version": "6.1.4",
+      "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-6.1.4.tgz",
+      "integrity": "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=14.16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/parse-headers": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.6.tgz",
+      "integrity": "sha512-Tz11t3uKztEW5FEVZnj1ox8GKblWn+PvHY9TmJV5Mll2uHEwRdR/5Li1OlXoECjLYkApdhWy44ocONwXLiKO5A==",
+      "license": "MIT"
+    },
+    "node_modules/preact": {
+      "version": "10.26.4",
+      "resolved": "https://registry.npmjs.org/preact/-/preact-10.26.4.tgz",
+      "integrity": "sha512-KJhO7LBFTjP71d83trW+Ilnjbo+ySsaAgCfXOXUlmGzJ4ygYPWmysm77yg4emwfmoz3b22yvH5IsVFHbhUaH5w==",
+      "license": "MIT",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/preact"
+      }
+    },
+    "node_modules/process": {
+      "version": "0.11.10",
+      "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
+      "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6.0"
+      }
+    },
+    "node_modules/retry": {
+      "version": "0.13.1",
+      "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
+      "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 4"
+      }
+    },
+    "node_modules/shallow-equal": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-3.1.0.tgz",
+      "integrity": "sha512-pfVOw8QZIXpMbhBWvzBISicvToTiM5WBF1EeAUZDDSb5Dt29yl4AYbyywbJFSEsRUMr7gJaxqCdr4L3tQf9wVg==",
+      "license": "MIT"
+    },
     "node_modules/unplugin": {
       "version": "1.16.1",
       "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.16.1.tgz",
@@ -119,6 +522,33 @@
       "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
       "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
       "license": "MIT"
+    },
+    "node_modules/wildcard": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-1.1.2.tgz",
+      "integrity": "sha512-DXukZJxpHA8LuotRwL0pP1+rS6CS7FF2qStDDE1C7DDg2rLud2PXRMuEDYIPhgEezwnlHNL4c+N6MfMTjCGTng==",
+      "license": "MIT"
+    },
+    "node_modules/xhr": {
+      "version": "2.6.0",
+      "resolved": "https://registry.npmjs.org/xhr/-/xhr-2.6.0.tgz",
+      "integrity": "sha512-/eCGLb5rxjx5e3mF1A7s+pLlR6CGyqWN91fv1JgER5mVWg1MZmlhBvy9kjcsOdRk8RrIujotWyJamfyrp+WIcA==",
+      "license": "MIT",
+      "dependencies": {
+        "global": "~4.4.0",
+        "is-function": "^1.0.1",
+        "parse-headers": "^2.0.0",
+        "xtend": "^4.0.0"
+      }
+    },
+    "node_modules/xtend": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+      "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4"
+      }
     }
   }
 }
diff --git a/apps/bright/assets/package.json b/apps/bright/assets/package.json
index 48ebcbe..e75d469 100644
--- a/apps/bright/assets/package.json
+++ b/apps/bright/assets/package.json
@@ -1,6 +1,12 @@
 {
   "dependencies": {
     "@fortawesome/fontawesome-free": "^6.7.2",
+    "@mux/upchunk": "^3.5.0",
+    "@uppy/aws-s3": "^4.2.3",
+    "@uppy/core": "^4.4.3",
+    "@uppy/dashboard": "^4.3.2",
+    "@uppy/golden-retriever": "^4.1.1",
+    "dropzone": "^6.0.0-beta.2",
     "hls.js": "^1.5.18",
     "vidstack": "^1.12.12"
   }
diff --git a/apps/bright/config/config.exs b/apps/bright/config/config.exs
index f0a8de2..f0898e8 100644
--- a/apps/bright/config/config.exs
+++ b/apps/bright/config/config.exs
@@ -71,7 +71,7 @@ config :bright, Bright.Mailer, adapter: Swoosh.Adapters.Local
 
 # Configure esbuild (the version is required)
 config :esbuild,
-  version: "0.17.11",
+  version: "0.25.1",
   bright: [
     args:
       ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),
diff --git a/apps/bright/lib/bright/oban_workers/process_posts.ex b/apps/bright/lib/bright/oban_workers/process_posts.ex
index 9d1fa5d..a47300c 100644
--- a/apps/bright/lib/bright/oban_workers/process_posts.ex
+++ b/apps/bright/lib/bright/oban_workers/process_posts.ex
@@ -5,12 +5,10 @@ defmodule Bright.ObanWorkers.ProcessPosts do
 
   use Oban.Worker, queue: :default, max_attempts: 3
 
-  alias Bright.Vtubers.Vtuber
   alias Bright.Repo
   alias Bright.Socials.XPost
   alias Bright.Streams.Stream
   alias Bright.Platforms.Platform
-  alias Bright.Platforms
   import Ecto.Query
 
   require Logger
@@ -26,28 +24,30 @@ defmodule Bright.ObanWorkers.ProcessPosts do
 
     known_platforms = Platform |> Repo.all() |> Repo.preload(:platform_aliases)
 
-    {num, nil} =
-      XPost.get_unprocessed_posts()
-      |> then(fn posts ->
-        if posts == [] do
-          Logger.info("No unprocessed posts found")
-        else
-          Logger.debug("#{length(posts)} unprocessed posts found.")
-          Enum.each(posts, &process_post(&1, known_platforms))
-          mark_posts_as_processed(posts)
-        end
-      end)
+    posts = XPost.get_unprocessed_posts()
+    num = length(posts)
+
+    if posts == [] do
+      Logger.info("No unprocessed posts found")
+    else
+      Logger.debug("#{num} unprocessed posts found.")
+      Enum.each(posts, &process_post(&1, known_platforms))
+      mark_posts_as_processed(posts)
+    end
 
     {:ok, num}
   end
 
   def process_post(post, known_platforms) do
-    with %{is_nsfw_live_announcement: is_live, platforms_mentioned: platforms} <-
-           XPost.parse(post, known_platforms),
-         {:ok, _stream} <- create_stream(post, platforms) do
-      :ok
-    else
-      idk ->
+    case XPost.parse(post, known_platforms) do
+      %{is_nsfw_live_announcement: true, platforms_mentioned: platforms_mentioned} ->
+        create_stream(post, platforms_mentioned)
+
+      # %{is_nsfw_live_announcement: false, platforms_mentioned: _} ->
+      #   Logger.warning("is_nsfw_live_announcement was false for post with raw=#{post.raw}")
+      #   :ok
+
+      _ ->
         Logger.debug(
           "process_post did not find a nsfw live announcement. post=#{inspect(post)} known_platforms=#{inspect(known_platforms)}"
         )
@@ -64,9 +64,7 @@ defmodule Bright.ObanWorkers.ProcessPosts do
     date = post |> Map.get(:date)
     title = "#{post.vtuber.display_name} #{date}"
 
-    Logger.debug(
-      "WE ARE CREATING A Stream with platforms=#{inspect(platforms)} title=#{inspect(title)} with date=#{inspect(date)} post=#{inspect(post)}"
-    )
+    Logger.warning("WE ARE CREATING A Stream with post.raw=#{inspect(post.raw)}")
 
     changeset =
       %Stream{}
@@ -80,7 +78,7 @@ defmodule Bright.ObanWorkers.ProcessPosts do
         Logger.info("Created stream: #{inspect(stream)}")
 
       {:error, %Ecto.Changeset{errors: [date: {"has already been taken", _}]}} ->
-        Logger.warn("Stream #{title} already exists. Skipping...")
+        Logger.warning("Stream #{title} already exists. Skipping...")
 
       {:error, changeset} ->
         Logger.error(
diff --git a/apps/bright/lib/bright/platforms.ex b/apps/bright/lib/bright/platforms.ex
index 3f842b1..53a7cc9 100644
--- a/apps/bright/lib/bright/platforms.ex
+++ b/apps/bright/lib/bright/platforms.ex
@@ -109,26 +109,6 @@ defmodule Bright.Platforms do
     Platform.changeset(platform, attrs)
   end
 
-  @doc """
-  Creates a platform alias.
-
-  ## Examples
-
-      iex> create_platform_alias(%{field: value})
-      {:ok, %PlatformAlias{}}
-
-      iex> create_platform(%{field: bad_value})
-      {:error, %Ecto.Changeset{}}
-
-  """
-  def create_platform_alias(attrs \\ %{}) do
-    IO.inspect(attrs, label: "PlatformAlias Attributes Before Insert")
-
-    %PlatformAlias{}
-    |> PlatformAlias.changeset(attrs)
-    |> Repo.insert()
-  end
-
   def match_platform?(a, b) do
     host_a = URI.parse(a.url).host
     host_b = URI.parse(b.url).host
@@ -147,4 +127,119 @@ defmodule Bright.Platforms do
     Logger.debug("contains_platform? a=#{inspect(a)} b=#{inspect(b)}")
     Enum.any?(a, fn plat -> Enum.any?(b, &match_platform?(plat, &1)) end)
   end
+
+  alias Bright.Platforms.PlatformAlias
+
+  @doc """
+  Returns the list of platform_aliases.
+
+  ## Examples
+
+      iex> list_platform_aliases()
+      [%PlatformAlias{}, ...]
+
+  """
+  def list_platform_aliases do
+    PlatformAlias
+    |> Repo.all()
+    |> Repo.preload([:platform])
+  end
+
+  # def list_streams do
+  #   Stream
+  #   |> Repo.all()
+  #   |> Repo.preload([:tags, :vods, :vtubers, :platforms, :x_post])
+  # end
+
+  @doc """
+  Gets a single platform_alias.
+
+  Raises `Ecto.NoResultsError` if the Platform alias does not exist.
+
+  ## Examples
+
+      iex> get_platform_alias!(123)
+      %PlatformAlias{}
+
+      iex> get_platform_alias!(456)
+      ** (Ecto.NoResultsError)
+
+  """
+  def get_platform_alias!(id) do
+    PlatformAlias
+    |> Repo.get!(id)
+    |> Repo.preload([:platform])
+  end
+
+  # reference
+  # def get_stream!(id) do
+  #   Stream
+  #   |> Repo.get!(id)
+  #   |> Repo.preload([:tags, :vods, :vtubers, :platforms, :x_post])
+  # end
+
+  @doc """
+  Creates a platform_alias.
+
+  ## Examples
+
+      iex> create_platform_alias(%{field: value})
+      {:ok, %PlatformAlias{}}
+
+      iex> create_platform_alias(%{field: bad_value})
+      {:error, %Ecto.Changeset{}}
+
+  """
+  def create_platform_alias(attrs \\ %{}) do
+    %PlatformAlias{}
+    |> PlatformAlias.changeset(attrs)
+    |> Repo.insert()
+  end
+
+  @doc """
+  Updates a platform_alias.
+
+  ## Examples
+
+      iex> update_platform_alias(platform_alias, %{field: new_value})
+      {:ok, %PlatformAlias{}}
+
+      iex> update_platform_alias(platform_alias, %{field: bad_value})
+      {:error, %Ecto.Changeset{}}
+
+  """
+  def update_platform_alias(%PlatformAlias{} = platform_alias, attrs) do
+    platform_alias
+    |> PlatformAlias.changeset(attrs)
+    |> Repo.update()
+  end
+
+  @doc """
+  Deletes a platform_alias.
+
+  ## Examples
+
+      iex> delete_platform_alias(platform_alias)
+      {:ok, %PlatformAlias{}}
+
+      iex> delete_platform_alias(platform_alias)
+      {:error, %Ecto.Changeset{}}
+
+  """
+  def delete_platform_alias(%PlatformAlias{} = platform_alias) do
+    Repo.delete(platform_alias)
+  end
+
+  @doc """
+  Returns an `%Ecto.Changeset{}` for tracking platform_alias changes.
+
+  ## Examples
+
+      iex> change_platform_alias(platform_alias)
+      %Ecto.Changeset{data: %PlatformAlias{}}
+
+  """
+  def change_platform_alias(%PlatformAlias{} = platform_alias, attrs \\ %{}) do
+    PlatformAlias.changeset(platform_alias, attrs)
+  end
 end
diff --git a/apps/bright/lib/bright/socials/x_post.ex b/apps/bright/lib/bright/socials/x_post.ex
index 1b8b95a..ba58b73 100644
--- a/apps/bright/lib/bright/socials/x_post.ex
+++ b/apps/bright/lib/bright/socials/x_post.ex
@@ -6,6 +6,7 @@ defmodule Bright.Socials.XPost do
   alias Bright.Socials.{XPost, RSSParser}
   alias Bright.Platforms.Platform
   alias Bright.Platforms
+  alias Bright.Streams.Stream
   alias Quinn
   require Logger
 
@@ -113,7 +114,7 @@ defmodule Bright.Socials.XPost do
   """
   def includes_platform?(
         raw_text,
-        %Platform{url: url, platform_aliases: %Ecto.Association.NotLoaded{}} = platform
+        %Platform{platform_aliases: %Ecto.Association.NotLoaded{}} = platform
       ) do
     platform = Repo.preload(platform, :platform_aliases)
 
@@ -178,7 +179,7 @@ defmodule Bright.Socials.XPost do
     parse(post, known_platforms)
   end
 
-  def parse(%XPost{vtuber: vtuber} = post, known_platforms) when is_list(known_platforms) do
+  def parse(%XPost{} = post, known_platforms) when is_list(known_platforms) do
     is_nsfw_live_announcement = is_nsfw_live_announcement?(post, known_platforms)
     platforms_mentioned = get_platforms_mentioned(post, known_platforms)
 
@@ -195,7 +196,7 @@ defmodule Bright.Socials.XPost do
   To be considered a NSFW live announcement, a post must satisfy all the following conditions.
 
     * The post is authored by the lewdtuber
-    * The post does not contain, "VOD/i"
+    * The post does not contain, "VOD/i" or "/post"
     * The post mentions a NSFW platform
     * The post does not mention any SFW streaming platform.
 
@@ -216,7 +217,7 @@ defmodule Bright.Socials.XPost do
 
     conditions = [
       {:is_not_rt, XPost.authored_by_vtuber?(post, vtuber)},
-      {:is_not_vod, not String.contains?(String.downcase(post.raw), "vod")},
+      {:is_not_vod, not String.contains?(String.downcase(post.raw), ["vod", "/post"])},
       {:is_nsfw_platform, Platforms.contains_platform?(nsfw_platforms, mentioned_platforms)},
       {:is_no_sfw_platform, not Platforms.contains_platform?(mentioned_platforms, sfw_platforms)}
     ]
diff --git a/apps/bright/lib/bright/streams.ex b/apps/bright/lib/bright/streams.ex
index 703c965..2b6e32f 100644
--- a/apps/bright/lib/bright/streams.ex
+++ b/apps/bright/lib/bright/streams.ex
@@ -33,6 +33,7 @@ defmodule Bright.Streams do
   """
   def list_streams do
     Stream
+    |> order_by(desc: :date)
     |> Repo.all()
     |> Repo.preload([:tags, :vods, :vtubers, :platforms, :x_post])
   end
@@ -602,4 +603,100 @@ defmodule Bright.Streams do
 
     Repo.aggregate(query, :count, :id)
   end
+
+  alias Bright.Streams.Upload
+
+  @doc """
+  Returns the list of uploads.
+
+  ## Examples
+
+      iex> list_uploads()
+      [%Upload{}, ...]
+
+  """
+  def list_uploads do
+    Repo.all(Upload)
+  end
+
+  @doc """
+  Gets a single upload.
+
+  Raises `Ecto.NoResultsError` if the Upload does not exist.
+
+  ## Examples
+
+      iex> get_upload!(123)
+      %Upload{}
+
+      iex> get_upload!(456)
+      ** (Ecto.NoResultsError)
+
+  """
+  def get_upload!(id), do: Repo.get!(Upload, id)
+
+  @doc """
+  Creates a upload.
+
+  ## Examples
+
+      iex> create_upload(%{field: value})
+      {:ok, %Upload{}}
+
+      iex> create_upload(%{field: bad_value})
+      {:error, %Ecto.Changeset{}}
+
+  """
+  def create_upload(attrs \\ %{}) do
+    %Upload{}
+    |> Upload.changeset(attrs)
+    |> Repo.insert()
+  end
+
+  @doc """
+  Updates a upload.
+
+  ## Examples
+
+      iex> update_upload(upload, %{field: new_value})
+      {:ok, %Upload{}}
+
+      iex> update_upload(upload, %{field: bad_value})
+      {:error, %Ecto.Changeset{}}
+
+  """
+  def update_upload(%Upload{} = upload, attrs) do
+    upload
+    |> Upload.changeset(attrs)
+    |> Repo.update()
+  end
+
+  @doc """
+  Deletes a upload.
+
+  ## Examples
+
+      iex> delete_upload(upload)
+      {:ok, %Upload{}}
+
+      iex> delete_upload(upload)
+      {:error, %Ecto.Changeset{}}
+
+  """
+  def delete_upload(%Upload{} = upload) do
+    Repo.delete(upload)
+  end
+
+  @doc """
+  Returns an `%Ecto.Changeset{}` for tracking upload changes.
+
+  ## Examples
+
+      iex> change_upload(upload)
+      %Ecto.Changeset{data: %Upload{}}
+
+  """
+  def change_upload(%Upload{} = upload, attrs \\ %{}) do
+    Upload.changeset(upload, attrs)
+  end
 end
diff --git a/apps/bright/lib/bright/streams/upload.ex b/apps/bright/lib/bright/streams/upload.ex
new file mode 100644
index 0000000..14b7ced
--- /dev/null
+++ b/apps/bright/lib/bright/streams/upload.ex
@@ -0,0 +1,21 @@
+defmodule Bright.Streams.Upload do
+  use Ecto.Schema
+  import Ecto.Changeset
+
+  schema "uploads" do
+    field :size, :integer
+    field :filename, :string
+    field :content_type, :string
+    field :user_id, :id
+    field :vod, :id
+
+    timestamps(type: :utc_datetime)
+  end
+
+  @doc false
+  def changeset(upload, attrs) do
+    upload
+    |> cast(attrs, [:filename, :size, :content_type])
+    |> validate_required([:filename, :size, :content_type])
+  end
+end
diff --git a/apps/bright/lib/bright/utils.ex b/apps/bright/lib/bright/utils.ex
new file mode 100644
index 0000000..d8c210b
--- /dev/null
+++ b/apps/bright/lib/bright/utils.ex
@@ -0,0 +1,17 @@
+defmodule Bright.Utils do
+  @chars ~c"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
+
+  @doc """
+  Generates a random string of the given length.
+
+  ## Parameters
+    - length: The length of the random string to generate.
+
+  ## Examples
+      iex> RandomString.generate(12)
+      "aB3dEfG7hIjK"
+  """
+  def random_string(length \\ 12) do
+    for _ <- 1..length, into: "", do: <<Enum.random(@chars)>>
+  end
+end
diff --git a/apps/bright/lib/bright_web.ex b/apps/bright/lib/bright_web.ex
index e80a48b..32794bb 100644
--- a/apps/bright/lib/bright_web.ex
+++ b/apps/bright/lib/bright_web.ex
@@ -85,7 +85,6 @@ defmodule BrightWeb do
       import Phoenix.HTML
       # Core UI components and translation
       import BrightWeb.CoreComponents
-      import BrightWeb.SVGIcon
       import BrightWeb.Gettext
 
       # Shortcut for generating JS commands
diff --git a/apps/bright/lib/bright_web/components/core_components.ex b/apps/bright/lib/bright_web/components/core_components.ex
index 8ceea4d..c8b7c61 100644
--- a/apps/bright/lib/bright_web/components/core_components.ex
+++ b/apps/bright/lib/bright_web/components/core_components.ex
@@ -18,7 +18,6 @@ defmodule BrightWeb.CoreComponents do
   use Gettext, backend: BrightWeb.Gettext
 
   alias Phoenix.LiveView.JS
-  import BrightWeb.SVGIcon
 
   @doc """
   Renders an external link.
@@ -628,15 +627,11 @@ defmodule BrightWeb.CoreComponents do
   # attr :style, :string, default: "solid"
   # attr :class, :string, default: ""
 
-  # def icon(assigns) do
-  #   ~H"""
-  #   <i class={"fa#{style_prefix(@style)} fa-#{@name} #{@class}"}></i>
-  #   """
-  # end
-
-  defp style_prefix("solid"), do: "s"
-  defp style_prefix("brands"), do: "b"
-  defp style_prefix(_), do: "s"
+  def icon(assigns) do
+    ~H"""
+    <span>🇪</span>
+    """
+  end
 
   ## JS Commands
 
diff --git a/apps/bright/lib/bright_web/components/layouts/app.html.heex b/apps/bright/lib/bright_web/components/layouts/app.html.heex
index 9113baf..bafb346 100644
--- a/apps/bright/lib/bright_web/components/layouts/app.html.heex
+++ b/apps/bright/lib/bright_web/components/layouts/app.html.heex
@@ -51,7 +51,9 @@
 
       <div class="navbar-end">
         <div class="navbar-item">
-          <.icon name="person_digging" type="solid" class="h-4 w-4 is-unclickable"/>
+          <.icon name="person_digging" type="solid" class="h-4 w-4 is-unclickable" />
+          <.icon name="magnet" type="solid" class="h-4 w-4 is-unclickable" />
+          <.icon name="circle-exclamation" type="solid" class="h-4 w-4 is-unclickable" />
           <.icon name="hammer" class="is-unclickable" />
         </div>
 
diff --git a/apps/bright/lib/bright_web/components/svg_icon.ex b/apps/bright/lib/bright_web/components/svg_icon.ex
deleted file mode 100644
index 6e4e435..0000000
--- a/apps/bright/lib/bright_web/components/svg_icon.ex
+++ /dev/null
@@ -1,122 +0,0 @@
-defmodule BrightWeb.SVGIcon do
-  @moduledoc """
-  This package adds a convenient way of using svg icons with your Phoenix, Phoenix LiveView and Surface applications.
-
-  greets https://github.com/miguel-s/ex_heroicons/blob/main/lib/heroicons.ex
-
-  ## Usage
-
-      <SVGIcons.icon name="chaturbate" class="h-4 w-4" />
-
-  ## Config
-
-  Defaults can be set in the application configuration.
-
-      config :bright, :icons_type: "outline"
-  """
-
-  use Phoenix.Component
-  alias BrightWeb.SVGIcon.Icon
-
-  svg_icons_path = "priv/static/assets/icons"
-
-  unless File.exists?(svg_icons_path) do
-    raise """
-    SVG icons not found. Expected to load them from #{svg_icons_path}.
-    """
-  end
-
-  icon_paths =
-    svg_icons_path
-    |> Path.join("**/*.svg")
-    |> Path.wildcard()
-
-  icons =
-    for icon_path <- icon_paths do
-      @external_resource Path.relative_to_cwd(icon_path)
-      Icon.parse!(icon_path)
-    end
-
-  types = icons |> Enum.map(& &1.type) |> Enum.uniq()
-  names = icons |> Enum.map(& &1.name) |> Enum.uniq()
-
-  default_type =
-    case Application.compile_env(:bright, :icons_type) do
-      nil ->
-        "solid"
-
-      type when is_binary(type) ->
-        if type in types do
-          type
-        else
-          raise ArgumentError,
-                "expected default type to be one of #{inspect(types)}, got: #{inspect(type)}"
-        end
-
-      type ->
-        raise ArgumentError,
-              "expected default type to be one of #{inspect(types)}, got: #{inspect(type)}"
-    end
-
-  @names names
-  def names, do: @names
-
-  @types types
-  def types, do: @types
-
-  attr :name, :string, values: @names, required: true, doc: "the name of the icon"
-  attr :type, :string, values: @types, default: default_type, doc: "the type of the icon"
-  attr :class, :string, default: nil, doc: "the css classes to add to the svg container"
-  attr :rest, :global, doc: "the arbitrary HTML attributes to add to the svg container"
-
-  def icon(assigns) do
-    name = assigns[:name]
-
-    if name == nil or name not in @names do
-      raise ArgumentError,
-            "expected icon name to be one of #{inspect(unquote(@names))}, got: #{inspect(name)}"
-    end
-
-    type = assigns[:type]
-
-    if type == nil or type not in @types do
-      raise ArgumentError,
-            "expected icon type to be one of #{inspect(unquote(@types))}, got: #{inspect(type)}"
-    end
-
-    ~H"""
-    <span class="icon">
-      <.svg_container focusable="false" type={@type} class={@class} {@rest}>
-        <%= {:safe, svg_body(@name, @type)} %>
-      </.svg_container>
-    </span>
-    """
-  end
-
-  attr :type, :string, values: @types, default: default_type, doc: "the type of the icon"
-  attr :class, :string, default: nil, doc: "the css classes to add to the svg container"
-  attr :rest, :global, doc: "the arbitrary HTML attributes to add to the svg container"
-
-  slot :inner_block, required: true, doc: "the svg to render"
-
-  defp svg_container(assigns) do
-    ~H"""
-    <%= render_slot(@inner_block) %>
-    """
-  end
-
-  defp svg_viewbox(type) do
-    case type do
-      "micro" -> "0 0 16 16"
-      "mini" -> "0 0 20 20"
-      "solid" -> "0 0 24 24"
-      "outline" -> "0 0 24 24"
-    end
-  end
-
-  for %Icon{name: name, type: type, file: file} <- icons do
-    defp svg_body(unquote(name), unquote(type)) do
-      unquote(file)
-    end
-  end
-end
diff --git a/apps/bright/lib/bright_web/components/svg_icon/icon.ex b/apps/bright/lib/bright_web/components/svg_icon/icon.ex
deleted file mode 100644
index ef02f8f..0000000
--- a/apps/bright/lib/bright_web/components/svg_icon/icon.ex
+++ /dev/null
@@ -1,44 +0,0 @@
-defmodule BrightWeb.SVGIcon.Icon do
-  @moduledoc """
-  This module defines the data structure and functions for working with icons stored as SVG files.
-  """
-
-  alias __MODULE__
-
-  @doc """
-  Defines the SVGIcon.Icon struct.
-
-  Its fields are:
-
-    * `:type` - the type of the icon
-    * `:name` - the name of the icon
-    * `:file` - the binary content of the file
-
-  """
-  defstruct [:type, :name, :file]
-
-  @type t :: %Icon{type: String.t(), name: String.t(), file: binary}
-
-  @doc "Parses a SVG file and returns structured data"
-  @spec parse!(String.t()) :: Icon.t()
-  def parse!(filename) do
-    [type, name] =
-      filename
-      |> Path.split()
-      |> Enum.take(-2)
-      |> case do
-        ["solid", name] -> ["solid", name]
-        ["outline", name] -> ["outline", name]
-      end
-
-    name = Path.rootname(name)
-
-    file =
-      filename
-      |> File.read!()
-      |> String.split("\n")
-      |> Enum.map(&String.trim/1)
-
-    %__MODULE__{type: type, name: name, file: file}
-  end
-end
diff --git a/apps/bright/lib/bright_web/controllers/page_controller.ex b/apps/bright/lib/bright_web/controllers/page_controller.ex
index dabebfd..1d874d6 100644
--- a/apps/bright/lib/bright_web/controllers/page_controller.ex
+++ b/apps/bright/lib/bright_web/controllers/page_controller.ex
@@ -38,6 +38,10 @@ defmodule BrightWeb.PageController do
     json(conn, data)
   end
 
+  def upload(conn, _params) do
+    render(conn, :upload)
+  end
+
   def redirect_test(conn, _params) do
     render(conn, :home)
   end
diff --git a/apps/bright/lib/bright_web/controllers/page_html/about.html.heex b/apps/bright/lib/bright_web/controllers/page_html/about.html.heex
index 004b3e5..79f33da 100644
--- a/apps/bright/lib/bright_web/controllers/page_html/about.html.heex
+++ b/apps/bright/lib/bright_web/controllers/page_html/about.html.heex
@@ -8,8 +8,6 @@
     </div>
   </section>
 
-
-
   <div class="section">
     <p>
       A platform built by fans, for fans, dedicated to preserving the moments that matter in the world of R-18 VTuber live streaming. It all started with a simple need: capturing ProjektMelody's streams on Chaturbate. Chaturbate doesn’t save VODs, and sometimes we missed the magic. Other times, creators like ProjektMelody faced unnecessary de-platforming for simply being unique. We wanted to create a space where this content could endure, unshaken by the tides of censorship or fleeting platforms.
diff --git a/apps/bright/lib/bright_web/controllers/page_html/api.html.heex b/apps/bright/lib/bright_web/controllers/page_html/api.html.heex
index 052c731..380612f 100644
--- a/apps/bright/lib/bright_web/controllers/page_html/api.html.heex
+++ b/apps/bright/lib/bright_web/controllers/page_html/api.html.heex
@@ -9,15 +9,14 @@
   <p>@todo documentation</p>
 </section>
 
-
-  <div class="section">
+<div class="section">
   <p class="title">icons test</p>
-  <.icon name="bittorrent" type="solid"/>
-  <.icon name="hammer" type="solid"/>
-  <.icon name="fansly" type="solid"/>
-  <.icon name="reddit" type="solid"/>
-  <.icon name="chaturbate" type="solid"/>
-  <.icon name="throne" type="solid"/>
-  <.icon name="tiktok" type="solid"/>
-  <.icon name="pornhub" type="solid"/>
-  </div>
\ No newline at end of file
+  <.icon name="bittorrent" type="solid" />
+  <.icon name="hammer" type="solid" />
+  <.icon name="fansly" type="solid" />
+  <.icon name="reddit" type="solid" />
+  <.icon name="chaturbate" type="solid" />
+  <.icon name="throne" type="solid" />
+  <.icon name="tiktok" type="solid" />
+  <.icon name="pornhub" type="solid" />
+</div>
diff --git a/apps/bright/lib/bright_web/controllers/page_html/home.html.heex b/apps/bright/lib/bright_web/controllers/page_html/home.html.heex
index d474945..4db45ba 100644
--- a/apps/bright/lib/bright_web/controllers/page_html/home.html.heex
+++ b/apps/bright/lib/bright_web/controllers/page_html/home.html.heex
@@ -15,9 +15,7 @@
         </p>
         <i>
           <p class="mt-3">
-            <% # <.icon name="circle-exclamation" class="icon h-4 w-4" /> %>
-            
-            (Yeah, we’re still testing.) check back each week for updates.
+            <% # <.icon name="circle-exclamation" class="icon h-4 w-4" /> %> (Yeah, we’re still testing.) check back each week for updates.
           </p>
         </i>
 
diff --git a/apps/bright/lib/bright_web/controllers/page_html/profile.html.heex b/apps/bright/lib/bright_web/controllers/page_html/profile.html.heex
deleted file mode 100644
index 22c1119..0000000
--- a/apps/bright/lib/bright_web/controllers/page_html/profile.html.heex
+++ /dev/null
@@ -1,48 +0,0 @@
-<.flash_group flash={@flash} />
-
-<%= if @current_user do %>
-  <main class="section">
-    <h2 class="title is-2">Profile</h2>
-    <p class="subtitle">{@current_user.identicon_seed}</p>
-  </main>
-
-  <p class="title">@deprecated</p>
-  <section class="section">
-    <div class="card">
-      <div class="card-content">
-        <div class="media">
-          <div class="media-left">
-            <figure class="image is-48x48">
-              {raw(
-                IdenticonSvg.generate(@current_user.identicon_seed, 5, :basic, 0.8, 2,
-                  squircle_curvature: 0.8
-                )
-              )}
-            </figure>
-          </div>
-          <div class="media-content">
-            <p class="title is-4">{@current_user.identicon_seed}</p>
-            <p class="subtitle is-6">Github User {@current_user.github_id}</p>
-          </div>
-        </div>
-
-        <div class="content">
-          <p class="subtitle is-6">Futureporn User {@current_user.id}</p>
-          <p class="subtitle is-6"><i>n</i> uploads</p>
-          Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus nec
-          iaculis mauris. <.link>@bulmaio</.link>. <.link href="#">#css</.link>
-          <.link href="#">#responsive</.link>
-          <br />
-          <time datetime="2016-1-1">11:09 PM - 1 Jan 2016</time>
-        </div>
-      </div>
-    </div>
-  </section>
-<% else %>
-  <.flash_group flash={@flash} />
-
-  <section class="section">
-    <p>User not found in session.</p>
-    <p>Please <.link href={~p"/auth/patreon"}>sign in</.link></p>
-  </section>
-<% end %>
diff --git a/apps/bright/lib/bright_web/controllers/platform_alias_controller.ex b/apps/bright/lib/bright_web/controllers/platform_alias_controller.ex
new file mode 100644
index 0000000..1220b38
--- /dev/null
+++ b/apps/bright/lib/bright_web/controllers/platform_alias_controller.ex
@@ -0,0 +1,62 @@
+defmodule BrightWeb.PlatformAliasController do
+  use BrightWeb, :controller
+
+  alias Bright.Platforms
+  alias Bright.Platforms.PlatformAlias
+
+  def index(conn, _params) do
+    platform_aliases = Platforms.list_platform_aliases()
+    render(conn, :index, platform_aliases: platform_aliases)
+  end
+
+  def new(conn, _params) do
+    changeset = Platforms.change_platform_alias(%PlatformAlias{})
+    render(conn, :new, changeset: changeset)
+  end
+
+  def create(conn, %{"platform_alias" => platform_alias_params}) do
+    case Platforms.create_platform_alias(platform_alias_params) do
+      {:ok, platform_alias} ->
+        conn
+        |> put_flash(:info, "Platform alias created successfully.")
+        |> redirect(to: ~p"/platform_aliases/#{platform_alias}")
+
+      {:error, %Ecto.Changeset{} = changeset} ->
+        render(conn, :new, changeset: changeset)
+    end
+  end
+
+  def show(conn, %{"id" => id}) do
+    platform_alias = Platforms.get_platform_alias!(id)
+    render(conn, :show, platform_alias: platform_alias)
+  end
+
+  def edit(conn, %{"id" => id}) do
+    platform_alias = Platforms.get_platform_alias!(id)
+    changeset = Platforms.change_platform_alias(platform_alias)
+    render(conn, :edit, platform_alias: platform_alias, changeset: changeset)
+  end
+
+  def update(conn, %{"id" => id, "platform_alias" => platform_alias_params}) do
+    platform_alias = Platforms.get_platform_alias!(id)
+
+    case Platforms.update_platform_alias(platform_alias, platform_alias_params) do
+      {:ok, platform_alias} ->
+        conn
+        |> put_flash(:info, "Platform alias updated successfully.")
+        |> redirect(to: ~p"/platform_aliases/#{platform_alias}")
+
+      {:error, %Ecto.Changeset{} = changeset} ->
+        render(conn, :edit, platform_alias: platform_alias, changeset: changeset)
+    end
+  end
+
+  def delete(conn, %{"id" => id}) do
+    platform_alias = Platforms.get_platform_alias!(id)
+    {:ok, _platform_alias} = Platforms.delete_platform_alias(platform_alias)
+
+    conn
+    |> put_flash(:info, "Platform alias deleted successfully.")
+    |> redirect(to: ~p"/platform_aliases")
+  end
+end
diff --git a/apps/bright/lib/bright_web/controllers/platform_alias_html.ex b/apps/bright/lib/bright_web/controllers/platform_alias_html.ex
new file mode 100644
index 0000000..c725e19
--- /dev/null
+++ b/apps/bright/lib/bright_web/controllers/platform_alias_html.ex
@@ -0,0 +1,23 @@
+defmodule BrightWeb.PlatformAliasHTML do
+  use BrightWeb, :html
+
+  embed_templates "platform_alias_html/*"
+
+  @doc """
+  Renders a platform_alias form.
+  """
+  attr :changeset, Ecto.Changeset, required: true
+  attr :action, :string, required: true
+
+  def platform_alias_form(assigns)
+
+  def platform_opts(changeset) do
+    existing_ids =
+      changeset
+      |> Ecto.Changeset.get_change(:vtubers, [])
+      |> Enum.map(& &1.data.id)
+
+    for platform <- Bright.Platforms.list_platforms(),
+        do: [key: platform.name, value: platform.id, selected: platform.id in existing_ids]
+  end
+end
diff --git a/apps/bright/lib/bright_web/controllers/platform_alias_html/edit.html.heex b/apps/bright/lib/bright_web/controllers/platform_alias_html/edit.html.heex
new file mode 100644
index 0000000..9ca852a
--- /dev/null
+++ b/apps/bright/lib/bright_web/controllers/platform_alias_html/edit.html.heex
@@ -0,0 +1,8 @@
+<.header>
+  Edit Platform alias {@platform_alias.id}
+  <:subtitle>Use this form to manage platform_alias records in your database.</:subtitle>
+</.header>
+
+<.platform_alias_form changeset={@changeset} action={~p"/platform_aliases/#{@platform_alias}"} />
+
+<.back navigate={~p"/platform_aliases"}>Back to platform_aliases</.back>
diff --git a/apps/bright/lib/bright_web/controllers/platform_alias_html/index.html.heex b/apps/bright/lib/bright_web/controllers/platform_alias_html/index.html.heex
new file mode 100644
index 0000000..4ae67ef
--- /dev/null
+++ b/apps/bright/lib/bright_web/controllers/platform_alias_html/index.html.heex
@@ -0,0 +1,33 @@
+<.header>
+  Listing Platform aliases
+  <:actions>
+    <.link href={~p"/platform_aliases/new"}>
+      <.button>New Platform alias</.button>
+    </.link>
+  </:actions>
+</.header>
+
+<.table
+  id="platform_aliases"
+  rows={@platform_aliases}
+  row_click={&JS.navigate(~p"/platform_aliases/#{&1}")}
+>
+  <:col :let={platform_alias} label="Url">{platform_alias.url}</:col>
+  <:col :let={platform_alias} label="Platform">{platform_alias.platform.name}</:col>
+
+  <:action :let={platform_alias}>
+    <div class="sr-only">
+      <.link navigate={~p"/platform_aliases/#{platform_alias}"}>Show</.link>
+    </div>
+    <.link navigate={~p"/platform_aliases/#{platform_alias}/edit"}>Edit</.link>
+  </:action>
+  <:action :let={platform_alias}>
+    <.link
+      href={~p"/platform_aliases/#{platform_alias}"}
+      method="delete"
+      data-confirm="Are you sure?"
+    >
+      Delete
+    </.link>
+  </:action>
+</.table>
diff --git a/apps/bright/lib/bright_web/controllers/platform_alias_html/new.html.heex b/apps/bright/lib/bright_web/controllers/platform_alias_html/new.html.heex
new file mode 100644
index 0000000..f739322
--- /dev/null
+++ b/apps/bright/lib/bright_web/controllers/platform_alias_html/new.html.heex
@@ -0,0 +1,8 @@
+<.header>
+  New Platform alias
+  <:subtitle>Use this form to manage platform_alias records in your database.</:subtitle>
+</.header>
+
+<.platform_alias_form changeset={@changeset} action={~p"/platform_aliases"} />
+
+<.back navigate={~p"/platform_aliases"}>Back to platform_aliases</.back>
diff --git a/apps/bright/lib/bright_web/controllers/platform_alias_html/platform_alias_form.html.heex b/apps/bright/lib/bright_web/controllers/platform_alias_html/platform_alias_form.html.heex
new file mode 100644
index 0000000..e6e3225
--- /dev/null
+++ b/apps/bright/lib/bright_web/controllers/platform_alias_html/platform_alias_form.html.heex
@@ -0,0 +1,16 @@
+<.simple_form :let={f} for={@changeset} action={@action}>
+  <.error :if={@changeset.action}>
+    Oops, something went wrong! Please check the errors below.
+  </.error>
+  <.input field={f[:url]} type="text" label="Url" />
+  <.input
+    field={f[:platform_id]}
+    label="Platform"
+    type="select"
+    multiple={false}
+    options={platform_opts(@changeset)}
+  />
+  <:actions>
+    <.button>Save Platform alias</.button>
+  </:actions>
+</.simple_form>
diff --git a/apps/bright/lib/bright_web/controllers/platform_alias_html/show.html.heex b/apps/bright/lib/bright_web/controllers/platform_alias_html/show.html.heex
new file mode 100644
index 0000000..8704aa7
--- /dev/null
+++ b/apps/bright/lib/bright_web/controllers/platform_alias_html/show.html.heex
@@ -0,0 +1,16 @@
+<.header>
+  Platform alias {@platform_alias.id}
+  <:subtitle>This is a platform_alias record from your database.</:subtitle>
+  <:actions>
+    <.link href={~p"/platform_aliases/#{@platform_alias}/edit"}>
+      <.button>Edit platform_alias</.button>
+    </.link>
+  </:actions>
+</.header>
+
+<.list>
+  <:item title="Url">{@platform_alias.url}</:item>
+  <:item title="Platform">{@platform_alias.platform.name}</:item>
+</.list>
+
+<.back navigate={~p"/platform_aliases"}>Back to platform_aliases</.back>
diff --git a/apps/bright/lib/bright_web/controllers/platform_html/index.html.heex b/apps/bright/lib/bright_web/controllers/platform_html/index.html.heex
index 3d643c4..f3b1f9d 100644
--- a/apps/bright/lib/bright_web/controllers/platform_html/index.html.heex
+++ b/apps/bright/lib/bright_web/controllers/platform_html/index.html.heex
@@ -10,7 +10,7 @@
 <.table id="platforms" rows={@platforms} row_click={&JS.navigate(~p"/platforms/#{&1}")}>
   <:col :let={platform} label="Name">{platform.name}</:col>
   <:col :let={platform} label="Url">{platform.url}</:col>
-  <:col :let={platform} label="Icon">{raw(platform.icon)}</:col>
+  <:col :let={platform} label="NSFW?">{platform.nsfw}</:col>
   <:action :let={platform}>
     <div class="sr-only">
       <.link navigate={~p"/platforms/#{platform}"}>Show</.link>
diff --git a/apps/bright/lib/bright_web/controllers/platform_html/platform_form.html.heex b/apps/bright/lib/bright_web/controllers/platform_html/platform_form.html.heex
index e3478ff..dc8d573 100644
--- a/apps/bright/lib/bright_web/controllers/platform_html/platform_form.html.heex
+++ b/apps/bright/lib/bright_web/controllers/platform_html/platform_form.html.heex
@@ -3,8 +3,9 @@
     Oops, something went wrong! Please check the errors below.
   </.error>
   <.input field={f[:name]} type="text" label="Name" />
+  <.input field={f[:slug]} type="text" label="Slug" />
   <.input field={f[:url]} type="text" label="Url" />
-  <.input field={f[:icon]} type="text" label="Icon" />
+  <.input field={f[:nsfw]} type="checkbox" label="NSFW?" />
   <:actions>
     <.button>Save Platform</.button>
   </:actions>
diff --git a/apps/bright/lib/bright_web/controllers/platform_html/show.html.heex b/apps/bright/lib/bright_web/controllers/platform_html/show.html.heex
index c94efa7..cc7b8ba 100644
--- a/apps/bright/lib/bright_web/controllers/platform_html/show.html.heex
+++ b/apps/bright/lib/bright_web/controllers/platform_html/show.html.heex
@@ -11,7 +11,7 @@
 <.list>
   <:item title="Name">{@platform.name}</:item>
   <:item title="Url">{@platform.url}</:item>
-  <:item title="Icon">{raw(@platform.icon)}</:item>
+  <:item title="NSFW?">{@platform.nsfw}</:item>
 </.list>
 
 <.back navigate={~p"/platforms"}>Back to platforms</.back>
diff --git a/apps/bright/lib/bright_web/controllers/stream_html/index.html.heex b/apps/bright/lib/bright_web/controllers/stream_html/index.html.heex
index aa48eb7..3c1b338 100644
--- a/apps/bright/lib/bright_web/controllers/stream_html/index.html.heex
+++ b/apps/bright/lib/bright_web/controllers/stream_html/index.html.heex
@@ -8,15 +8,13 @@
 </.header>
 
 <.table id="streams" rows={@streams} row_click={&JS.navigate(~p"/streams/#{&1}")}>
-  <:col :let={stream} label="ID">{stream.id}</:col>
-  <:col :let={stream} label="Title">{stream.title}</:col>
   <:col :let={stream} label="Date">{stream.date}</:col>
+  <:col :let={stream} label="Title">{stream.title}</:col>
   <:col :let={stream} label="Platforms">
     <div class="columns is-1">
       <%= for platform <- stream.platforms do %>
         <div class="column is-mobile is-narrow">
-          <%# {raw(platform.icon)} %>
-          <.icon name={platform.slug}/>
+          <.icon name={platform.slug} />
         </div>
       <% end %>
     </div>
diff --git a/apps/bright/lib/bright_web/controllers/upload_controller.ex b/apps/bright/lib/bright_web/controllers/upload_controller.ex
new file mode 100644
index 0000000..186132d
--- /dev/null
+++ b/apps/bright/lib/bright_web/controllers/upload_controller.ex
@@ -0,0 +1,62 @@
+defmodule BrightWeb.UploadController do
+  use BrightWeb, :controller
+
+  alias Bright.Streams
+  alias Bright.Streams.Upload
+
+  def index(conn, _params) do
+    uploads = Streams.list_uploads()
+    render(conn, :index, uploads: uploads)
+  end
+
+  def new(conn, _params) do
+    changeset = Streams.change_upload(%Upload{})
+    render(conn, :new, changeset: changeset)
+  end
+
+  def create(conn, %{"upload" => upload_params}) do
+    case Streams.create_upload(upload_params) do
+      {:ok, upload} ->
+        conn
+        |> put_flash(:info, "Upload created successfully.")
+        |> redirect(to: ~p"/uploads/#{upload}")
+
+      {:error, %Ecto.Changeset{} = changeset} ->
+        render(conn, :new, changeset: changeset)
+    end
+  end
+
+  def show(conn, %{"id" => id}) do
+    upload = Streams.get_upload!(id)
+    render(conn, :show, upload: upload)
+  end
+
+  def edit(conn, %{"id" => id}) do
+    upload = Streams.get_upload!(id)
+    changeset = Streams.change_upload(upload)
+    render(conn, :edit, upload: upload, changeset: changeset)
+  end
+
+  def update(conn, %{"id" => id, "upload" => upload_params}) do
+    upload = Streams.get_upload!(id)
+
+    case Streams.update_upload(upload, upload_params) do
+      {:ok, upload} ->
+        conn
+        |> put_flash(:info, "Upload updated successfully.")
+        |> redirect(to: ~p"/uploads/#{upload}")
+
+      {:error, %Ecto.Changeset{} = changeset} ->
+        render(conn, :edit, upload: upload, changeset: changeset)
+    end
+  end
+
+  def delete(conn, %{"id" => id}) do
+    upload = Streams.get_upload!(id)
+    {:ok, _upload} = Streams.delete_upload(upload)
+
+    conn
+    |> put_flash(:info, "Upload deleted successfully.")
+    |> redirect(to: ~p"/uploads")
+  end
+end
diff --git a/apps/bright/lib/bright_web/controllers/upload_html.ex b/apps/bright/lib/bright_web/controllers/upload_html.ex
new file mode 100644
index 0000000..e885bbe
--- /dev/null
+++ b/apps/bright/lib/bright_web/controllers/upload_html.ex
@@ -0,0 +1,13 @@
+defmodule BrightWeb.UploadHTML do
+  use BrightWeb, :html
+
+  embed_templates "upload_html/*"
+
+  @doc """
+  Renders a upload form.
+  """
+  attr :changeset, Ecto.Changeset, required: true
+  attr :action, :string, required: true
+
+  def upload_form(assigns)
+end
diff --git a/apps/bright/lib/bright_web/controllers/upload_html/edit.html.heex b/apps/bright/lib/bright_web/controllers/upload_html/edit.html.heex
new file mode 100644
index 0000000..575822f
--- /dev/null
+++ b/apps/bright/lib/bright_web/controllers/upload_html/edit.html.heex
@@ -0,0 +1,8 @@
+<.header>
+  Edit Upload {@upload.id}
+  <:subtitle>Use this form to manage upload records in your database.</:subtitle>
+</.header>
+
+<.upload_form changeset={@changeset} action={~p"/uploads/#{@upload}"} />
+
+<.back navigate={~p"/uploads"}>Back to uploads</.back>
diff --git a/apps/bright/lib/bright_web/controllers/upload_html/index.html.heex b/apps/bright/lib/bright_web/controllers/upload_html/index.html.heex
new file mode 100644
index 0000000..e329877
--- /dev/null
+++ b/apps/bright/lib/bright_web/controllers/upload_html/index.html.heex
@@ -0,0 +1,25 @@
+<.header>
+  Listing Uploads
+  <:actions>
+    <.link href={~p"/uploads/new"}>
+      <.button>New Upload</.button>
+    </.link>
+  </:actions>
+</.header>
+
+<.table id="uploads" rows={@uploads} row_click={&JS.navigate(~p"/uploads/#{&1}")}>
+  <:col :let={upload} label="Filename">{upload.filename}</:col>
+  <:col :let={upload} label="Size">{upload.size}</:col>
+  <:col :let={upload} label="Content type">{upload.content_type}</:col>
+  <:action :let={upload}>
+    <div class="sr-only">
+      <.link navigate={~p"/uploads/#{upload}"}>Show</.link>
+    </div>
+    <.link navigate={~p"/uploads/#{upload}/edit"}>Edit</.link>
+  </:action>
+  <:action :let={upload}>
+    <.link href={~p"/uploads/#{upload}"} method="delete" data-confirm="Are you sure?">
+      Delete
+    </.link>
+  </:action>
+</.table>
diff --git a/apps/bright/lib/bright_web/controllers/upload_html/new.html.heex b/apps/bright/lib/bright_web/controllers/upload_html/new.html.heex
new file mode 100644
index 0000000..b42d0b6
--- /dev/null
+++ b/apps/bright/lib/bright_web/controllers/upload_html/new.html.heex
@@ -0,0 +1,8 @@
+<.header>
+  New Upload
+  <:subtitle>Use this form to manage upload records in your database.</:subtitle>
+</.header>
+
+<.upload_form changeset={@changeset} action={~p"/uploads"} />
+
+<.back navigate={~p"/uploads"}>Back to uploads</.back>
diff --git a/apps/bright/lib/bright_web/controllers/upload_html/show.html.heex b/apps/bright/lib/bright_web/controllers/upload_html/show.html.heex
new file mode 100644
index 0000000..d015d82
--- /dev/null
+++ b/apps/bright/lib/bright_web/controllers/upload_html/show.html.heex
@@ -0,0 +1,17 @@
+<.header>
+  Upload {@upload.id}
+  <:subtitle>This is a upload record from your database.</:subtitle>
+  <:actions>
+    <.link href={~p"/uploads/#{@upload}/edit"}>
+      <.button>Edit upload</.button>
+    </.link>
+  </:actions>
+</.header>
+
+<.list>
+  <:item title="Filename">{@upload.filename}</:item>
+  <:item title="Size">{@upload.size}</:item>
+  <:item title="Content type">{@upload.content_type}</:item>
+</.list>
+
+<.back navigate={~p"/uploads"}>Back to uploads</.back>
diff --git a/apps/bright/lib/bright_web/controllers/upload_html/upload_form.html.heex b/apps/bright/lib/bright_web/controllers/upload_html/upload_form.html.heex
new file mode 100644
index 0000000..d5b2bfe
--- /dev/null
+++ b/apps/bright/lib/bright_web/controllers/upload_html/upload_form.html.heex
@@ -0,0 +1,11 @@
+<.simple_form :let={f} for={@changeset} action={@action}>
+  <.error :if={@changeset.action}>
+    Oops, something went wrong! Please check the errors below.
+  </.error>
+  <.input field={f[:filename]} type="text" label="Filename" />
+  <.input field={f[:size]} type="number" label="Size" />
+  <.input field={f[:content_type]} type="text" label="Content type" />
+  <:actions>
+    <.button>Save Upload</.button>
+  </:actions>
+</.simple_form>
diff --git a/apps/bright/lib/bright_web/controllers/vtuber_html/index.html.heex b/apps/bright/lib/bright_web/controllers/vtuber_html/index.html.heex
index e08ae9a..a5c2161 100644
--- a/apps/bright/lib/bright_web/controllers/vtuber_html/index.html.heex
+++ b/apps/bright/lib/bright_web/controllers/vtuber_html/index.html.heex
@@ -15,16 +15,19 @@
     </figure>
   </:col>
   <:col :let={vtuber} label="Archival">
-
-    <% %{total_streams: total_streams, streams_with_vods: streams_with_vods, percentage: percentage} = Vtubers.vtuber_archive_percentage(vtuber) %>
+    <% %{
+      total_streams: total_streams,
+      streams_with_vods: streams_with_vods,
+      percentage: percentage
+    } = Vtubers.vtuber_archive_percentage(vtuber) %>
     <div class="columns is-mobile">
       <div class="column is-two-thirds">
         <progress class="progress is-success" value={percentage} max="100">
-          <%= percentage %>%
+          {percentage}%
         </progress>
       </div>
       <div class="column is-one-third">
-        <%= streams_with_vods %> of <%= total_streams %> (<%= percentage %>%)
+        {streams_with_vods} of {total_streams} ({percentage}%)
       </div>
     </div>
   </:col>
diff --git a/apps/bright/lib/bright_web/live/upload_live/form_component.ex b/apps/bright/lib/bright_web/live/upload_live/form_component.ex
new file mode 100644
index 0000000..e10549e
--- /dev/null
+++ b/apps/bright/lib/bright_web/live/upload_live/form_component.ex
@@ -0,0 +1,84 @@
+defmodule BrightWeb.UploadLive.FormComponent do
+  use BrightWeb, :live_component
+
+  alias Bright.Streams
+
+  @impl true
+  def render(assigns) do
+    ~H"""
+    <div>
+      <.header>
+        {@title}
+        <:subtitle>Use this form to manage upload records in your database.</:subtitle>
+      </.header>
+
+      <.simple_form
+        for={@form}
+        id="upload-form"
+        phx-target={@myself}
+        phx-change="validate"
+        phx-submit="save"
+      >
+        <.input field={@form[:filename]} type="text" label="Filename" />
+        <.input field={@form[:size]} type="number" label="Size" />
+        <.input field={@form[:content_type]} type="text" label="Content type" />
+        <:actions>
+          <.button phx-disable-with="Saving...">Save Upload</.button>
+        </:actions>
+      </.simple_form>
+    </div>
+    """
+  end
+
+  @impl true
+  def update(%{upload: upload} = assigns, socket) do
+    {:ok,
+     socket
+     |> assign(assigns)
+     |> assign_new(:form, fn ->
+       to_form(Streams.change_upload(upload))
+     end)}
+  end
+
+  @impl true
+  def handle_event("validate", %{"upload" => upload_params}, socket) do
+    changeset = Streams.change_upload(socket.assigns.upload, upload_params)
+    {:noreply, assign(socket, form: to_form(changeset, action: :validate))}
+  end
+
+  def handle_event("save", %{"upload" => upload_params}, socket) do
+    save_upload(socket, socket.assigns.action, upload_params)
+  end
+
+  defp save_upload(socket, :edit, upload_params) do
+    case Streams.update_upload(socket.assigns.upload, upload_params) do
+      {:ok, upload} ->
+        notify_parent({:saved, upload})
+
+        {:noreply,
+         socket
+         |> put_flash(:info, "Upload updated successfully")
+         |> push_patch(to: socket.assigns.patch)}
+
+      {:error, %Ecto.Changeset{} = changeset} ->
+        {:noreply, assign(socket, form: to_form(changeset))}
+    end
+  end
+
+  defp save_upload(socket, :new, upload_params) do
+    case Streams.create_upload(upload_params) do
+      {:ok, upload} ->
+        notify_parent({:saved, upload})
+
+        {:noreply,
+         socket
+         |> put_flash(:info, "Upload created successfully")
+         |> push_patch(to: socket.assigns.patch)}
+
+      {:error, %Ecto.Changeset{} = changeset} ->
+        {:noreply, assign(socket, form: to_form(changeset))}
+    end
+  end
+
+  defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
+end
diff --git a/apps/bright/lib/bright_web/live/upload_live/index.ex b/apps/bright/lib/bright_web/live/upload_live/index.ex
new file mode 100644
index 0000000..16fda4e
--- /dev/null
+++ b/apps/bright/lib/bright_web/live/upload_live/index.ex
@@ -0,0 +1,331 @@
+defmodule BrightWeb.UploadLive.Index do
+  use BrightWeb, :live_view
+
+  alias Bright.Streams
+  alias Bright.Streams.Upload
+  alias Bright.Utils
+  require Logger
+
+  def mount(_params, _session, socket) do
+    # {:ok,
+    #  socket
+    #  |> assign(:uploaded_files, [])
+    #  |> allow_upload(:vods,
+    #    accept: ~w(.mp4 .mov .ts .avi .mpeg .ogv .webm .3gp .3g2),
+    #    max_entries: 3,
+    #    max_file_size: 80 * 1_000_000_000,
+    #    external: &presign_upload/2
+    #  )}
+
+    # socket = assign(socket, endpoint: System.fetch_env!("UPPY_ENDPOINT"))
+    socket =
+      socket
+      |> assign(:uploaded_files, [])
+
+    {:ok, socket}
+  end
+
+  @impl true
+  def render(assigns) do
+    ~H"""
+    <div>
+
+      <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/uppy/4.12.2/uppy.min.css" integrity="sha512-oPlr9/HXIlp7YoIRNsexheOu2/P2sEVi8EFQEAWUlHHijx0QbQ9qgihNYmIYtdJP3xOIMbZcnSVhrznIh5DKkg==" crossorigin="anonymous" referrerpolicy="no-referrer" />
+
+
+
+
+      <div id="uppy-dashboard" phx-hook="UppyHook" phx-update="ignore"></div>
+
+
+
+    </div>
+    """
+  end
+
+  # @impl true
+  # def handle_event(
+  #       "presign",
+  #       %{"type" => type, "name" => name},
+  #       socket
+  #     ) do
+  #   {:ok, %{url: url}, socket} = presign_url(name, type, socket)
+  #   Logger.debug("✍️✍️✍️ presign called with name=#{name} type=#{type}. url=#{url}")
+  #   # socket = assign(socket, signatures: {name, url})
+  #   # socket =
+  #   #   socket
+  #   #   |> update(:uploaded_files, &(&1 ++ uploaded_files))
+
+  #   # |> push_event("process_upload", %{name: name, url: url, type: type})
+
+  #   {:reply, %{name: name, type: type, url: url}, socket}
+  # end
+
+  # @impl true
+  # def handle_event("upload_videos", _params, socket) do
+  #   uploaded_files =
+  #     consume_uploaded_entries(socket, :vods, fn %{path: _path}, entry ->
+  #       IO.puts("@todo ⭐⭐⭐ Handling #{entry.client_type} @todo @todo @todo")
+  #       IO.puts(inspect(entry))
+  #     end)
+
+  #   socket =
+  #     socket
+  #     |> update(:uploaded_files, &(&1 ++ uploaded_files))
+
+  #   {:noreply, socket}
+  # end
+
+  # def handle_event("update_preview_srcs", %{"srcs" => srcs}, socket) do
+  #   uploaded_files =
+  #     socket.assigns.uploaded_files
+  #     |> Enum.map(fn entry ->
+  #       if Map.has_key?(srcs, entry.ref) do
+  #         entry
+  #         |> Map.put(:preview_src, Map.fetch!(srcs, entry.ref))
+  #       else
+  #         entry
+  #       end
+  #     end)
+
+  #   socket =
+  #     socket
+  #     |> assign(:uploaded_files, uploaded_files)
+
+  #   {:noreply, socket}
+  # end
+
+  # def handle_event("validate_upload", _params, socket) do
+  #   num_remaining_uploads =
+  #     length(socket.assigns.uploaded_files) - socket.assigns.uploads.vods.max_entries
+
+  #   valid =
+  #     Enum.uniq_by(socket.assigns.uploads.vods.entries, & &1.client_name)
+  #     |> Enum.take(num_remaining_uploads)
+
+  #   socket =
+  #     Enum.reduce(socket.assigns.uploads.vods.entries, socket, fn entry, socket ->
+  #       if entry in valid do
+  #         socket
+  #       else
+  #         socket
+  #         |> cancel_upload(:vods, entry.ref)
+  #         |> put_flash(
+  #           :error,
+  #           "Uploaded files should be unique and cannot exceed #{socket.assigns.uploads.vods.max_entries} total files."
+  #         )
+  #       end
+  #     end)
+
+  #   {:noreply, socket}
+  # end
+
+  # def handle_event("cancel_upload", %{"ref" => ref}, socket) do
+  #   {:noreply, cancel_upload(socket, :vods, ref)}
+  # end
+
+  # def handle_event("cancel_upload", _params, socket) do
+  #   socket =
+  #     Enum.reduce(socket.assigns.uploads.vods.entries, socket, fn entry, socket ->
+  #       cancel_upload(socket, :vods, entry.ref)
+  #     end)
+
+  #   {:noreply, socket}
+  # end
+
+  # def presign_upload(name, type, socket) do
+  #   Logger.debug("presign_upload was called with name=#{inspect(name)} and type=#{inspect(type)}")
+  #   config = ExAws.Config.new(:s3)
+  #   bucket = System.fetch_env!("AWS_BUCKET")
+  #   key = "usc/#{Utils.random_string()}/#{name}"
+
+  #   {:ok, url} =
+  #     ExAws.S3.presigned_url(config, :put, bucket, key,
+  #       expires_in: 3600,
+  #       query_params: [{"Content-Type", type}]
+  #     )
+
+  #   {:ok, %{uploader: "S3", key: key, url: url}, socket}
+  # end
+
+  # # @doc @see https://hexdocs.pm/ex_aws_s3/ExAws.S3.html#presigned_post/4
+  # def presigned_post(name, socket) do
+  #   Logger.debug("presigned_post with name=#{inspect(name)}")
+  #   config = ExAws.Config.new(:s3)
+  #   bucket = System.fetch_env!("AWS_BUCKET")
+  #   key = "usc/#{Utils.random_string()}/#{name}"
+  #   {:ok, url} = ExAws.S3.presigned_post(config, )
+  # end
+
+  defp get_s3_config(name) do
+    config = ExAws.Config.new(:s3)
+    bucket = System.fetch_env!("AWS_BUCKET")
+    key = "usc/#{Utils.random_string()}/#{name}"
+    %{config: config, bucket: bucket, key: key}
+  end
+
+  def handle_event(
+        "list_parts",
+        %{"upload_id" => upload_id, "key" => key},
+        socket
+      ) do
+    config = ExAws.Config.new(:s3)
+    bucket = System.fetch_env!("AWS_BUCKET")
+    # key = "usc/#{Utils.random_string()}/#{name}"
+    # %{config: config, bucket: bucket, key: key} = get_s3_config(name)
+    case ExAws.S3.list_parts(bucket, key, upload_id) do
+      # <Part>
+      # <ETag>"85f30635602dc09bd85957a6e82a2c21"</ETag>
+      # <LastModified>2023-08-31T18:54:55.693Z</LastModified>
+      # <PartNumber>1</PartNumber>
+      # <Size>11</Size>
+
+      {:ok, part: %{etag: etag, partNumber: partNumber, size: size}} ->
+        Logger.debug("🦠🦠🦠 we got an etag from list_parts. etag=#{inspect(etag)}")
+        {:reply, %{etag: etag, partNumber: partNumber, size: size}, socket}
+
+      {:error, reason} ->
+        Logger.error("error while running S3.list_parts. reason=#{inspect(reason)}")
+        {:reply, %{error: "Failed to list_parts"}, socket}
+    end
+  end
+
+  # @doc @see https://hexdocs.pm/ex_aws_s3/ExAws.S3.html#initiate_multipart_upload/3
+  def handle_event(
+        "initiate_multipart_upload",
+        %{"name" => name, "type" => type},
+        socket
+      ) do
+    %{config: config, bucket: bucket, key: key} = get_s3_config(name)
+
+    operation = ExAws.S3.initiate_multipart_upload(bucket, key, content_type: type)
+
+    case ExAws.request(operation, config) do
+      {:ok, %{body: %{key: key, upload_id: upload_id}} = response} ->
+        Logger.debug(
+          "Multipart upload initiated. Upload ID: #{upload_id}, Key: #{key}, response=#{inspect(response)}"
+        )
+
+        {:reply, %{upload_id: upload_id, key: key}, socket}
+
+      {:error, reason} ->
+        Logger.error("Failed to initiate multipart upload: #{inspect(reason)}")
+        {:reply, %{error: "Failed to initiate upload"}, socket}
+    end
+  end
+
+  def handle_event(
+        "abort_multipart_upload",
+        %{"key" => key, "uploadId" => upload_id},
+        socket
+      ) do
+    config = ExAws.Config.new(:s3)
+    bucket = System.fetch_env!("AWS_BUCKET")
+
+    operation = ExAws.S3.abort_multipart_upload(bucket, key, upload_id)
+
+    case ExAws.request(operation, config) do
+      {:ok, _} ->
+        Logger.debug("Aborting multipart upload. Upload ID: #{upload_id}}")
+        {:reply, %{upload_id: upload_id}, socket}
+
+      {:error, reason} ->
+        Logger.error("Failed to abort multipart upload: #{inspect(reason)}")
+        {:reply, %{error: "Failed to initiate upload"}, socket}
+    end
+  end
+
+  def handle_event(
+        "get_upload_parameters",
+        %{"type" => type, "name" => name},
+        socket
+      ) do
+    %{config: config, bucket: bucket, key: key} = get_s3_config(name)
+
+    {:ok, url} =
+      ExAws.S3.presigned_url(config, :put, bucket, key,
+        expires_in: 3600,
+        query_params: [
+          {"Content-Type", type}
+        ]
+      )
+
+    {:reply, %{type: type, method: "PUT", url: url}, socket}
+  end
+
+  def handle_event(
+        "complete_multipart_upload",
+        %{"key" => key, "uploadId" => upload_id, "parts" => parts},
+        socket
+      ) do
+    config = ExAws.Config.new(:s3)
+    bucket = System.fetch_env!("AWS_BUCKET")
+
+    Logger.debug("(before) parts=#{inspect(parts)}")
+
+    parts =
+      Enum.map(parts, fn part ->
+        {part["part_number"], part["etag"]}
+      end)
+
+    operation = ExAws.S3.complete_multipart_upload(bucket, key, upload_id, parts)
+
+    case ExAws.request(operation, config) do
+      {:ok, %{body: %{location: location}} = response} ->
+        Logger.debug(
+          "Completing multipart upload. Upload ID: #{upload_id}, response=#{inspect(response)}"
+        )
+
+        {:reply, %{location: location}, socket}
+
+      {:error, reason} ->
+        Logger.error("Failed to complete multipart upload: #{inspect(reason)}")
+        {:reply, %{error: "The server failed to complete multipart upload"}, socket}
+    end
+  end
+
+  def handle_event(
+        "sign_part",
+        %{"body" => _body, "key" => key, "partNumber" => part_number, "uploadId" => upload_id},
+        socket
+      ) do
+    Logger.debug(
+      "sign_part was called with key=#{inspect(key)} part_number=#{inspect(part_number)} upload_id=#{inspect(upload_id)}"
+    )
+
+    config = ExAws.Config.new(:s3)
+    bucket = System.fetch_env!("AWS_BUCKET")
+
+    {:ok, url} =
+      ExAws.S3.presigned_url(config, :put, bucket, key,
+        expires_in: 3600,
+        query_params: [
+          {"partNumber", part_number},
+          {"uploadId", upload_id}
+        ]
+      )
+
+    {:reply, %{url: url}, socket}
+  end
+
+  defp join_refs(entries), do: Enum.join(entries, ",")
+  def error_to_string(:too_large), do: "File too large!"
+  def error_to_string(:not_accepted), do: "Bad file type!"
+
+  defp to_megabytes_or_kilobytes(bytes) when is_integer(bytes) do
+    case bytes do
+      b when b < 1_048_576 ->
+        kilobytes = (b / 1024) |> Float.round(1)
+
+        if kilobytes < 1 do
+          "#{kilobytes}MB"
+        else
+          "#{round(kilobytes)}KB"
+        end
+
+      _ ->
+        megabytes = (bytes / 1_048_576) |> Float.round(1)
+        "#{megabytes}MB"
+    end
+  end
+end
diff --git a/apps/bright/lib/bright_web/live/upload_live/show.ex b/apps/bright/lib/bright_web/live/upload_live/show.ex
new file mode 100644
index 0000000..5ada707
--- /dev/null
+++ b/apps/bright/lib/bright_web/live/upload_live/show.ex
@@ -0,0 +1,21 @@
+defmodule BrightWeb.UploadLive.Show do
+  use BrightWeb, :live_view
+
+  alias Bright.Streams
+
+  @impl true
+  def mount(_params, _session, socket) do
+    {:ok, socket}
+  end
+
+  @impl true
+  def handle_params(%{"id" => id}, _, socket) do
+    {:noreply,
+     socket
+     |> assign(:page_title, page_title(socket.assigns.live_action))
+     |> assign(:upload, Streams.get_upload!(id))}
+  end
+
+  defp page_title(:show), do: "Show Upload"
+  defp page_title(:edit), do: "Edit Upload"
+end
diff --git a/apps/bright/lib/bright_web/live/upload_live/show.html.heex b/apps/bright/lib/bright_web/live/upload_live/show.html.heex
new file mode 100644
index 0000000..b861be5
--- /dev/null
+++ b/apps/bright/lib/bright_web/live/upload_live/show.html.heex
@@ -0,0 +1,28 @@
+<.header>
+  Upload {@upload.id}
+  <:subtitle>This is a upload record from your database.</:subtitle>
+  <:actions>
+    <.link patch={~p"/uploads/#{@upload}/show/edit"} phx-click={JS.push_focus()}>
+      <.button>Edit upload</.button>
+    </.link>
+  </:actions>
+</.header>
+
+<.list>
+  <:item title="Filename">{@upload.filename}</:item>
+  <:item title="Size">{@upload.size}</:item>
+  <:item title="Content type">{@upload.content_type}</:item>
+</.list>
+
+<.back navigate={~p"/uploads"}>Back to uploads</.back>
+
+<.modal :if={@live_action == :edit} id="upload-modal" show on_cancel={JS.patch(~p"/uploads/#{@upload}")}>
+  <.live_component
+    module={BrightWeb.UploadLive.FormComponent}
+    id={@upload.id}
+    title={@page_title}
+    action={@live_action}
+    upload={@upload}
+    patch={~p"/uploads/#{@upload}"}
+  />
+</.modal>
diff --git a/apps/bright/lib/bright_web/live/upload_live/simple_s3_upload.ex b/apps/bright/lib/bright_web/live/upload_live/simple_s3_upload.ex
new file mode 100644
index 0000000..d13ca7c
--- /dev/null
+++ b/apps/bright/lib/bright_web/live/upload_live/simple_s3_upload.ex
@@ -0,0 +1,129 @@
+defmodule SimpleS3Upload do
+  @moduledoc """
+  Dependency-free S3 Form Upload using HTTP POST sigv4
+
+  @see https://gist.github.com/chrismccord/37862f1f8b1f5148644b75d20d1cb073
+
+  https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-post-example.html
+  """
+
+  @doc """
+  Signs a form upload.
+
+  The configuration is a map which must contain the following keys:
+
+    * `:region` - The AWS region, such as "us-east-1"
+    * `:access_key_id` - The AWS access key id
+    * `:secret_access_key` - The AWS secret access key
+
+
+  Returns a map of form fields to be used on the client via the JavaScript `FormData` API.
+
+  ## Options
+
+    * `:key` - The required key of the object to be uploaded.
+    * `:max_file_size` - The required maximum allowed file size in bytes.
+    * `:content_type` - The required MIME type of the file to be uploaded.
+    * `:expires_in` - The required expiration time in milliseconds from now
+      before the signed upload expires.
+
+  ## Examples
+
+      config = %{
+        region: "us-east-1",
+        access_key_id: System.fetch_env!("AWS_ACCESS_KEY_ID"),
+        secret_access_key: System.fetch_env!("AWS_SECRET_ACCESS_KEY")
+      }
+
+      {:ok, fields} =
+        SimpleS3Upload.sign_form_upload(config, "my-bucket",
+          key: "public/my-file-name",
+          content_type: "image/png",
+          max_file_size: 10_000,
+          expires_in: :timer.hours(1)
+        )
+
+  """
+  def sign_form_upload(config, bucket, opts) do
+    key = Keyword.fetch!(opts, :key)
+    max_file_size = Keyword.fetch!(opts, :max_file_size)
+    content_type = Keyword.fetch!(opts, :content_type)
+    expires_in = Keyword.fetch!(opts, :expires_in)
+
+    expires_at = DateTime.add(DateTime.utc_now(), expires_in, :millisecond)
+    amz_date = amz_date(expires_at)
+    credential = credential(config, expires_at)
+
+    encoded_policy =
+      Base.encode64("""
+      {
+        "expiration": "#{DateTime.to_iso8601(expires_at)}",
+        "conditions": [
+          {"bucket":  "#{bucket}"},
+          ["eq", "$key", "#{key}"],
+          {"acl": "public-read"},
+          ["eq", "$Content-Type", "#{content_type}"],
+          ["content-length-range", 0, #{max_file_size}],
+          {"x-amz-server-side-encryption": "AES256"},
+          {"x-amz-credential": "#{credential}"},
+          {"x-amz-algorithm": "AWS4-HMAC-SHA256"},
+          {"x-amz-date": "#{amz_date}"}
+        ]
+      }
+      """)
+
+    fields = %{
+      "key" => key,
+      "acl" => "public-read",
+      "content-type" => content_type,
+      "x-amz-server-side-encryption" => "AES256",
+      "x-amz-credential" => credential,
+      "x-amz-algorithm" => "AWS4-HMAC-SHA256",
+      "x-amz-date" => amz_date,
+      "policy" => encoded_policy,
+      "x-amz-signature" => signature(config, expires_at, encoded_policy)
+    }
+
+    {:ok, fields}
+  end
+
+  defp amz_date(time) do
+    time
+    |> NaiveDateTime.to_iso8601()
+    |> String.split(".")
+    |> List.first()
+    |> String.replace("-", "")
+    |> String.replace(":", "")
+    |> Kernel.<>("Z")
+  end
+
+  defp credential(%{} = config, %DateTime{} = expires_at) do
+    "#{config.access_key_id}/#{short_date(expires_at)}/#{config.region}/s3/aws4_request"
+  end
+
+  defp signature(config, %DateTime{} = expires_at, encoded_policy) do
+    config
+    |> signing_key(expires_at, "s3")
+    |> sha256(encoded_policy)
+    |> Base.encode16(case: :lower)
+  end
+
+  defp signing_key(%{} = config, %DateTime{} = expires_at, service) when service in ["s3"] do
+    amz_date = short_date(expires_at)
+    %{secret_access_key: secret, region: region} = config
+
+    ("AWS4" <> secret)
+    |> sha256(amz_date)
+    |> sha256(region)
+    |> sha256(service)
+    |> sha256("aws4_request")
+  end
+
+  defp short_date(%DateTime{} = expires_at) do
+    expires_at
+    |> amz_date()
+    |> String.slice(0..7)
+  end
+
+  defp sha256(secret, msg), do: :crypto.mac(:hmac, :sha256, secret, msg)
+end
diff --git a/apps/bright/lib/bright_web/live/upload_live/upload_live.ex b/apps/bright/lib/bright_web/live/upload_live/upload_live.ex
new file mode 100644
index 0000000..f5b1ff3
--- /dev/null
+++ b/apps/bright/lib/bright_web/live/upload_live/upload_live.ex
@@ -0,0 +1,39 @@
+# # lib/my_app_web/live/upload_live.ex
+# defmodule MyAppWeb.UploadLive do
+#   use MyAppWeb, :live_view
+
+#   @impl Phoenix.LiveView
+#   def mount(_params, _session, socket) do
+#     {:ok,
+#      socket
+#      |> assign(:uploaded_files, [])
+#      |> allow_upload(:avatar, accept: ~w(.jpg .jpeg), max_entries: 2)}
+#   end
+
+#   @impl Phoenix.LiveView
+#   def handle_event("validate", _params, socket) do
+#     {:noreply, socket}
+#   end
+
+#   @impl Phoenix.LiveView
+#   def handle_event("cancel-upload", %{"ref" => ref}, socket) do
+#     {:noreply, cancel_upload(socket, :avatar, ref)}
+#   end
+
+#   @impl Phoenix.LiveView
+#   def handle_event("save", _params, socket) do
+#     uploaded_files =
+#       consume_uploaded_entries(socket, :avatar, fn %{path: path}, _entry ->
+#         dest = Path.join([:code.priv_dir(:my_app), "static", "uploads", Path.basename(path)])
+#         # You will need to create `priv/static/uploads` for `File.cp!/2` to work.
+#         File.cp!(path, dest)
+#         {:ok, ~p"/uploads/#{Path.basename(dest)}"}
+#       end)
+
+#     {:noreply, update(socket, :uploaded_files, &(&1 ++ uploaded_files))}
+#   end
+
+#   defp error_to_string(:too_large), do: "Too large"
+#   defp error_to_string(:too_many_files), do: "You have selected too many files"
+#   defp error_to_string(:not_accepted), do: "You have selected an unacceptable file type"
+# end
diff --git a/apps/bright/lib/bright_web/router.ex b/apps/bright/lib/bright_web/router.ex
index 7585711..83e69e4 100644
--- a/apps/bright/lib/bright_web/router.ex
+++ b/apps/bright/lib/bright_web/router.ex
@@ -75,6 +75,7 @@ defmodule BrightWeb.Router do
     #   get "/vtubers/:id/edit", VtuberController, :edit
     # end
 
+    resources("/uploads", UploadController, only: [:show, :index, :delete])
     resources("/vods", VodController, only: [:create, :new, :edit, :update, :delete])
     resources("/vtubers", VtuberController, only: [:delete])
 
@@ -82,10 +83,12 @@ defmodule BrightWeb.Router do
 
     resources("/torrents", TorrentController, only: [:new, :create, :edit, :update, :delete])
 
-    ## !!! DANGER, platforms must only be writable by admins, (unless we implement SVG sanitizing)
-    ## @todo remove SVGs from the database and instead put them in assets
     resources("/platforms", PlatformController, only: [:new, :create, :edit, :update, :delete])
 
+    resources("/platform_aliases", PlatformAliasController,
+      only: [:new, :create, :edit, :update, :delete]
+    )
+
     oban_dashboard("/oban")
   end
 
@@ -112,6 +115,9 @@ defmodule BrightWeb.Router do
     get("/platforms", PlatformController, :index)
     get("/platforms/:id", PlatformController, :show)
 
+    get("/platform_aliases", PlatformAliasController, :index)
+    get("/platform_aliases/:id", PlatformAliasController, :show)
+
     resources("/vtubers", VtuberController, only: [:index, :show])
 
     resources "/vt", VtuberController do
@@ -122,6 +128,8 @@ defmodule BrightWeb.Router do
     live_session :authenticated,
       on_mount: [{BrightWeb.AuthController, :ensure_authenticated}] do
       live("/profile", ProfileLive)
+      live("/upload", UploadLive.Index, :index)
+      # live("/upload/presign", , :)
     end
   end
 
diff --git a/apps/bright/mix.exs b/apps/bright/mix.exs
index 5fa75af..f382cd0 100644
--- a/apps/bright/mix.exs
+++ b/apps/bright/mix.exs
@@ -47,7 +47,7 @@ defmodule Bright.MixProject do
       {:phoenix_live_view, "~> 1.0"},
       {:floki, ">= 0.30.0", only: :test},
       {:phoenix_live_dashboard, "~> 0.8.3"},
-      {:esbuild, "~> 0.8", runtime: Mix.env() == :dev},
+      {:esbuild, "~> 0.9.0", runtime: Mix.env() == :dev},
       {:dart_sass, "0.7.0", runtime: Mix.env() == :dev},
       {:bulma, "1.0.2"},
       {:swoosh, "~> 1.5"},
@@ -74,7 +74,7 @@ defmodule Bright.MixProject do
       {:bento, "~> 1.0"},
       {:identicon_svg, "~> 0.9"},
       {:excoveralls, "~> 0.18", only: :test},
-      {:quinn, "~> 1.1.3"},
+      {:quinn, "~> 1.1.3"}
     ]
   end
 
diff --git a/apps/bright/mix.lock b/apps/bright/mix.lock
index ae87ca3..b2c44d7 100644
--- a/apps/bright/mix.lock
+++ b/apps/bright/mix.lock
@@ -9,7 +9,7 @@
   "bunch_native": {:hex, :bunch_native, "0.5.0", "8ac1536789a597599c10b652e0b526d8833348c19e4739a0759a2bedfd924e63", [:mix], [{:bundlex, "~> 1.0", [hex: :bundlex, repo: "hexpm", optional: false]}], "hexpm", "24190c760e32b23b36edeb2dc4852515c7c5b3b8675b1a864e0715bdd1c8f80d"},
   "bundlex": {:hex, :bundlex, "1.5.4", "3726acd463f4d31894a59bbc177c17f3b574634a524212f13469f41c4834a1d9", [:mix], [{:bunch, "~> 1.0", [hex: :bunch, repo: "hexpm", optional: false]}, {:elixir_uuid, "~> 1.2", [hex: :elixir_uuid, repo: "hexpm", optional: false]}, {:qex, "~> 0.5", [hex: :qex, repo: "hexpm", optional: false]}, {:req, ">= 0.4.0", [hex: :req, repo: "hexpm", optional: false]}, {:zarex, "~> 1.0", [hex: :zarex, repo: "hexpm", optional: false]}], "hexpm", "e745726606a560275182a8ac1c8ebd5e11a659bb7460d8abf30f397e59b4c5d2"},
   "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"},
-  "castore": {:hex, :castore, "1.0.11", "4bbd584741601eb658007339ea730b082cc61f3554cf2e8f39bf693a11b49073", [:mix], [], "hexpm", "e03990b4db988df56262852f20de0f659871c35154691427a5047f4967a16a62"},
+  "castore": {:hex, :castore, "1.0.12", "053f0e32700cbec356280c0e835df425a3be4bc1e0627b714330ad9d0f05497f", [:mix], [], "hexpm", "3dca286b2186055ba0c9449b4e95b97bf1b57b47c1f2644555879e659960c224"},
   "cc_precompiler": {:hex, :cc_precompiler, "0.1.10", "47c9c08d8869cf09b41da36538f62bc1abd3e19e41701c2cea2675b53c704258", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f6e046254e53cd6b41c6bacd70ae728011aa82b2742a80d6e2214855c6e06b22"},
   "certifi": {:hex, :certifi, "2.14.0", "ed3bef654e69cde5e6c022df8070a579a79e8ba2368a00acf3d75b82d9aceeed", [:rebar3], [], "hexpm", "ea59d87ef89da429b8e905264fdec3419f84f2215bb3d81e07a18aac919026c3"},
   "coerce": {:hex, :coerce, "1.0.1", "211c27386315dc2894ac11bc1f413a0e38505d808153367bd5c6e75a4003d096", [:mix], [], "hexpm", "b44a691700f7a1a15b4b7e2ff1fa30bebd669929ac8aa43cffe9e2f8bf051cf1"},
@@ -25,7 +25,7 @@
   "ecto_sql": {:hex, :ecto_sql, "3.12.1", "c0d0d60e85d9ff4631f12bafa454bc392ce8b9ec83531a412c12a0d415a3a4d0", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5"},
   "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"},
   "elixir_uuid": {:hex, :elixir_uuid, "1.2.1", "dce506597acb7e6b0daeaff52ff6a9043f5919a4c3315abb4143f0b00378c097", [:mix], [], "hexpm", "f7eba2ea6c3555cea09706492716b0d87397b88946e6380898c2889d68585752"},
-  "esbuild": {:hex, :esbuild, "0.8.2", "5f379dfa383ef482b738e7771daf238b2d1cfb0222bef9d3b20d4c8f06c7a7ac", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "558a8a08ed78eb820efbfda1de196569d8bfa9b51e8371a1934fbb31345feda7"},
+  "esbuild": {:hex, :esbuild, "0.9.0", "f043eeaca4932ca8e16e5429aebd90f7766f31ac160a25cbd9befe84f2bc068f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "b415027f71d5ab57ef2be844b2a10d0c1b5a492d431727f43937adce22ba45ae"},
   "ex_aws": {:hex, :ex_aws, "2.5.8", "0393cfbc5e4a9e7017845451a015d836a670397100aa4c86901980e2a2c5f7d4", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:req, "~> 0.3", [hex: :req, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8f79777b7932168956c8cc3a6db41f5783aa816eb50de356aed3165a71e5f8c3"},
   "ex_aws_s3": {:hex, :ex_aws_s3, "2.5.6", "d135983bbd8b6df6350dfd83999437725527c1bea151e5055760bfc9b2d17c20", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "9874e12847e469ca2f13a5689be04e546c16f63caf6380870b7f25bf7cb98875"},
   "ex_m3u8": {:hex, :ex_m3u8, "0.14.2", "3eb17f936e2ca2fdcde11664f3a543e75a94814d928098e050bda5b1e149c021", [:mix], [{:nimble_parsec, "~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:typed_struct, "~> 0.3.0", [hex: :typed_struct, repo: "hexpm", optional: false]}], "hexpm", "d2a1fb4382a521cce7f966502ecce6187f286ca2852dbb0dcc25dea72f8ba039"},
@@ -109,7 +109,6 @@
   "superstreamer_player": {:git, "https://github.com/superstreamerapp/superstreamer.git", "9e868acede851f396b3db98fb9799ab4bf712b02", [sparse: "packages/player"]},
   "sweet_xml": {:hex, :sweet_xml, "0.7.5", "803a563113981aaac202a1dbd39771562d0ad31004ddbfc9b5090bdcd5605277", [:mix], [], "hexpm", "193b28a9b12891cae351d81a0cead165ffe67df1b73fe5866d10629f4faefb12"},
   "swoosh": {:hex, :swoosh, "1.17.6", "27ff070f96246e35b7105ab1c52b2b689f523a3cb83ed9faadb2f33bd653ccba", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9798f3e72165f40c950f6762c06dab68afcdcf616138fc4a07965c09c250e1e2"},
-  "tailwind": {:hex, :tailwind, "0.2.4", "5706ec47182d4e7045901302bf3a333e80f3d1af65c442ba9a9eed152fb26c2e", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "c6e4a82b8727bab593700c998a4d98cf3d8025678bfde059aed71d0000c3e463"},
   "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
   "telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"},
   "telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"},
diff --git a/apps/bright/priv/repo/migrations/20250311210011_add_x_posts.exs b/apps/bright/priv/repo/migrations/20250311210011_add_x_posts.exs
index 0070d6d..4aea450 100644
--- a/apps/bright/priv/repo/migrations/20250311210011_add_x_posts.exs
+++ b/apps/bright/priv/repo/migrations/20250311210011_add_x_posts.exs
@@ -3,10 +3,14 @@ defmodule :"Elixir.Bright.Repo.Migrations.AddXPosts" do
 
   def change do
     create table(:x_posts) do
-      add :raw, :string, null: false                # Raw content of the tweet
-      add :url, :string, null: false                # URL of the tweet
-      add :date, :utc_datetime, null: false         # Date and time of the tweet
-      add :is_invitation, :boolean, default: false  # Whether the tweet contains an invite link
+      # Raw content of the tweet
+      add :raw, :string, null: false
+      # URL of the tweet
+      add :url, :string, null: false
+      # Date and time of the tweet
+      add :date, :utc_datetime, null: false
+      # Whether the tweet contains an invite link
+      add :is_invitation, :boolean, default: false
 
       add :vtuber_id, references(:vtubers, on_delete: :delete_all), null: false
 
@@ -19,5 +23,4 @@ defmodule :"Elixir.Bright.Repo.Migrations.AddXPosts" do
     # Add an index on the `date` field for sorting and filtering
     create index(:x_posts, [:date])
   end
-
 end
diff --git a/apps/bright/priv/repo/migrations/20250320193227_create_uploads.exs b/apps/bright/priv/repo/migrations/20250320193227_create_uploads.exs
new file mode 100644
index 0000000..3185746
--- /dev/null
+++ b/apps/bright/priv/repo/migrations/20250320193227_create_uploads.exs
@@ -0,0 +1,16 @@
+defmodule Bright.Repo.Migrations.CreateUploads do
+  use Ecto.Migration
+
+  def change do
+    create table(:uploads) do
+      add :filename, :string
+      add :size, :integer
+      add :content_type, :string
+      add :user_id, references(:users, on_delete: :nothing)
+
+      timestamps(type: :utc_datetime)
+    end
+
+    create index(:uploads, [:user_id])
+  end
+end
diff --git a/apps/bright/priv/repo/migrations/20250323033920_create_uploads.exs b/apps/bright/priv/repo/migrations/20250323033920_create_uploads.exs
new file mode 100644
index 0000000..0f5271c
--- /dev/null
+++ b/apps/bright/priv/repo/migrations/20250323033920_create_uploads.exs
@@ -0,0 +1,18 @@
+defmodule Bright.Repo.Migrations.CreateUploads do
+  use Ecto.Migration
+
+  def change do
+    create table(:uploads) do
+      add :filename, :string
+      add :size, :integer
+      add :content_type, :string
+      add :user_id, references(:users, on_delete: :nothing)
+      add :vod, references(:vods, on_delete: :nothing)
+
+      timestamps(type: :utc_datetime)
+    end
+
+    create index(:uploads, [:user_id])
+    create index(:uploads, [:vod])
+  end
+end
diff --git a/apps/bright/priv/static/favicon-5b6eb29e50adf329d96e0eeca8d2d0ee.ico b/apps/bright/priv/static/favicon-5b6eb29e50adf329d96e0eeca8d2d0ee.ico
new file mode 100644
index 0000000..04e664e
Binary files /dev/null and b/apps/bright/priv/static/favicon-5b6eb29e50adf329d96e0eeca8d2d0ee.ico differ
diff --git a/apps/bright/test/bright/images_test.exs b/apps/bright/test/bright/images_test.exs
index 9bb7d14..91c15f0 100644
--- a/apps/bright/test/bright/images_test.exs
+++ b/apps/bright/test/bright/images_test.exs
@@ -3,6 +3,7 @@ defmodule Bright.ImagesTest do
 
   require Logger
   alias Bright.Images
+  alias Bright.Utils
 
   @test_mp4_fixture "./test/fixtures/SampleVideo_1280x720_1mb.mp4"
   @test_ts_fixture "./test/fixtures/test-fixture.ts"
@@ -24,13 +25,7 @@ defmodule Bright.ImagesTest do
 
       basename = "thumb.jpg"
 
-      random_string =
-        for _ <- 1..12,
-            into: "",
-            do:
-              <<Enum.random(~c"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")>>
-
-      output_file = "/tmp/#{random_string}-#{basename}"
+      output_file = "/tmp/#{Utils.random_string()}-#{basename}"
 
       Logger.debug(
         "output_file=#{inspect(output_file)} @test_mp4_fixture=#{inspect(@test_mp4_fixture)}"
diff --git a/apps/bright/test/bright/oban_workers/process_posts_test.exs b/apps/bright/test/bright/oban_workers/process_posts_test.exs
index a1ae486..6fdbcb9 100644
--- a/apps/bright/test/bright/oban_workers/process_posts_test.exs
+++ b/apps/bright/test/bright/oban_workers/process_posts_test.exs
@@ -56,7 +56,7 @@ defmodule Bright.ProcessPostsTest do
       Socials.create_x_post(%{
         raw: "I'm going live! fansly.com/fakename <3",
         url: "https://x.com/fakename/status/283498235",
-        date: DateTime.utc_now(:second),
+        date: ~U[2025-03-18T13:25:39.000Z],
         processed_at: nil,
         vtuber_id: vtuber.id
       }),
@@ -64,7 +64,7 @@ defmodule Bright.ProcessPostsTest do
       Socials.create_x_post(%{
         raw: "gm! tiem for sex breakfast https://onlyfans.com/fakename",
         url: "https://x.com/fakename/status/283498234",
-        date: DateTime.utc_now(:second),
+        date: ~U[2025-03-18T12:00:00.000Z],
         processed_at: nil,
         vtuber_id: vtuber.id
       }),
@@ -72,7 +72,7 @@ defmodule Bright.ProcessPostsTest do
       Socials.create_x_post(%{
         raw: "ero strim rn http://chaturbate.com/fakename",
         url: "https://x.com/fakename/status/283498232",
-        date: DateTime.utc_now(:second),
+        date: ~U[2025-03-18T10:02:49.000Z],
         processed_at: nil,
         vtuber_id: vtuber.id
       }),
@@ -80,7 +80,7 @@ defmodule Bright.ProcessPostsTest do
       Socials.create_x_post(%{
         raw: "Join NOW for some fun! https://example.buzz",
         url: "https://x.com/fakename/status/394848232",
-        date: DateTime.utc_now(:second),
+        date: ~U[2025-03-18T13:00:03.000Z],
         processed_at: nil,
         vtuber_id: vtuber.id
       }),
@@ -89,7 +89,7 @@ defmodule Bright.ProcessPostsTest do
       Socials.create_x_post(%{
         raw: "Let's play a game http://twitch.tv/fakename",
         url: "https://x.com/fakename/status/283498343",
-        date: DateTime.utc_now(:second),
+        date: ~U[2025-03-18T03:19:23.000Z],
         processed_at: nil,
         vtuber_id: vtuber.id
       }),
@@ -98,7 +98,7 @@ defmodule Bright.ProcessPostsTest do
         raw:
           "Be sure to follow me on my socials http://chaturbate.com/fakename http://twitch.tv/fakename http://onlyfans.com/fakename http://linktree.com/fakename",
         url: "https://x.com/fakename/status/283498349",
-        date: DateTime.utc_now(:second),
+        date: ~U[2025-03-18T14:31:30.000Z],
         processed_at: nil,
         vtuber_id: vtuber.id
       })
@@ -114,8 +114,7 @@ defmodule Bright.ProcessPostsTest do
     test "create a stream for each post containing an invite link" do
       {:ok, _} = perform_job(Bright.ObanWorkers.ProcessPosts, %{})
       streams = Repo.all(Stream)
-
-      assert length(streams) == 4
+      assert length(streams) === 4
     end
 
     @tag :integration
diff --git a/apps/bright/test/bright/oban_workers/process_vod_test.exs b/apps/bright/test/bright/oban_workers/process_vod_test.exs
index 0d7aad4..ed2b903 100644
--- a/apps/bright/test/bright/oban_workers/process_vod_test.exs
+++ b/apps/bright/test/bright/oban_workers/process_vod_test.exs
@@ -36,7 +36,11 @@ defmodule Bright.ObanWorkers.ProcessVodTest do
     test "schedule ProcessVod when a new vod is created" do
       stream = stream_fixture()
 
-      vod_fixture(%{thumbnail_url: nil, stream_id: stream.id, origin_temp_input_url: @example_url})
+      vod_fixture(%{
+        thumbnail_url: nil,
+        stream_id: stream.id,
+        origin_temp_input_url: @example_url
+      })
 
       assert_enqueued(worker: ProcessVod, queue: :default)
     end
@@ -45,7 +49,11 @@ defmodule Bright.ObanWorkers.ProcessVodTest do
     test "schedule CreateThumbnail when thumbnail_url is nil" do
       stream = stream_fixture()
 
-      vod_fixture(%{thumbnail_url: nil, stream_id: stream.id, origin_temp_input_url: @example_url})
+      vod_fixture(%{
+        thumbnail_url: nil,
+        stream_id: stream.id,
+        origin_temp_input_url: @example_url
+      })
 
       assert_enqueued(worker: ProcessVod, queue: :default)
       # ProcessVod is what queues CreateThumbnail so we need to make it run
diff --git a/apps/bright/test/bright/platforms_test.exs b/apps/bright/test/bright/platforms_test.exs
index 2f53504..3038fc2 100644
--- a/apps/bright/test/bright/platforms_test.exs
+++ b/apps/bright/test/bright/platforms_test.exs
@@ -95,4 +95,65 @@ defmodule Bright.PlatformsTest do
       assert not Platforms.contains_platform?([], [platformA])
     end
   end
+
+  describe "platform_aliases" do
+    alias Bright.Platforms.PlatformAlias
+
+    import Bright.PlatformsFixtures
+
+    @invalid_attrs %{url: nil}
+
+    test "list_platform_aliases/0 returns all platform_aliases" do
+      platform_alias = platform_alias_fixture()
+      assert Platforms.list_platform_aliases() == [platform_alias]
+    end
+
+    test "get_platform_alias!/1 returns the platform_alias with given id" do
+      platform_alias = platform_alias_fixture()
+      assert Platforms.get_platform_alias!(platform_alias.id) == platform_alias
+    end
+
+    test "create_platform_alias/1 with valid data creates a platform_alias" do
+      valid_attrs = %{url: "some url"}
+
+      assert {:ok, %PlatformAlias{} = platform_alias} =
+               Platforms.create_platform_alias(valid_attrs)
+
+      assert platform_alias.url == "some url"
+    end
+
+    test "create_platform_alias/1 with invalid data returns error changeset" do
+      assert {:error, %Ecto.Changeset{}} = Platforms.create_platform_alias(@invalid_attrs)
+    end
+
+    test "update_platform_alias/2 with valid data updates the platform_alias" do
+      platform_alias = platform_alias_fixture()
+      update_attrs = %{url: "some updated url"}
+
+      assert {:ok, %PlatformAlias{} = platform_alias} =
+               Platforms.update_platform_alias(platform_alias, update_attrs)
+
+      assert platform_alias.url == "some updated url"
+    end
+
+    test "update_platform_alias/2 with invalid data returns error changeset" do
+      platform_alias = platform_alias_fixture()
+
+      assert {:error, %Ecto.Changeset{}} =
+               Platforms.update_platform_alias(platform_alias, @invalid_attrs)
+
+      assert platform_alias == Platforms.get_platform_alias!(platform_alias.id)
+    end
+
+    test "delete_platform_alias/1 deletes the platform_alias" do
+      platform_alias = platform_alias_fixture()
+      assert {:ok, %PlatformAlias{}} = Platforms.delete_platform_alias(platform_alias)
+      assert_raise Ecto.NoResultsError, fn -> Platforms.get_platform_alias!(platform_alias.id) end
+    end
+
+    test "change_platform_alias/1 returns a platform_alias changeset" do
+      platform_alias = platform_alias_fixture()
+      assert %Ecto.Changeset{} = Platforms.change_platform_alias(platform_alias)
+    end
+  end
 end
diff --git a/apps/bright/test/bright/socials/x_post_test.exs b/apps/bright/test/bright/socials/x_post_test.exs
index ad405ce..c29946d 100644
--- a/apps/bright/test/bright/socials/x_post_test.exs
+++ b/apps/bright/test/bright/socials/x_post_test.exs
@@ -449,6 +449,25 @@ defmodule Bright.XPostTest do
       assert XPost.is_nsfw_live_announcement?(x_post, known_platforms)
     end
 
+    test "should return false when receiving an XPost mentioning a Fansly /post", %{
+      known_platforms: known_platforms
+    } do
+      vtuber = VtubersFixtures.el_xox_fixture()
+
+      x_post =
+        %XPost{
+          url: "https://x.com/el_XoX34/status/1899913287808262522",
+          raw:
+            "Being a good girl and taking all of your big cock inside me 🍆💦 Subscribe Tier 2 Cummy Bunny 🐰💙 or pay $15 to see 5 spicy photos!! ▶️ fansly.com/post/754877662",
+          date: ~U[2025-03-12T20:00:01Z],
+          processed_at: nil,
+          vtuber_id: vtuber.id
+        }
+        |> Repo.preload(:vtuber)
+
+      assert not XPost.is_nsfw_live_announcement?(x_post, known_platforms)
+    end
+
     test "should return false when the XPost is a retweet", %{
       known_platforms: known_platforms
     } do
diff --git a/apps/bright/test/bright/streams_test.exs b/apps/bright/test/bright/streams_test.exs
index 2e9875f..14277e5 100644
--- a/apps/bright/test/bright/streams_test.exs
+++ b/apps/bright/test/bright/streams_test.exs
@@ -188,4 +188,62 @@ defmodule Bright.StreamsTest do
       # assert_received {:progress, %{stage: :generating_thumbnail, done: 1, total: 1}}
     end
   end
+
+  describe "uploads" do
+    alias Bright.Streams.Upload
+
+    import Bright.StreamsFixtures
+
+    @invalid_attrs %{size: nil, filename: nil, content_type: nil}
+
+    test "list_uploads/0 returns all uploads" do
+      upload = upload_fixture()
+      assert Streams.list_uploads() == [upload]
+    end
+
+    test "get_upload!/1 returns the upload with given id" do
+      upload = upload_fixture()
+      assert Streams.get_upload!(upload.id) == upload
+    end
+
+    test "create_upload/1 with valid data creates a upload" do
+      valid_attrs = %{size: 42, filename: "some filename", content_type: "some content_type"}
+
+      assert {:ok, %Upload{} = upload} = Streams.create_upload(valid_attrs)
+      assert upload.size == 42
+      assert upload.filename == "some filename"
+      assert upload.content_type == "some content_type"
+    end
+
+    test "create_upload/1 with invalid data returns error changeset" do
+      assert {:error, %Ecto.Changeset{}} = Streams.create_upload(@invalid_attrs)
+    end
+
+    test "update_upload/2 with valid data updates the upload" do
+      upload = upload_fixture()
+      update_attrs = %{size: 43, filename: "some updated filename", content_type: "some updated content_type"}
+
+      assert {:ok, %Upload{} = upload} = Streams.update_upload(upload, update_attrs)
+      assert upload.size == 43
+      assert upload.filename == "some updated filename"
+      assert upload.content_type == "some updated content_type"
+    end
+
+    test "update_upload/2 with invalid data returns error changeset" do
+      upload = upload_fixture()
+      assert {:error, %Ecto.Changeset{}} = Streams.update_upload(upload, @invalid_attrs)
+      assert upload == Streams.get_upload!(upload.id)
+    end
+
+    test "delete_upload/1 deletes the upload" do
+      upload = upload_fixture()
+      assert {:ok, %Upload{}} = Streams.delete_upload(upload)
+      assert_raise Ecto.NoResultsError, fn -> Streams.get_upload!(upload.id) end
+    end
+
+    test "change_upload/1 returns a upload changeset" do
+      upload = upload_fixture()
+      assert %Ecto.Changeset{} = Streams.change_upload(upload)
+    end
+  end
 end
diff --git a/apps/bright/test/bright_web/controllers/platform_alias_controller_test.exs b/apps/bright/test/bright_web/controllers/platform_alias_controller_test.exs
new file mode 100644
index 0000000..40c8655
--- /dev/null
+++ b/apps/bright/test/bright_web/controllers/platform_alias_controller_test.exs
@@ -0,0 +1,87 @@
+defmodule BrightWeb.PlatformAliasControllerTest do
+  use BrightWeb.ConnCase
+
+  import Bright.PlatformsFixtures
+
+  @create_attrs %{url: "some url"}
+  @update_attrs %{url: "some updated url"}
+  @invalid_attrs %{url: nil}
+
+  describe "index" do
+    test "lists all platform_aliases", %{conn: conn} do
+      conn = get(conn, ~p"/platform_aliases")
+      assert html_response(conn, 200) =~ "Listing Platform aliases"
+    end
+  end
+
+  describe "new platform_alias" do
+    test "renders form", %{conn: conn} do
+      conn = get(conn, ~p"/platform_aliases/new")
+      assert html_response(conn, 200) =~ "New Platform alias"
+    end
+  end
+
+  describe "create platform_alias" do
+    test "redirects to show when data is valid", %{conn: conn} do
+      conn = post(conn, ~p"/platform_aliases", platform_alias: @create_attrs)
+
+      assert %{id: id} = redirected_params(conn)
+      assert redirected_to(conn) == ~p"/platform_aliases/#{id}"
+
+      conn = get(conn, ~p"/platform_aliases/#{id}")
+      assert html_response(conn, 200) =~ "Platform alias #{id}"
+    end
+
+    test "renders errors when data is invalid", %{conn: conn} do
+      conn = post(conn, ~p"/platform_aliases", platform_alias: @invalid_attrs)
+      assert html_response(conn, 200) =~ "New Platform alias"
+    end
+  end
+
+  describe "edit platform_alias" do
+    setup [:create_platform_alias]
+
+    test "renders form for editing chosen platform_alias", %{
+      conn: conn,
+      platform_alias: platform_alias
+    } do
+      conn = get(conn, ~p"/platform_aliases/#{platform_alias}/edit")
+      assert html_response(conn, 200) =~ "Edit Platform alias"
+    end
+  end
+
+  describe "update platform_alias" do
+    setup [:create_platform_alias]
+
+    test "redirects when data is valid", %{conn: conn, platform_alias: platform_alias} do
+      conn = put(conn, ~p"/platform_aliases/#{platform_alias}", platform_alias: @update_attrs)
+      assert redirected_to(conn) == ~p"/platform_aliases/#{platform_alias}"
+
+      conn = get(conn, ~p"/platform_aliases/#{platform_alias}")
+      assert html_response(conn, 200) =~ "some updated url"
+    end
+
+    test "renders errors when data is invalid", %{conn: conn, platform_alias: platform_alias} do
+      conn = put(conn, ~p"/platform_aliases/#{platform_alias}", platform_alias: @invalid_attrs)
+      assert html_response(conn, 200) =~ "Edit Platform alias"
+    end
+  end
+
+  describe "delete platform_alias" do
+    setup [:create_platform_alias]
+
+    test "deletes chosen platform_alias", %{conn: conn, platform_alias: platform_alias} do
+      conn = delete(conn, ~p"/platform_aliases/#{platform_alias}")
+      assert redirected_to(conn) == ~p"/platform_aliases"
+
+      assert_error_sent 404, fn ->
+        get(conn, ~p"/platform_aliases/#{platform_alias}")
+      end
+    end
+  end
+
+  defp create_platform_alias(_) do
+    platform_alias = platform_alias_fixture()
+    %{platform_alias: platform_alias}
+  end
+end
diff --git a/apps/bright/test/bright_web/controllers/upload_controller_test.exs b/apps/bright/test/bright_web/controllers/upload_controller_test.exs
new file mode 100644
index 0000000..d61ef8b
--- /dev/null
+++ b/apps/bright/test/bright_web/controllers/upload_controller_test.exs
@@ -0,0 +1,84 @@
+defmodule BrightWeb.UploadControllerTest do
+  use BrightWeb.ConnCase
+
+  import Bright.StreamsFixtures
+
+  @create_attrs %{size: 42, filename: "some filename", content_type: "some content_type"}
+  @update_attrs %{size: 43, filename: "some updated filename", content_type: "some updated content_type"}
+  @invalid_attrs %{size: nil, filename: nil, content_type: nil}
+
+  describe "index" do
+    test "lists all uploads", %{conn: conn} do
+      conn = get(conn, ~p"/uploads")
+      assert html_response(conn, 200) =~ "Listing Uploads"
+    end
+  end
+
+  describe "new upload" do
+    test "renders form", %{conn: conn} do
+      conn = get(conn, ~p"/uploads/new")
+      assert html_response(conn, 200) =~ "New Upload"
+    end
+  end
+
+  describe "create upload" do
+    test "redirects to show when data is valid", %{conn: conn} do
+      conn = post(conn, ~p"/uploads", upload: @create_attrs)
+
+      assert %{id: id} = redirected_params(conn)
+      assert redirected_to(conn) == ~p"/uploads/#{id}"
+
+      conn = get(conn, ~p"/uploads/#{id}")
+      assert html_response(conn, 200) =~ "Upload #{id}"
+    end
+
+    test "renders errors when data is invalid", %{conn: conn} do
+      conn = post(conn, ~p"/uploads", upload: @invalid_attrs)
+      assert html_response(conn, 200) =~ "New Upload"
+    end
+  end
+
+  describe "edit upload" do
+    setup [:create_upload]
+
+    test "renders form for editing chosen upload", %{conn: conn, upload: upload} do
+      conn = get(conn, ~p"/uploads/#{upload}/edit")
+      assert html_response(conn, 200) =~ "Edit Upload"
+    end
+  end
+
+  describe "update upload" do
+    setup [:create_upload]
+
+    test "redirects when data is valid", %{conn: conn, upload: upload} do
+      conn = put(conn, ~p"/uploads/#{upload}", upload: @update_attrs)
+      assert redirected_to(conn) == ~p"/uploads/#{upload}"
+
+      conn = get(conn, ~p"/uploads/#{upload}")
+      assert html_response(conn, 200) =~ "some updated filename"
+    end
+
+    test "renders errors when data is invalid", %{conn: conn, upload: upload} do
+      conn = put(conn, ~p"/uploads/#{upload}", upload: @invalid_attrs)
+      assert html_response(conn, 200) =~ "Edit Upload"
+    end
+  end
+
+  describe "delete upload" do
+    setup [:create_upload]
+
+    test "deletes chosen upload", %{conn: conn, upload: upload} do
+      conn = delete(conn, ~p"/uploads/#{upload}")
+      assert redirected_to(conn) == ~p"/uploads"
+
+      assert_error_sent 404, fn ->
+        get(conn, ~p"/uploads/#{upload}")
+      end
+    end
+  end
+
+  defp create_upload(_) do
+    upload = upload_fixture()
+    %{upload: upload}
+  end
+end
diff --git a/apps/bright/test/bright_web/live/upload_live_test.exs b/apps/bright/test/bright_web/live/upload_live_test.exs
new file mode 100644
index 0000000..3e789d8
--- /dev/null
+++ b/apps/bright/test/bright_web/live/upload_live_test.exs
@@ -0,0 +1,113 @@
+defmodule BrightWeb.UploadLiveTest do
+  use BrightWeb.ConnCase
+
+  import Phoenix.LiveViewTest
+  import Bright.StreamsFixtures
+
+  @create_attrs %{size: 42, filename: "some filename", content_type: "some content_type"}
+  @update_attrs %{size: 43, filename: "some updated filename", content_type: "some updated content_type"}
+  @invalid_attrs %{size: nil, filename: nil, content_type: nil}
+
+  defp create_upload(_) do
+    upload = upload_fixture()
+    %{upload: upload}
+  end
+
+  describe "Index" do
+    setup [:create_upload]
+
+    test "lists all uploads", %{conn: conn, upload: upload} do
+      {:ok, _index_live, html} = live(conn, ~p"/uploads")
+
+      assert html =~ "Listing Uploads"
+      assert html =~ upload.filename
+    end
+
+    test "saves new upload", %{conn: conn} do
+      {:ok, index_live, _html} = live(conn, ~p"/uploads")
+
+      assert index_live |> element("a", "New Upload") |> render_click() =~
+               "New Upload"
+
+      assert_patch(index_live, ~p"/uploads/new")
+
+      assert index_live
+             |> form("#upload-form", upload: @invalid_attrs)
+             |> render_change() =~ "can&#39;t be blank"
+
+      assert index_live
+             |> form("#upload-form", upload: @create_attrs)
+             |> render_submit()
+
+      assert_patch(index_live, ~p"/uploads")
+
+      html = render(index_live)
+      assert html =~ "Upload created successfully"
+      assert html =~ "some filename"
+    end
+
+    test "updates upload in listing", %{conn: conn, upload: upload} do
+      {:ok, index_live, _html} = live(conn, ~p"/uploads")
+
+      assert index_live |> element("#uploads-#{upload.id} a", "Edit") |> render_click() =~
+               "Edit Upload"
+
+      assert_patch(index_live, ~p"/uploads/#{upload}/edit")
+
+      assert index_live
+             |> form("#upload-form", upload: @invalid_attrs)
+             |> render_change() =~ "can&#39;t be blank"
+
+      assert index_live
+             |> form("#upload-form", upload: @update_attrs)
+             |> render_submit()
+
+      assert_patch(index_live, ~p"/uploads")
+
+      html = render(index_live)
+      assert html =~ "Upload updated successfully"
+      assert html =~ "some updated filename"
+    end
+
+    test "deletes upload in listing", %{conn: conn, upload: upload} do
+      {:ok, index_live, _html} = live(conn, ~p"/uploads")
+
+      assert index_live |> element("#uploads-#{upload.id} a", "Delete") |> render_click()
+      refute has_element?(index_live, "#uploads-#{upload.id}")
+    end
+  end
+
+  describe "Show" do
+    setup [:create_upload]
+
+    test "displays upload", %{conn: conn, upload: upload} do
+      {:ok, _show_live, html} = live(conn, ~p"/uploads/#{upload}")
+
+      assert html =~ "Show Upload"
+      assert html =~ upload.filename
+    end
+
+    test "updates upload within modal", %{conn: conn, upload: upload} do
+      {:ok, show_live, _html} = live(conn, ~p"/uploads/#{upload}")
+
+      assert show_live |> element("a", "Edit") |> render_click() =~
+               "Edit Upload"
+
+      assert_patch(show_live, ~p"/uploads/#{upload}/show/edit")
+
+      assert show_live
+             |> form("#upload-form", upload: @invalid_attrs)
+             |> render_change() =~ "can&#39;t be blank"
+
+      assert show_live
+             |> form("#upload-form", upload: @update_attrs)
+             |> render_submit()
+
+      assert_patch(show_live, ~p"/uploads/#{upload}")
+
+      html = render(show_live)
+      assert html =~ "Upload updated successfully"
+      assert html =~ "some updated filename"
+    end
+  end
+end
diff --git a/apps/bright/test/support/fixtures/platforms_fixtures.ex b/apps/bright/test/support/fixtures/platforms_fixtures.ex
index c865ce7..6db525a 100644
--- a/apps/bright/test/support/fixtures/platforms_fixtures.ex
+++ b/apps/bright/test/support/fixtures/platforms_fixtures.ex
@@ -4,9 +4,6 @@ defmodule Bright.PlatformsFixtures do
   entities via the `Bright.Platforms` context.
   """
 
-  alias Bright.Platforms.Platform
-  alias Bright.Platforms.PlatformAlias
-
   @doc """
   Generate a platform.
   """
@@ -55,7 +52,7 @@ defmodule Bright.PlatformsFixtures do
       }
       |> Bright.Platforms.create_platform()
 
-    {:ok, platform_alias} =
+    {:ok, _platform_alias} =
       %{
         url: "https://melody.buzz",
         platform_id: platform.id
@@ -112,4 +109,18 @@ defmodule Bright.PlatformsFixtures do
 
     platform
   end
+
+  @doc """
+  Generate a platform_alias.
+  """
+  def platform_alias_fixture(attrs \\ %{}) do
+    {:ok, platform_alias} =
+      attrs
+      |> Enum.into(%{
+        url: "some url"
+      })
+      |> Bright.Platforms.create_platform_alias()
+
+    platform_alias
+  end
 end
diff --git a/apps/bright/test/support/fixtures/streams_fixtures.ex b/apps/bright/test/support/fixtures/streams_fixtures.ex
index 89a5a08..924e084 100644
--- a/apps/bright/test/support/fixtures/streams_fixtures.ex
+++ b/apps/bright/test/support/fixtures/streams_fixtures.ex
@@ -36,4 +36,20 @@ defmodule Bright.StreamsFixtures do
 
     vod
   end
+
+  @doc """
+  Generate a upload.
+  """
+  def upload_fixture(attrs \\ %{}) do
+    {:ok, upload} =
+      attrs
+      |> Enum.into(%{
+        content_type: "some content_type",
+        filename: "some filename",
+        size: 42
+      })
+      |> Bright.Streams.create_upload()
+
+    upload
+  end
 end
diff --git a/apps/bright/test/support/fixtures/x_posts_fixtures.ex b/apps/bright/test/support/fixtures/x_posts_fixtures.ex
index c230a8e..5f0bc39 100644
--- a/apps/bright/test/support/fixtures/x_posts_fixtures.ex
+++ b/apps/bright/test/support/fixtures/x_posts_fixtures.ex
@@ -1,5 +1,4 @@
 defmodule Bright.XPostsFixtures do
-  alias Bright.Socials.XPost
   alias Bright.Socials
 
   @moduledoc """
@@ -149,7 +148,7 @@ defmodule Bright.XPostsFixtures do
     {:ok, x_post} =
       attrs
       |> Enum.into(defaults)
-      |> XPost.create_x_post()
+      |> Socials.create_x_post()
 
     x_post
   end
diff --git a/docker-compose.yml b/docker-compose.yml
index 42f18c7..3fc5e7a 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -64,7 +64,15 @@ services:
   #       # - path: /home/cj/Documents/ueberauth_patreon
   #       #   action: sync
   #       #   target: /app/contrib/ueberauth_patreon
-
+  # uppy:
+  #   image: transloadit/companion:latest
+  #   container_name: uppy-companion
+  #   env_file:
+  #     - .env.development
+  #   volumes:
+  #     - uppy_data:/mnt/uppy-server-data
+  #   ports:
+  #     - '3020:3020'
 
   db:
     image: postgres:15
@@ -96,3 +104,4 @@ services:
 volumes:
   pg_data:
   cache:
+  uppy_data:
diff --git a/terraform/main.tf b/terraform/main.tf
index 270563e..82ae4a1 100644
--- a/terraform/main.tf
+++ b/terraform/main.tf
@@ -91,6 +91,7 @@ resource "vultr_reserved_ip" "futureporn_tracker_ip" {
   ip_type = "v4"
 }
 
+
 # Virtual Private Cloud for connecting many VPS together on a private network
 # We use this network connection for app<->db comms.
 resource "vultr_vpc2" "futureporn_vpc2" {
@@ -155,6 +156,29 @@ resource "vultr_instance" "capture_vps" {
   user_data       = base64encode(var.vps_user_data)
 }
 
+
+# vultr instance with a GPU. experimental.
+# resource "vultr_instance" "capture_vps" {
+#   count           = 0
+#   hostname        = "fp-cap-${count.index}"
+#   plan            = "vcg-a16-2c-8g-2vram"
+#   region          = "ord"
+#   backups         = "disabled"
+#   ddos_protection = "false"
+#   # os_id           = 1743
+#   image_id = "ubuntu-xfce"
+#   app_variables = {
+#     desktopuser = "cj_clippy"
+#   }
+#   enable_ipv6 = true
+#   vpc2_ids    = [vultr_vpc2.futureporn_vpc2.id]
+#   label       = "fp capture ${count.index}"
+#   tags        = ["futureporn", "capture"]
+#   ssh_key_ids = [local.envs.VULTR_SSH_KEY_ID]
+#   user_data   = base64encode(var.vps_user_data)
+# }
+
+
 resource "vultr_instance" "database" {
   count    = 1
   hostname = "fp-db-${count.index}"
@@ -254,6 +278,7 @@ resource "ansible_group" "capture" {
   name = "capture"
 }
 
+
 resource "ansible_group" "bright" {
   name = "bright"
 }
@@ -277,7 +302,7 @@ resource "ansible_group" "futureporn" {
     "database",
     "capture",
     "bright",
-    "tracker",
+    "tracker"
   ]
 }