Repository: IgnisDa/ryot (language: rust)

Issue:
Watchlists



Discussion / hints:

Fix patch:
diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml
index c9083c7d91..c8b65e2a5b 100644
--- a/.devcontainer/docker-compose.yml
+++ b/.devcontainer/docker-compose.yml
@@ -18,19 +18,19 @@ services:
       - "9000:9000"
       - "9001:9001"
     volumes:
-      - minio_storage:/data
+      - ryot_minio_storage:/data
     command: server --console-address ":9001" /data
 
   ryot-postgres:
     image: postgres:16-alpine
     restart: unless-stopped
     volumes:
-      - postgres_storage:/var/lib/postgresql/data
+      - ryot_postgres_storage:/var/lib/postgresql/data
     environment:
       POSTGRES_PASSWORD: postgres
       POSTGRES_USER: postgres
       POSTGRES_DB: postgres
 
 volumes:
-  minio_storage:
-  postgres_storage:
+  ryot_minio_storage:
+  ryot_postgres_storage:
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index c981a4ae2c..3fbd282d27 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -41,7 +41,9 @@ jobs:
 
             let imageNames = [
               `name=${dockerUsername}/${repositoryName}`,
-              `name=${ghcrRegistry}/${owner}/${repositoryName}`
+              `name=${ghcrRegistry}/${owner}/${repositoryName}`,
+              `name=${dockerUsername}/${repositoryName}-pro`,
+              `name=${ghcrRegistry}/${owner}/${repositoryName}-pro`,
             ];
 
             let shouldRun = 'false';
@@ -55,9 +57,6 @@ jobs:
               }).then(commit => commit.data.commit.message);
               if (commitMsg.includes("Run CI")) {
                 shouldRun = 'true';
-                if (context.payload.pull_request.head.repo.full_name !== context.repo.full_name) {
-                  imageNames.shift();
-                }
               }
             }
 
@@ -175,6 +174,8 @@ jobs:
           APP_VERSION: ${{ steps.build.outputs.version }}
           DEFAULT_TMDB_ACCESS_TOKEN: ${{ secrets.DEFAULT_TMDB_ACCESS_TOKEN }}
           DEFAULT_MAL_CLIENT_ID: ${{ secrets.DEFAULT_MAL_CLIENT_ID }}
+          TRAKT_CLIENT_ID: ${{ secrets.TRAKT_CLIENT_ID }}
+          UNKEY_API_ID: ${{ secrets.UNKEY_API_ID }}
         run: |
           ${{ matrix.platform.command }} build --locked --target ${{ matrix.platform.target }} --release
       - name: Upload build artifact
diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml
new file mode 100644
index 0000000000..72c459d5b1
--- /dev/null
+++ b/.github/workflows/website.yml
@@ -0,0 +1,38 @@
+name: Website
+
+on:
+  workflow_dispatch:
+
+env:
+  GHCR_REGISTRY: ghcr.io
+
+permissions:
+  contents: write
+  packages: write
+
+
+jobs:
+  docker-release:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+
+      - name: Set up Docker Buildx
+        uses: docker/setup-buildx-action@v3
+
+      - name: Log in to the ghcr container registry
+        uses: docker/login-action@v3
+        with:
+          registry: ${{ env.GHCR_REGISTRY }}
+          username: ${{ github.actor }}
+          password: ${{ secrets.GITHUB_TOKEN }}
+
+      - name: Build and push to ghcr
+        uses: docker/build-push-action@v5
+        with:
+          context: .
+          push: true
+          cache-from: type=gha
+          cache-to: type=gha,mode=max
+          file: apps/website/Dockerfile
+          tags: ${{ env.GHCR_REGISTRY }}/ignisda/ryot-website:latest
diff --git a/.moon/workspace.yml b/.moon/workspace.yml
index 0dc04c849e..6f2dd156c7 100644
--- a/.moon/workspace.yml
+++ b/.moon/workspace.yml
@@ -8,6 +8,7 @@ projects:
   graphql: "libs/graphql"
   transactional: "libs/transactional"
   ts-utils: "libs/ts-utils"
+  website: "apps/website"
 
 vcs:
   manager: "git"
diff --git a/Cargo.lock b/Cargo.lock
index e426244964..9a2051db4a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -95,9 +95,9 @@ dependencies = [
 
 [[package]]
 name = "anstream"
-version = "0.6.15"
+version = "0.6.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526"
+checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b"
 dependencies = [
  "anstyle",
  "anstyle-parse",
@@ -110,33 +110,33 @@ dependencies = [
 
 [[package]]
 name = "anstyle"
-version = "1.0.8"
+version = "1.0.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
+checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b"
 
 [[package]]
 name = "anstyle-parse"
-version = "0.2.5"
+version = "0.2.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb"
+checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4"
 dependencies = [
  "utf8parse",
 ]
 
 [[package]]
 name = "anstyle-query"
-version = "1.1.1"
+version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a"
+checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391"
 dependencies = [
  "windows-sys 0.52.0",
 ]
 
 [[package]]
 name = "anstyle-wincon"
-version = "3.0.4"
+version = "3.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8"
+checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19"
 dependencies = [
  "anstyle",
  "windows-sys 0.52.0",
@@ -269,9 +269,9 @@ dependencies = [
 
 [[package]]
 name = "arrayvec"
-version = "0.7.6"
+version = "0.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
+checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
 
 [[package]]
 name = "ascii"
@@ -427,6 +427,17 @@ dependencies = [
  "serde_json",
 ]
 
+[[package]]
+name = "async-lock"
+version = "3.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18"
+dependencies = [
+ "event-listener 5.3.1",
+ "event-listener-strategy",
+ "pin-project-lite",
+]
+
 [[package]]
 name = "async-oneshot"
 version = "0.5.9"
@@ -504,9 +515,9 @@ dependencies = [
 
 [[package]]
 name = "aws-runtime"
-version = "1.4.0"
+version = "1.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f42c2d4218de4dcd890a109461e2f799a1a2ba3bcd2cde9af88360f5df9266c6"
+checksum = "87c5f920ffd1e0526ec9e70e50bf444db50b204395a0fa7016bbf9e31ea1698f"
 dependencies = [
  "aws-credential-types",
  "aws-sigv4",
@@ -520,7 +531,6 @@ dependencies = [
  "fastrand",
  "http 0.2.12",
  "http-body 0.4.6",
- "once_cell",
  "percent-encoding",
  "pin-project-lite",
  "tracing",
@@ -604,9 +614,9 @@ dependencies = [
 
 [[package]]
 name = "aws-smithy-checksums"
-version = "0.60.12"
+version = "0.60.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "598b1689d001c4d4dc3cb386adb07d37786783aee3ac4b324bcadac116bf3d23"
+checksum = "48c4134cf3adaeacff34d588dbe814200357b0c466d730cf1c0d8054384a2de4"
 dependencies = [
  "aws-smithy-http",
  "aws-smithy-types",
@@ -666,9 +676,9 @@ dependencies = [
 
 [[package]]
 name = "aws-smithy-runtime"
-version = "1.6.3"
+version = "1.6.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0abbf454960d0db2ad12684a1640120e7557294b0ff8e2f11236290a1b293225"
+checksum = "ce87155eba55e11768b8c1afa607f3e864ae82f03caf63258b37455b0ad02537"
 dependencies = [
  "aws-smithy-async",
  "aws-smithy-http",
@@ -693,9 +703,9 @@ dependencies = [
 
 [[package]]
 name = "aws-smithy-runtime-api"
-version = "1.7.2"
+version = "1.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e086682a53d3aa241192aa110fa8dfce98f2f5ac2ead0de84d41582c7e8fdb96"
+checksum = "30819352ed0a04ecf6a2f3477e344d2d1ba33d43e0f09ad9047c12e0d923616f"
 dependencies = [
  "aws-smithy-async",
  "aws-smithy-types",
@@ -710,9 +720,9 @@ dependencies = [
 
 [[package]]
 name = "aws-smithy-types"
-version = "1.2.2"
+version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6cee7cadb433c781d3299b916fbf620fea813bf38f49db282fb6858141a05cc8"
+checksum = "cfe321a6b21f5d8eabd0ade9c55d3d0335f3c3157fc2b3e87f05f34b539e4df5"
 dependencies = [
  "base64-simd",
  "bytes",
@@ -850,6 +860,7 @@ dependencies = [
  "database-models",
  "dependent-models",
  "dotenvy",
+ "env-utils",
  "exporter-resolver",
  "exporter-service",
  "file-storage-resolver",
@@ -871,7 +882,9 @@ dependencies = [
  "schematic",
  "sea-orm",
  "sea-orm-migration",
+ "serde",
  "serde_json",
+ "serde_with 3.9.0",
  "statistics-resolver",
  "statistics-service",
  "tokio",
@@ -879,6 +892,7 @@ dependencies = [
  "tower-http",
  "tracing",
  "tracing-subscriber",
+ "unkey",
  "user-resolver",
  "user-service",
 ]
@@ -1015,11 +1029,11 @@ dependencies = [
 
 [[package]]
 name = "block-buffer"
-version = "0.11.0-rc.1"
+version = "0.11.0-rc.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8969801e57d15e15bc4d7cdc5600dc15ca06a9a62b622bd4871c2d21d8aeb42d"
+checksum = "17092d478f4fadfb35a7e082f62e49f0907fdf048801d9d706277e34f9df8a78"
 dependencies = [
- "crypto-common 0.2.0-rc.1",
+ "crypto-common 0.2.0-rc.0",
 ]
 
 [[package]]
@@ -1093,9 +1107,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
 
 [[package]]
 name = "bytes"
-version = "1.7.1"
+version = "1.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50"
+checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952"
 dependencies = [
  "serde",
 ]
@@ -1110,43 +1124,6 @@ dependencies = [
  "either",
 ]
 
-[[package]]
-name = "cached"
-version = "0.53.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b4d73155ae6b28cf5de4cfc29aeb02b8a1c6dab883cb015d15cd514e42766846"
-dependencies = [
- "ahash 0.8.11",
- "cached_proc_macro",
- "cached_proc_macro_types",
- "directories",
- "hashbrown 0.14.5",
- "once_cell",
- "rmp-serde",
- "serde",
- "sled",
- "thiserror",
- "web-time",
-]
-
-[[package]]
-name = "cached_proc_macro"
-version = "0.23.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2f42a145ed2d10dce2191e1dcf30cfccfea9026660e143662ba5eec4017d5daa"
-dependencies = [
- "darling",
- "proc-macro2",
- "quote",
- "syn 2.0.77",
-]
-
-[[package]]
-name = "cached_proc_macro_types"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0"
-
 [[package]]
 name = "castaway"
 version = "0.2.3"
@@ -1158,12 +1135,9 @@ dependencies = [
 
 [[package]]
 name = "cc"
-version = "1.1.13"
+version = "1.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "72db2f7947ecee9b03b510377e8bb9077afa27176fdbff55c51027e976fdcc48"
-dependencies = [
- "shlex",
-]
+checksum = "2aba8f4e9906c7ce3c73463f62a7f0c65183ada1a2d47e397cc8810827f9694f"
 
 [[package]]
 name = "cfg-if"
@@ -1226,9 +1200,9 @@ dependencies = [
 
 [[package]]
 name = "clap"
-version = "4.5.16"
+version = "4.5.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019"
+checksum = "64acc1846d54c1fe936a78dc189c34e28d3f5afc348403f28ecf53660b9b8462"
 dependencies = [
  "clap_builder",
  "clap_derive",
@@ -1236,9 +1210,9 @@ dependencies = [
 
 [[package]]
 name = "clap_builder"
-version = "4.5.15"
+version = "4.5.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6"
+checksum = "6fb8393d67ba2e7bfaf28a23458e4e2b543cc73a99595511eb207fdb8aede942"
 dependencies = [
  "anstream",
  "anstyle",
@@ -1248,9 +1222,9 @@ dependencies = [
 
 [[package]]
 name = "clap_derive"
-version = "4.5.13"
+version = "4.5.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0"
+checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085"
 dependencies = [
  "heck 0.5.0",
  "proc-macro2",
@@ -1260,9 +1234,9 @@ dependencies = [
 
 [[package]]
 name = "clap_lex"
-version = "0.7.2"
+version = "0.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97"
+checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70"
 
 [[package]]
 name = "collection-resolver"
@@ -1297,9 +1271,9 @@ dependencies = [
 
 [[package]]
 name = "colorchoice"
-version = "1.0.2"
+version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
+checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422"
 
 [[package]]
 name = "combine"
@@ -1349,7 +1323,7 @@ dependencies = [
  "chrono",
  "compile-time",
  "const-str",
- "dotenvy_macro",
+ "env-utils",
  "reqwest 0.12.7",
 ]
 
@@ -1380,13 +1354,22 @@ dependencies = [
  "time",
 ]
 
+[[package]]
+name = "concurrent-queue"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
+dependencies = [
+ "crossbeam-utils",
+]
+
 [[package]]
 name = "config"
 version = "0.1.0"
 dependencies = [
  "anyhow",
  "common-utils",
- "dotenvy_macro",
+ "env-utils",
  "schematic",
  "serde",
 ]
@@ -1424,15 +1407,15 @@ dependencies = [
 
 [[package]]
 name = "core-foundation-sys"
-version = "0.8.7"
+version = "0.8.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
 
 [[package]]
 name = "cpufeatures"
-version = "0.2.13"
+version = "0.2.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "51e852e6dc9a5bed1fae92dd2375037bf2b768725bf3be87811edee3249d09ad"
+checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504"
 dependencies = [
  "libc",
 ]
@@ -1481,6 +1464,15 @@ dependencies = [
  "once_cell",
 ]
 
+[[package]]
+name = "crossbeam-channel"
+version = "0.5.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2"
+dependencies = [
+ "crossbeam-utils",
+]
+
 [[package]]
 name = "crossbeam-epoch"
 version = "0.9.18"
@@ -1541,9 +1533,9 @@ dependencies = [
 
 [[package]]
 name = "crypto-common"
-version = "0.2.0-rc.1"
+version = "0.2.0-rc.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b0b8ce8218c97789f16356e7896b3714f26c2ee1079b79c0b7ae7064bb9089fa"
+checksum = "8c070b79a496dccd931229780ad5bbedd535ceff6c3565605a8e440e18e1aa2b"
 dependencies = [
  "hybrid-array",
 ]
@@ -1708,6 +1700,7 @@ dependencies = [
  "fitness-models",
  "futures",
  "itertools 0.13.0",
+ "jwt-service",
  "markdown",
  "media-models",
  "migrations",
@@ -1823,20 +1816,11 @@ version = "0.11.0-pre.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cf2e3d6615d99707295a9673e889bf363a04b2a466bd320c65a72536f7577379"
 dependencies = [
- "block-buffer 0.11.0-rc.1",
- "crypto-common 0.2.0-rc.1",
+ "block-buffer 0.11.0-rc.0",
+ "crypto-common 0.2.0-rc.0",
  "subtle",
 ]
 
-[[package]]
-name = "directories"
-version = "5.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35"
-dependencies = [
- "dirs-sys",
-]
-
 [[package]]
 name = "dirs"
 version = "5.0.1"
@@ -1891,22 +1875,16 @@ dependencies = [
 ]
 
 [[package]]
-name = "dotenvy"
-version = "0.15.7"
+name = "dotenv-build"
+version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
+checksum = "f4547f16c17f6051a12cdb8c62b803f94bee6807c74aa7c530b30b737df981fc"
 
 [[package]]
-name = "dotenvy_macro"
+name = "dotenvy"
 version = "0.15.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cb0235d912a8c749f4e0c9f18ca253b4c28cfefc1d2518096016d6e3230b6424"
-dependencies = [
- "dotenvy",
- "proc-macro2",
- "quote",
- "syn 1.0.109",
-]
+checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
 
 [[package]]
 name = "dtoa"
@@ -2081,9 +2059,9 @@ dependencies = [
 
 [[package]]
 name = "email_address"
-version = "0.2.9"
+version = "0.2.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449"
+checksum = "46b7a0ac6570e31bfe2c6cf575a576a55af9893d1a6b30b4444e6e90b216bb84"
 
 [[package]]
 name = "encode_unicode"
@@ -2139,6 +2117,13 @@ dependencies = [
  "strum",
 ]
 
+[[package]]
+name = "env-utils"
+version = "0.1.0"
+dependencies = [
+ "dotenv-build",
+]
+
 [[package]]
 name = "equivalent"
 version = "1.0.1"
@@ -2172,6 +2157,27 @@ version = "2.5.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
 
+[[package]]
+name = "event-listener"
+version = "5.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba"
+dependencies = [
+ "concurrent-queue",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "event-listener-strategy"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1"
+dependencies = [
+ "event-listener 5.3.1",
+ "pin-project-lite",
+]
+
 [[package]]
 name = "eventsource-stream"
 version = "0.2.3"
@@ -2396,16 +2402,6 @@ dependencies = [
  "percent-encoding",
 ]
 
-[[package]]
-name = "fs2"
-version = "0.4.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213"
-dependencies = [
- "libc",
- "winapi",
-]
-
 [[package]]
 name = "funty"
 version = "2.0.0"
@@ -2472,7 +2468,7 @@ checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f"
 dependencies = [
  "futures-core",
  "lock_api",
- "parking_lot 0.12.3",
+ "parking_lot",
 ]
 
 [[package]]
@@ -2730,9 +2726,9 @@ dependencies = [
 
 [[package]]
 name = "h2"
-version = "0.4.6"
+version = "0.4.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205"
+checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab"
 dependencies = [
  "atomic-waker",
  "bytes",
@@ -2816,12 +2812,6 @@ version = "0.3.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
 
-[[package]]
-name = "hermit-abi"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc"
-
 [[package]]
 name = "hex"
 version = "0.4.3"
@@ -2999,7 +2989,7 @@ dependencies = [
  "bytes",
  "futures-channel",
  "futures-util",
- "h2 0.4.6",
+ "h2 0.4.5",
  "http 1.1.0",
  "http-body 1.0.1",
  "httparse",
@@ -3037,7 +3027,7 @@ dependencies = [
  "http 1.1.0",
  "hyper 1.4.1",
  "hyper-util",
- "rustls 0.23.12",
+ "rustls 0.23.11",
  "rustls-pki-types",
  "tokio",
  "tokio-rustls 0.26.0",
@@ -3075,9 +3065,9 @@ dependencies = [
 
 [[package]]
 name = "hyper-util"
-version = "0.1.7"
+version = "0.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cde7055719c54e36e95e8719f95883f22072a48ede39db7fc17a4e1d5281e9b9"
+checksum = "3ab92f4f49ee4fb4f997c784b7a2e0fa70050211e0b6a287f898c3c9785ca956"
 dependencies = [
  "bytes",
  "futures-channel",
@@ -3305,6 +3295,7 @@ dependencies = [
  "dependent-models",
  "enum_meta",
  "enums",
+ "env-utils",
  "fitness-models",
  "fitness-service",
  "flate2",
@@ -3367,15 +3358,6 @@ dependencies = [
  "syn 2.0.77",
 ]
 
-[[package]]
-name = "instant"
-version = "0.1.13"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
-dependencies = [
- "cfg-if",
-]
-
 [[package]]
 name = "integration-service"
 version = "0.1.0"
@@ -3383,6 +3365,7 @@ dependencies = [
  "anyhow",
  "application-utils",
  "async-graphql",
+ "common-models",
  "common-utils",
  "database-models",
  "database-utils",
@@ -3414,11 +3397,11 @@ checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
 
 [[package]]
 name = "is-terminal"
-version = "0.4.13"
+version = "0.4.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b"
+checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b"
 dependencies = [
- "hermit-abi 0.4.0",
+ "hermit-abi",
  "libc",
  "windows-sys 0.52.0",
 ]
@@ -3431,9 +3414,9 @@ checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45"
 
 [[package]]
 name = "is_terminal_polyfill"
-version = "1.70.1"
+version = "1.70.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
+checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800"
 
 [[package]]
 name = "isolang"
@@ -3470,9 +3453,9 @@ checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
 
 [[package]]
 name = "js-sys"
-version = "0.3.70"
+version = "0.3.69"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a"
+checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
 dependencies = [
  "wasm-bindgen",
 ]
@@ -3538,9 +3521,9 @@ dependencies = [
 
 [[package]]
 name = "libc"
-version = "0.2.158"
+version = "0.2.155"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439"
+checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
 
 [[package]]
 name = "libm"
@@ -3609,9 +3592,9 @@ dependencies = [
 
 [[package]]
 name = "lru"
-version = "0.12.4"
+version = "0.12.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "37ee39891760e7d94734f6f63fedc29a2e4a152f836120753a72503f09fcf904"
+checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc"
 dependencies = [
  "hashbrown 0.14.5",
 ]
@@ -3771,11 +3754,11 @@ dependencies = [
 
 [[package]]
 name = "mio"
-version = "1.0.2"
+version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec"
+checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4"
 dependencies = [
- "hermit-abi 0.3.9",
+ "hermit-abi",
  "libc",
  "wasi",
  "windows-sys 0.52.0",
@@ -3801,7 +3784,6 @@ dependencies = [
  "application-utils",
  "async-graphql",
  "background",
- "cached",
  "chrono",
  "chrono-tz",
  "common-models",
@@ -3810,7 +3792,9 @@ dependencies = [
  "database-models",
  "database-utils",
  "dependent-models",
+ "derive_more 1.0.0",
  "enums",
+ "env-utils",
  "file-storage-service",
  "futures",
  "integration-service",
@@ -3818,6 +3802,7 @@ dependencies = [
  "markdown",
  "media-models",
  "migrations",
+ "moka",
  "nanoid",
  "notification-service",
  "providers",
@@ -3835,6 +3820,30 @@ dependencies = [
  "uuid",
 ]
 
+[[package]]
+name = "moka"
+version = "0.12.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32cf62eb4dd975d2dde76432fb1075c49e3ee2331cf36f1f8fd4b66550d32b6f"
+dependencies = [
+ "async-lock",
+ "async-trait",
+ "crossbeam-channel",
+ "crossbeam-epoch",
+ "crossbeam-utils",
+ "event-listener 5.3.1",
+ "futures-util",
+ "once_cell",
+ "parking_lot",
+ "quanta",
+ "rustc_version",
+ "smallvec",
+ "tagptr",
+ "thiserror",
+ "triomphe",
+ "uuid",
+]
+
 [[package]]
 name = "multer"
 version = "3.1.0"
@@ -3996,9 +4005,9 @@ dependencies = [
 
 [[package]]
 name = "object"
-version = "0.36.3"
+version = "0.36.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9"
+checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce"
 dependencies = [
  "memchr",
 ]
@@ -4198,17 +4207,6 @@ version = "2.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae"
 
-[[package]]
-name = "parking_lot"
-version = "0.11.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
-dependencies = [
- "instant",
- "lock_api",
- "parking_lot_core 0.8.6",
-]
-
 [[package]]
 name = "parking_lot"
 version = "0.12.3"
@@ -4216,21 +4214,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
 dependencies = [
  "lock_api",
- "parking_lot_core 0.9.10",
-]
-
-[[package]]
-name = "parking_lot_core"
-version = "0.8.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc"
-dependencies = [
- "cfg-if",
- "instant",
- "libc",
- "redox_syscall 0.2.16",
- "smallvec",
- "winapi",
+ "parking_lot_core",
 ]
 
 [[package]]
@@ -4499,12 +4483,9 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
 
 [[package]]
 name = "ppv-lite86"
-version = "0.2.20"
+version = "0.2.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
-dependencies = [
- "zerocopy",
-]
+checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
 
 [[package]]
 name = "precomputed-hash"
@@ -4644,6 +4625,21 @@ dependencies = [
  "syn 1.0.109",
 ]
 
+[[package]]
+name = "quanta"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e5167a477619228a0b284fac2674e3c388cba90631d7b7de620e6f1fcd08da5"
+dependencies = [
+ "crossbeam-utils",
+ "libc",
+ "once_cell",
+ "raw-cpuid",
+ "wasi",
+ "web-sys",
+ "winapi",
+]
+
 [[package]]
 name = "quote"
 version = "1.0.37"
@@ -4741,6 +4737,15 @@ dependencies = [
  "zerocopy",
 ]
 
+[[package]]
+name = "raw-cpuid"
+version = "11.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb9ee317cfe3fbd54b36a511efc1edd42e216903c9cd575e686dd68a2ba90d8d"
+dependencies = [
+ "bitflags 2.6.0",
+]
+
 [[package]]
 name = "redis"
 version = "0.25.4"
@@ -4764,15 +4769,6 @@ dependencies = [
  "url",
 ]
 
-[[package]]
-name = "redox_syscall"
-version = "0.2.16"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
-dependencies = [
- "bitflags 1.3.2",
-]
-
 [[package]]
 name = "redox_syscall"
 version = "0.4.1"
@@ -4793,9 +4789,9 @@ dependencies = [
 
 [[package]]
 name = "redox_users"
-version = "0.4.6"
+version = "0.4.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
+checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891"
 dependencies = [
  "getrandom",
  "libredox",
@@ -4918,7 +4914,7 @@ dependencies = [
  "futures-channel",
  "futures-core",
  "futures-util",
- "h2 0.4.6",
+ "h2 0.4.5",
  "http 1.1.0",
  "http-body 1.0.1",
  "http-body-util",
@@ -4935,12 +4931,12 @@ dependencies = [
  "once_cell",
  "percent-encoding",
  "pin-project-lite",
- "rustls-pemfile 2.1.3",
+ "rustls-pemfile 2.1.2",
  "serde",
  "serde_json",
  "serde_urlencoded",
  "sync_wrapper 1.0.1",
- "system-configuration 0.6.0",
+ "system-configuration 0.6.1",
  "tokio",
  "tokio-native-tls",
  "tokio-util",
@@ -4991,9 +4987,9 @@ dependencies = [
 
 [[package]]
 name = "rkyv"
-version = "0.7.45"
+version = "0.7.44"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b"
+checksum = "5cba464629b3394fc4dbc6f940ff8f5b4ff5c7aef40f29166fd4ad12acbc99c0"
 dependencies = [
  "bitvec",
  "bytecheck",
@@ -5009,37 +5005,15 @@ dependencies = [
 
 [[package]]
 name = "rkyv_derive"
-version = "0.7.45"
+version = "0.7.44"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0"
+checksum = "a7dddfff8de25e6f62b9d64e6e432bf1c6736c57d20323e15ee10435fbda7c65"
 dependencies = [
  "proc-macro2",
  "quote",
  "syn 1.0.109",
 ]
 
-[[package]]
-name = "rmp"
-version = "0.8.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4"
-dependencies = [
- "byteorder",
- "num-traits",
- "paste",
-]
-
-[[package]]
-name = "rmp-serde"
-version = "1.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db"
-dependencies = [
- "byteorder",
- "rmp",
- "serde",
-]
-
 [[package]]
 name = "router-resolver"
 version = "0.1.0"
@@ -5155,9 +5129,9 @@ dependencies = [
 
 [[package]]
 name = "rustls"
-version = "0.23.12"
+version = "0.23.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044"
+checksum = "4828ea528154ae444e5a642dbb7d5623354030dc9822b83fd9bb79683c7399d0"
 dependencies = [
  "once_cell",
  "rustls-pki-types",
@@ -5189,9 +5163,9 @@ dependencies = [
 
 [[package]]
 name = "rustls-pemfile"
-version = "2.1.3"
+version = "2.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425"
+checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d"
 dependencies = [
  "base64 0.22.1",
  "rustls-pki-types",
@@ -5199,9 +5173,9 @@ dependencies = [
 
 [[package]]
 name = "rustls-pki-types"
-version = "1.8.0"
+version = "1.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0"
+checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d"
 
 [[package]]
 name = "rustls-webpki"
@@ -5774,12 +5748,6 @@ dependencies = [
  "lazy_static",
 ]
 
-[[package]]
-name = "shlex"
-version = "1.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
-
 [[package]]
 name = "signal-hook-registry"
 version = "1.4.2"
@@ -5830,22 +5798,6 @@ dependencies = [
  "autocfg",
 ]
 
-[[package]]
-name = "sled"
-version = "0.34.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935"
-dependencies = [
- "crc32fast",
- "crossbeam-epoch",
- "crossbeam-utils",
- "fs2",
- "fxhash",
- "libc",
- "log",
- "parking_lot 0.11.2",
-]
-
 [[package]]
 name = "slug"
 version = "0.1.5"
@@ -5961,7 +5913,7 @@ dependencies = [
  "crc",
  "crossbeam-queue",
  "either",
- "event-listener",
+ "event-listener 2.5.3",
  "futures-channel",
  "futures-core",
  "futures-intrusive",
@@ -6164,9 +6116,9 @@ dependencies = [
 
 [[package]]
 name = "starbase_styles"
-version = "0.4.2"
+version = "0.4.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "44854a14e28e3b1d602d802576162380504df73efae50d4b901934d25579524b"
+checksum = "21a18837e83f7754c667fddb2c954ce27a068f9d85053a88d41db4a81435b5fe"
 dependencies = [
  "dirs",
  "owo-colors",
@@ -6217,7 +6169,7 @@ checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b"
 dependencies = [
  "new_debug_unreachable",
  "once_cell",
- "parking_lot 0.12.3",
+ "parking_lot",
  "phf_shared 0.10.0",
  "precomputed-hash",
  "serde",
@@ -6294,9 +6246,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
 
 [[package]]
 name = "supports-color"
-version = "3.0.0"
+version = "3.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9829b314621dfc575df4e409e79f9d6a66a3bd707ab73f23cb4aa3a854ac854f"
+checksum = "8775305acf21c96926c900ad056abeef436701108518cf890020387236ac5a77"
 dependencies = [
  "is_ci",
 ]
@@ -6374,9 +6326,9 @@ dependencies = [
 
 [[package]]
 name = "system-configuration"
-version = "0.6.0"
+version = "0.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "658bc6ee10a9b4fcf576e9b0819d95ec16f4d2c02d39fd83ac1c8789785c4a42"
+checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
 dependencies = [
  "bitflags 2.6.0",
  "core-foundation",
@@ -6403,6 +6355,12 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "tagptr"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
+
 [[package]]
 name = "tap"
 version = "1.0.1"
@@ -6411,15 +6369,14 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
 
 [[package]]
 name = "tempfile"
-version = "3.12.0"
+version = "3.10.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64"
+checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1"
 dependencies = [
  "cfg-if",
  "fastrand",
- "once_cell",
  "rustix",
- "windows-sys 0.59.0",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
@@ -6540,7 +6497,7 @@ dependencies = [
  "bytes",
  "libc",
  "mio",
- "parking_lot 0.12.3",
+ "parking_lot",
  "pin-project-lite",
  "signal-hook-registry",
  "socket2",
@@ -6596,7 +6553,7 @@ version = "0.26.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
 dependencies = [
- "rustls 0.23.12",
+ "rustls 0.23.11",
  "rustls-pki-types",
  "tokio",
 ]
@@ -6720,15 +6677,15 @@ dependencies = [
 
 [[package]]
 name = "tower-layer"
-version = "0.3.3"
+version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
+checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0"
 
 [[package]]
 name = "tower-service"
-version = "0.3.3"
+version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
+checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
 
 [[package]]
 name = "tracing"
@@ -6814,13 +6771,21 @@ dependencies = [
  "async-trait",
  "common-models",
  "common-utils",
+ "config",
  "database-models",
+ "database-utils",
  "dependent-models",
  "media-models",
  "sea-orm",
  "tracing",
 ]
 
+[[package]]
+name = "triomphe"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "859eb650cfee7434994602c3a68b25d77ad9e68c8a6cd491616ef86661382eb3"
+
 [[package]]
 name = "try-lock"
 version = "0.2.5"
@@ -6907,9 +6872,9 @@ dependencies = [
 
 [[package]]
 name = "unicode-properties"
-version = "0.1.2"
+version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "52ea75f83c0137a9b98608359a5f1af8144876eb67bcb1ce837368e906a9f524"
+checksum = "e4259d9d4425d9f0661581b804cb85fe66a4c631cadd8f490d1c13a35d5d9291"
 
 [[package]]
 name = "unicode-segmentation"
@@ -6929,6 +6894,18 @@ version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
 
+[[package]]
+name = "unkey"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0e8f3f2f069033aa3fa93650025c02296e830dda5f5f0631004005fef436d49"
+dependencies = [
+ "lazy_static",
+ "reqwest 0.11.27",
+ "serde",
+ "serde_json",
+]
+
 [[package]]
 name = "unreachable"
 version = "1.0.0"
@@ -7075,9 +7052,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
 
 [[package]]
 name = "version_check"
-version = "0.9.5"
+version = "0.9.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
 
 [[package]]
 name = "void"
@@ -7114,20 +7091,19 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
 
 [[package]]
 name = "wasm-bindgen"
-version = "0.2.93"
+version = "0.2.92"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5"
+checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
 dependencies = [
  "cfg-if",
- "once_cell",
  "wasm-bindgen-macro",
 ]
 
 [[package]]
 name = "wasm-bindgen-backend"
-version = "0.2.93"
+version = "0.2.92"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b"
+checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
 dependencies = [
  "bumpalo",
  "log",
@@ -7140,9 +7116,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-futures"
-version = "0.4.43"
+version = "0.4.42"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed"
+checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0"
 dependencies = [
  "cfg-if",
  "js-sys",
@@ -7152,9 +7128,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-macro"
-version = "0.2.93"
+version = "0.2.92"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf"
+checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
 dependencies = [
  "quote",
  "wasm-bindgen-macro-support",
@@ -7162,9 +7138,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-macro-support"
-version = "0.2.93"
+version = "0.2.92"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836"
+checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -7175,9 +7151,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-shared"
-version = "0.2.93"
+version = "0.2.92"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484"
+checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
 
 [[package]]
 name = "wasm-streams"
@@ -7194,9 +7170,9 @@ dependencies = [
 
 [[package]]
 name = "web-sys"
-version = "0.3.70"
+version = "0.3.69"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0"
+checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef"
 dependencies = [
  "js-sys",
  "wasm-bindgen",
@@ -7317,15 +7293,6 @@ dependencies = [
  "windows-targets 0.52.6",
 ]
 
-[[package]]
-name = "windows-sys"
-version = "0.59.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
-dependencies = [
- "windows-targets 0.52.6",
-]
-
 [[package]]
 name = "windows-targets"
 version = "0.48.5"
@@ -7498,9 +7465,9 @@ dependencies = [
 
 [[package]]
 name = "xml-rs"
-version = "0.8.21"
+version = "0.8.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "539a77ee7c0de333dcc6da69b177380a0b81e0dacfa4f7344c465a36871ee601"
+checksum = "791978798f0597cfc70478424c2b4fdc2b7a8024aaff78497ef00f24ef674193"
 
 [[package]]
 name = "xmlparser"
@@ -7538,7 +7505,6 @@ version = "0.7.35"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
 dependencies = [
- "byteorder",
  "zerocopy-derive",
 ]
 
diff --git a/Cargo.toml b/Cargo.toml
index dea52bbec0..1acd1463a9 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -38,6 +38,7 @@ members = [
     "crates/utils/application",
     "crates/utils/common",
     "crates/utils/database",
+    "crates/utils/env",
 ]
 resolver = "2"
 
@@ -59,7 +60,11 @@ chrono = "=0.4.38"
 chrono-tz = "=0.9.0"
 const-str = "=0.5.7"
 convert_case = "=0.6.0"
-dotenvy_macro = "=0.15.7"
+derive_more = { version = "=1.0.0", features = [
+    "add",
+    "add_assign",
+    "sum",
+], default-features = false }
 enum_meta = "=0.7.0"
 itertools = "=0.13.0"
 futures = "=0.3.30"
diff --git a/Cross.toml b/Cross.toml
index 71d3ad73c1..4e0386df00 100644
--- a/Cross.toml
+++ b/Cross.toml
@@ -3,6 +3,8 @@ passthrough = [
     "APP_VERSION",
     "DEFAULT_TMDB_ACCESS_TOKEN",
     "DEFAULT_MAL_CLIENT_ID",
+    "TRAKT_CLIENT_ID",
+    "UNKEY_API_ID",
 ]
 
 [build]
diff --git a/README.md b/README.md
index 919389be75..0914b24422 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,5 @@
 <h1 align="center">Ryot</h1>
 
-<img alt="Project logo" align="left" src="docs/logo.png" width="150">
-
 <h3 align="center">
   A self hosted platform for tracking various facets of your life - media, fitness etc.
 </h3>
diff --git a/apps/backend/Cargo.toml b/apps/backend/Cargo.toml
index f56217d0cd..b786679b1b 100644
--- a/apps/backend/Cargo.toml
+++ b/apps/backend/Cargo.toml
@@ -23,6 +23,7 @@ config = { path = "../../crates/config" }
 database-models = { path = "../../crates/models/database" }
 dependent-models = { path = "../../crates/models/dependent" }
 dotenvy = "=0.15.7"
+env-utils = { path = "../../crates/utils/env" }
 exporter-resolver = { path = "../../crates/resolvers/exporter" }
 exporter-service = { path = "../../crates/services/exporter" }
 file-storage-resolver = { path = "../../crates/resolvers/file-storage" }
@@ -41,16 +42,19 @@ miscellaneous-service = { path = "../../crates/services/miscellaneous" }
 openidconnect = { workspace = true }
 reqwest = { workspace = true }
 router-resolver = { path = "../../crates/resolvers/router" }
-schematic = { workspace = true }
-statistics-resolver = { path = "../../crates/resolvers/statistics" }
-statistics-service = { path = "../../crates/services/statistics" }
 sea-orm = { workspace = true }
 sea-orm-migration = { workspace = true }
+serde = { workspace = true }
 serde_json = { workspace = true }
+serde_with = { workspace = true }
+schematic = { workspace = true }
+statistics-resolver = { path = "../../crates/resolvers/statistics" }
+statistics-service = { path = "../../crates/services/statistics" }
 tokio = { workspace = true }
 tower = { version = "=0.4.13", features = ["buffer"] }
 tower-http = { version = "=0.5.2", features = ["catch-panic", "cors", "trace"] }
 tracing = { workspace = true }
 tracing-subscriber = "=0.3.18"
+unkey = "=0.4.0"
 user-resolver = { path = "../../crates/resolvers/user" }
 user-service = { path = "../../crates/services/user" }
diff --git a/apps/backend/src/common.rs b/apps/backend/src/common.rs
index 7aa2389691..a194ef9aec 100644
--- a/apps/backend/src/common.rs
+++ b/apps/backend/src/common.rs
@@ -53,6 +53,7 @@ pub struct AppServices {
 
 #[allow(clippy::too_many_arguments)]
 pub async fn create_app_services(
+    is_pro: bool,
     db: DatabaseConnection,
     s3_client: aws_sdk_s3::Client,
     config: Arc<config::AppConfig>,
@@ -66,6 +67,7 @@ pub async fn create_app_services(
         config.file_storage.s3_bucket_name.clone(),
     ));
     let exercise_service = Arc::new(ExerciseService::new(
+        is_pro,
         &db,
         config.clone(),
         file_storage_service.clone(),
@@ -81,6 +83,7 @@ pub async fn create_app_services(
     ));
     let miscellaneous_service = Arc::new(
         MiscellaneousService::new(
+            is_pro,
             oidc_client.is_some(),
             &db,
             timezone.clone(),
@@ -92,6 +95,7 @@ pub async fn create_app_services(
         .await,
     );
     let user_service = Arc::new(UserService::new(
+        is_pro,
         &db,
         config.clone(),
         perform_application_job,
@@ -126,6 +130,8 @@ pub async fn create_app_services(
     .data(statistics_service.clone())
     .data(collection_service.clone())
     .data(user_service.clone())
+    .data(config.clone())
+    .data(db.clone())
     .finish();
 
     let cors_origins = config
diff --git a/apps/backend/src/main.rs b/apps/backend/src/main.rs
index e86d494441..d4b0e18762 100644
--- a/apps/backend/src/main.rs
+++ b/apps/backend/src/main.rs
@@ -18,14 +18,16 @@ use apalis::{
 };
 use aws_sdk_s3::config::Region;
 use background::ApplicationJob;
-use chrono::{TimeZone, Utc};
-use common_utils::{ryot_log, COMPILATION_TIMESTAMP, PROJECT_NAME, TEMP_DIR, VERSION};
+use chrono::{DateTime, NaiveDate, TimeZone, Utc};
+use common_utils::{convert_naive_to_utc, ryot_log, COMPILATION_TIMESTAMP, PROJECT_NAME, TEMP_DIR};
 use database_models::prelude::Exercise;
-use dependent_models::CompleteExport;
+use env_utils::{APP_VERSION, UNKEY_API_ID};
 use logs_wheel::LogFileInitializer;
 use migrations::Migrator;
 use sea_orm::{ConnectionTrait, Database, DatabaseConnection, EntityTrait, PaginatorTrait};
 use sea_orm_migration::MigratorTrait;
+use serde::{Deserialize, Serialize};
+use serde_with::skip_serializing_none;
 use tokio::{
     join,
     net::TcpListener,
@@ -33,6 +35,7 @@ use tokio::{
 };
 use tower::buffer::BufferLayer;
 use tracing_subscriber::{fmt, layer::SubscriberExt};
+use unkey::{models::VerifyKeyRequest, Client};
 
 use crate::{
     common::create_app_services,
@@ -57,7 +60,7 @@ async fn main() -> Result<()> {
     }
     init_tracing()?;
 
-    ryot_log!(info, "Running version: {}", VERSION);
+    ryot_log!(info, "Running version: {}", APP_VERSION);
 
     let config = Arc::new(config::load_app_config()?);
     if config.server.sleep_before_startup_seconds > 0 {
@@ -75,6 +78,9 @@ async fn main() -> Result<()> {
 
     let config_dump_path = PathBuf::new().join(TEMP_DIR).join("config.json");
     fs::write(config_dump_path, serde_json::to_string_pretty(&config)?)?;
+    let is_pro = get_is_pro(&config.server.pro_key, &compile_timestamp).await;
+
+    ryot_log!(debug, "Is pro: {:#?}", is_pro);
 
     let mut aws_conf = aws_sdk_s3::Config::builder()
         .region(Region::new(config.file_storage.s3_region.clone()))
@@ -119,6 +125,7 @@ async fn main() -> Result<()> {
     ryot_log!(info, "Timezone: {}", tz);
 
     let app_services = create_app_services(
+        is_pro,
         db.clone(),
         s3_client,
         config,
@@ -145,6 +152,7 @@ async fn main() -> Result<()> {
     }
 
     if cfg!(debug_assertions) {
+        use dependent_models::CompleteExport;
         use schematic::schema::{SchemaGenerator, TypeScriptRenderer, YamlTemplateRenderer};
 
         // TODO: Once https://github.com/rust-lang/cargo/issues/3946 is resolved
@@ -293,6 +301,50 @@ fn init_tracing() -> Result<()> {
     Ok(())
 }
 
+async fn get_is_pro(pro_key: &str, compilation_time: &DateTime<Utc>) -> bool {
+    if pro_key.is_empty() {
+        return false;
+    }
+
+    ryot_log!(debug, "Verifying pro key for API ID: {:#?}", UNKEY_API_ID);
+
+    #[skip_serializing_none]
+    #[derive(Debug, Serialize, Clone, Deserialize)]
+    struct Meta {
+        expiry: Option<NaiveDate>,
+    }
+
+    let unkey_client = Client::new("public");
+    let verify_request = VerifyKeyRequest::new(pro_key, UNKEY_API_ID);
+    let validated_key = match unkey_client.verify_key(verify_request).await {
+        Ok(verify_response) => {
+            if !verify_response.valid {
+                ryot_log!(debug, "Pro key is no longer valid.");
+                return false;
+            }
+            verify_response
+        }
+        Err(verify_error) => {
+            ryot_log!(debug, "Pro key verification error: {:?}", verify_error);
+            return false;
+        }
+    };
+    let key_meta = validated_key
+        .meta
+        .map(|meta| serde_json::from_value::<Meta>(meta).unwrap());
+    ryot_log!(debug, "Expiry: {:?}", key_meta.clone().map(|m| m.expiry));
+    if let Some(meta) = key_meta {
+        if let Some(expiry) = meta.expiry {
+            if compilation_time > &convert_naive_to_utc(expiry) {
+                ryot_log!(warn, "Pro key has expired. Please renew your subscription.");
+                return false;
+            }
+        }
+    }
+    ryot_log!(info, "Pro key verified successfully");
+    true
+}
+
 // upgrade from v6 ONLY IF APPLICABLE
 async fn migrate_from_v6(db: &DatabaseConnection) -> Result<()> {
     db.execute_unprepared(
@@ -326,6 +378,7 @@ BEGIN
                 ('m20230504_create_collection', 1684693316),
                 ('m20230822_create_exercise', 1684693316),
                 ('m20230819_create_workout', 1684693316),
+                ('m20230818_create_workout_template', 1684693316),
                 ('m20230505_create_review', 1684693316),
                 ('m20230509_create_import_report', 1684693316),
                 ('m20230820_create_user_measurement', 1684693316),
@@ -337,7 +390,8 @@ BEGIN
                 ('m20240531_create_queued_notification', 1717207621),
                 ('m20240607_create_integration', 1723854703),
                 ('m20240712_create_notification_platform', 1723854703),
-                ('m20240713_create_user_summary', 1723854703);
+                ('m20240713_create_user_summary', 1723854703),
+                ('m20240714_create_access_link', 1723854703);
         END IF;
     END IF;
 END $$;
diff --git a/apps/frontend/app/components/common.tsx b/apps/frontend/app/components/common.tsx
index 46f623abd3..09fb73314c 100644
--- a/apps/frontend/app/components/common.tsx
+++ b/apps/frontend/app/components/common.tsx
@@ -68,6 +68,7 @@ import type { DeepPartial } from "ts-essentials";
 import { match } from "ts-pattern";
 import { withQuery } from "ufo";
 import {
+	PRO_REQUIRED_MESSAGE,
 	ThreePointSmileyRating,
 	clientGqlService,
 	convertDecimalToThreePointSmiley,
@@ -89,7 +90,11 @@ import { useReviewEntity } from "~/lib/state/media";
 import type { action } from "~/routes/actions";
 import classes from "~/styles/common.module.css";
 import { confirmWrapper } from "./confirmation";
-import { ExerciseDisplayItem, WorkoutDisplayItem } from "./fitness";
+import {
+	ExerciseDisplayItem,
+	WorkoutDisplayItem,
+	WorkoutTemplateDisplayItem,
+} from "./fitness";
 import {
 	MetadataDisplayItem,
 	MetadataGroupDisplayItem,
@@ -290,12 +295,7 @@ export const ProRequiredAlert = (props: { tooltipLabel?: string }) => {
 	return !coreDetails.isPro ? (
 		<Alert>
 			<Tooltip label={props.tooltipLabel} disabled={!props.tooltipLabel}>
-				<Text size="xs">
-					<Anchor href={coreDetails.websiteUrl} target="_blank">
-						Ryot Pro
-					</Anchor>{" "}
-					required to use this feature
-				</Text>
+				<Text size="xs">{PRO_REQUIRED_MESSAGE}</Text>
 			</Tooltip>
 		</Alert>
 	) : null;
@@ -708,6 +708,12 @@ export const DisplayCollectionEntity = (props: {
 				rightLabel={changeCase(snakeCase(props.entityLot))}
 			/>
 		))
+		.with(EntityLot.WorkoutTemplate, () => (
+			<WorkoutTemplateDisplayItem
+				workoutTemplateId={props.entityId}
+				topRight={props.topRight}
+			/>
+		))
 		.run();
 
 export const DisplayCollection = (props: {
diff --git a/apps/frontend/app/components/fitness.tsx b/apps/frontend/app/components/fitness.tsx
index 0e17aeb5d3..0d1000e0a5 100644
--- a/apps/frontend/app/components/fitness.tsx
+++ b/apps/frontend/app/components/fitness.tsx
@@ -27,6 +27,7 @@ import {
 } from "@ryot/generated/graphql/backend/graphql";
 import { isNumber, startCase } from "@ryot/ts-utils";
 import {
+	IconArrowLeftToArc,
 	IconClock,
 	IconInfoCircle,
 	IconRotateClockwise,
@@ -40,12 +41,13 @@ import type { ReactNode } from "react";
 import { $path } from "remix-routes";
 import { match } from "ts-pattern";
 import { withFragment } from "ufo";
-import { dayjsLib, getSetColor } from "~/lib/generals";
+import { FitnessEntity, dayjsLib, getSetColor } from "~/lib/generals";
 import { useGetMantineColor, useUserUnitSystem } from "~/lib/hooks";
 import {
 	getExerciseDetailsQuery,
 	getUserExerciseDetailsQuery,
 	getWorkoutDetailsQuery,
+	getWorkoutTemplateDetailsQuery,
 } from "~/lib/state/fitness";
 import { BaseMediaDisplayItem } from "./media";
 
@@ -212,13 +214,23 @@ export const ExerciseHistory = (props: {
 	entityId: string;
 	exerciseIdx: number;
 	hideExerciseDetails?: boolean;
+	entityType: FitnessEntity;
+	onCopyButtonClick?: () => Promise<void>;
 	hideExtraDetailsButton?: boolean;
 }) => {
 	const unitSystem = useUserUnitSystem();
 	const [opened, { toggle }] = useDisclosure(false);
 	const [parent] = useAutoAnimate();
 	const { data: workoutDetails } = useQuery(
-		getWorkoutDetailsQuery(props.entityId),
+		// @ts-ignore: Too complicated to fix and it just works this way
+		match(props.entityType)
+			.with(FitnessEntity.Workouts, () =>
+				getWorkoutDetailsQuery(props.entityId),
+			)
+			.with(FitnessEntity.Templates, () =>
+				getWorkoutTemplateDetailsQuery(props.entityId),
+			)
+			.exhaustive(),
 	);
 	const exercise =
 		workoutDetails?.details.information.exercises[props.exerciseIdx];
@@ -285,6 +297,11 @@ export const ExerciseHistory = (props: {
 										<IconInfoCircle size={18} />
 									</ActionIcon>
 								) : null}
+								{props.onCopyButtonClick ? (
+									<ActionIcon onClick={props.onCopyButtonClick} size="sm">
+										<IconArrowLeftToArc size={16} />
+									</ActionIcon>
+								) : null}
 							</Group>
 							{props.hideExerciseDetails ? (
 								<Text c="dimmed" fz="sm">
@@ -453,3 +470,29 @@ export const WorkoutDisplayItem = (props: {
 		/>
 	);
 };
+
+export const WorkoutTemplateDisplayItem = (props: {
+	workoutTemplateId: string;
+	topRight?: ReactNode;
+}) => {
+	const {
+		data: workoutTemplateDetails,
+		isLoading: isWorkoutTemplateDetailsLoading,
+	} = useQuery(getWorkoutTemplateDetailsQuery(props.workoutTemplateId));
+
+	return (
+		<BaseMediaDisplayItem
+			name={workoutTemplateDetails?.details.name}
+			isLoading={isWorkoutTemplateDetailsLoading}
+			onImageClickBehavior={$path("/fitness/:entity/:id", {
+				id: props.workoutTemplateId,
+				entity: FitnessEntity.Templates,
+			})}
+			labels={{
+				left: dayjsLib(workoutTemplateDetails?.details.createdOn).format("l"),
+				right: "Template",
+			}}
+			imageOverlay={{ topRight: props.topRight }}
+		/>
+	);
+};
diff --git a/apps/frontend/app/lib/generals.ts b/apps/frontend/app/lib/generals.ts
index d240e95f41..2acc3f4283 100644
--- a/apps/frontend/app/lib/generals.ts
+++ b/apps/frontend/app/lib/generals.ts
@@ -63,6 +63,7 @@ export const redirectToQueryParam = "redirectTo";
 export const pageQueryParam = "page";
 export const AUTH_COOKIE_NAME = "Auth";
 export const toastKey = "Toast";
+export const PRO_REQUIRED_MESSAGE = "Ryot pro is required to use this feature";
 
 export const queryClient = new QueryClient({
 	defaultOptions: { queries: { staleTime: Number.POSITIVE_INFINITY } },
@@ -78,6 +79,11 @@ export const getDateFromTimeSpan = (timeSpan: TimeSpan) => {
 		.exhaustive();
 };
 
+export enum FitnessEntity {
+	Workouts = "workouts",
+	Templates = "templates",
+}
+
 export enum TimeSpan {
 	Last7Days = "Last 7 days",
 	Last30Days = "Last 30 days",
@@ -308,6 +314,9 @@ const mediaQueryKeys = createQueryKeys("media", {
 	personDetails: (personId: string) => ({
 		queryKey: ["personDetails", personId],
 	}),
+	genreImages: (genreId: string) => ({
+		queryKey: ["genreDetails", "images", genreId],
+	}),
 });
 
 const collectionQueryKeys = createQueryKeys("collections", {
@@ -317,6 +326,9 @@ const collectionQueryKeys = createQueryKeys("collections", {
 	details: (collectionId: string, take?: number) => ({
 		queryKey: ["collectionDetails", collectionId, take],
 	}),
+	images: (collectionId: string) => ({
+		queryKey: ["collectionDetails", "images", collectionId],
+	}),
 });
 
 const fitnessQueryKeys = createQueryKeys("fitness", {
@@ -332,6 +344,9 @@ const fitnessQueryKeys = createQueryKeys("fitness", {
 	exerciseParameters: () => ({
 		queryKey: ["exerciseParameters"],
 	}),
+	workoutTemplateDetails: (workoutTemplateId: string) => ({
+		queryKey: ["workoutTemplateDetails", workoutTemplateId],
+	}),
 });
 
 const miscellaneousQueryKeys = createQueryKeys("miscellaneous", {
diff --git a/apps/frontend/app/lib/state/fitness.ts b/apps/frontend/app/lib/state/fitness.ts
index c3368a0f9a..ed38ed241d 100644
--- a/apps/frontend/app/lib/state/fitness.ts
+++ b/apps/frontend/app/lib/state/fitness.ts
@@ -10,7 +10,9 @@ import {
 	type WorkoutDetailsQuery,
 	type WorkoutInformation,
 	type WorkoutSetStatistic,
+	WorkoutTemplateDetailsDocument,
 } from "@ryot/generated/graphql/backend/graphql";
+import { isString } from "@ryot/ts-utils";
 import { queryOptions } from "@tanstack/react-query";
 import type { Dayjs } from "dayjs";
 import { createDraft, finishDraft } from "immer";
@@ -28,11 +30,13 @@ import {
 	queryClient,
 	queryFactory,
 } from "~/lib/generals";
+import type { useCoreDetails } from "../hooks";
 
 export type ExerciseSet = {
 	statistic: WorkoutSetStatistic;
 	lot: SetLot;
 	confirmedAt: string | null;
+	note?: boolean | string | null;
 };
 
 type AlreadyDoneExerciseSet = Pick<ExerciseSet, "statistic">;
@@ -52,17 +56,22 @@ export type Exercise = {
 	images: Array<Media>;
 	supersetWith: Array<string>;
 	isShowDetailsOpen: boolean;
+	openedDetailsTab?: "images" | "history";
 };
 
 export type InProgressWorkout = {
+	updateWorkoutTemplateId?: string;
 	repeatedFrom?: string;
+	templateId?: string;
 	startTime: string;
 	endTime?: string;
 	name: string;
 	comment?: string;
+	defaultRestTimer?: number | null;
 	exercises: Array<Exercise>;
 	videos: Array<string>;
 	images: Array<string>;
+	highlightedSet?: { exerciseIdx: number; setIdx: number };
 };
 
 type CurrentWorkout = InProgressWorkout | null;
@@ -134,6 +143,16 @@ export const getWorkoutDetailsQuery = (workoutId: string) =>
 export const getWorkoutDetails = async (workoutId: string) =>
 	queryClient.ensureQueryData(getWorkoutDetailsQuery(workoutId));
 
+export const getWorkoutTemplateDetailsQuery = (workoutTemplateId: string) =>
+	queryOptions({
+		queryKey:
+			queryFactory.fitness.workoutTemplateDetails(workoutTemplateId).queryKey,
+		queryFn: () =>
+			clientGqlService
+				.request(WorkoutTemplateDetailsDocument, { workoutTemplateId })
+				.then((data) => data.workoutTemplateDetails),
+	});
+
 type TWorkoutDetails = WorkoutDetailsQuery["workoutDetails"];
 
 export const convertHistorySetToCurrentSet = (
@@ -151,18 +170,26 @@ export const convertHistorySetToCurrentSet = (
 export const duplicateOldWorkout = async (
 	workoutInformation: WorkoutInformation,
 	name: string,
+	coreDetails: ReturnType<typeof useCoreDetails>,
 	repeatedFromId?: string,
+	templateId?: string,
+	updateWorkoutTemplateId?: string,
+	defaultRestTimer?: number | null,
 ) => {
 	const inProgress = getDefaultWorkout();
 	inProgress.name = name;
 	inProgress.repeatedFrom = repeatedFromId;
+	inProgress.templateId = templateId;
+	inProgress.updateWorkoutTemplateId = updateWorkoutTemplateId;
 	inProgress.comment = workoutInformation.comment || undefined;
-	for (const [_exerciseIdx, ex] of workoutInformation.exercises.entries()) {
+	inProgress.defaultRestTimer = defaultRestTimer;
+	for (const [exerciseIdx, ex] of workoutInformation.exercises.entries()) {
 		const sets = ex.sets.map(convertHistorySetToCurrentSet);
 		const exerciseDetails = await getExerciseDetails(ex.name);
+		const defaultRestTime = defaultRestTimer || ex.restTime;
 		inProgress.exercises.push({
 			identifier: randomUUID(),
-			isShowDetailsOpen: false,
+			isShowDetailsOpen: exerciseIdx === 0,
 			exerciseDetails: { images: exerciseDetails.details.attributes.images },
 			images: [],
 			videos: [],
@@ -171,8 +198,15 @@ export const duplicateOldWorkout = async (
 			lot: ex.lot,
 			notes: ex.notes,
 			supersetWith: [],
-			restTimer: ex.restTime ? { duration: ex.restTime, enabled: true } : null,
+			restTimer: defaultRestTime
+				? { duration: defaultRestTime, enabled: true }
+				: null,
 			sets: sets,
+			openedDetailsTab: !coreDetails.isPro
+				? "images"
+				: (exerciseDetails.userDetails.history?.length || 0) > 0
+					? "history"
+					: "images",
 		});
 	}
 	for (const [idx, exercise] of workoutInformation.exercises.entries()) {
@@ -205,7 +239,7 @@ export const addExerciseToWorkout = async (
 		}
 		draft.exercises.push({
 			identifier: randomUUID(),
-			isShowDetailsOpen: false,
+			isShowDetailsOpen: true,
 			exerciseId: ex.name,
 			exerciseDetails: {
 				images: exerciseDetails.details.attributes.images,
@@ -218,6 +252,10 @@ export const addExerciseToWorkout = async (
 			notes: [],
 			images: [],
 			videos: [],
+			openedDetailsTab:
+				(exerciseDetails.userDetails.history?.length || 0) > 0
+					? "history"
+					: "images",
 		});
 	}
 	const finishedDraft = finishDraft(draft);
@@ -241,10 +279,13 @@ export const currentWorkoutToCreateWorkoutInput = (
 	const input: CreateUserWorkoutMutationVariables = {
 		input: {
 			endTime: new Date().toISOString(),
+			templateId: currentWorkout.templateId,
+			updateWorkoutTemplateId: currentWorkout.updateWorkoutTemplateId,
 			startTime: new Date(currentWorkout.startTime).toISOString(),
 			name: currentWorkout.name,
 			comment: currentWorkout.comment,
 			repeatedFrom: currentWorkout.repeatedFrom,
+			defaultRestTimer: currentWorkout.defaultRestTimer,
 			exercises: [],
 			assets: {
 				images: [...currentWorkout.images],
@@ -256,8 +297,10 @@ export const currentWorkoutToCreateWorkoutInput = (
 		const sets = Array<UserWorkoutSetRecord>();
 		for (const set of exercise.sets)
 			if (isCreatingTemplate || set.confirmedAt) {
+				const note = isString(set.note) ? set.note : undefined;
 				if (Object.keys(set.statistic).length === 0) continue;
 				sets.push({
+					note,
 					lot: set.lot,
 					confirmedAt: set.confirmedAt
 						? new Date(set.confirmedAt).toISOString()
diff --git a/apps/frontend/app/lib/utilities.server.ts b/apps/frontend/app/lib/utilities.server.ts
index 07cc71b048..4928aa69f7 100644
--- a/apps/frontend/app/lib/utilities.server.ts
+++ b/apps/frontend/app/lib/utilities.server.ts
@@ -366,9 +366,13 @@ export const getToast = async (request: Request) => {
 	};
 };
 
-export const getCookiesForApplication = async (token: string) => {
+export const getCookiesForApplication = async (
+	token: string,
+	tokenValidForDays?: number,
+) => {
 	const [{ coreDetails }] = await Promise.all([getCachedCoreDetails()]);
-	const maxAge = coreDetails.tokenValidForDays * 24 * 60 * 60;
+	const maxAge =
+		(tokenValidForDays || coreDetails.tokenValidForDays) * 24 * 60 * 60;
 	const options = { maxAge, path: "/" } satisfies CookieSerializeOptions;
 	return combineHeaders({
 		"set-cookie": serialize(AUTH_COOKIE_NAME, token, options),
diff --git a/apps/frontend/app/routes/_dashboard._index.tsx b/apps/frontend/app/routes/_dashboard._index.tsx
index 8c3e4b8312..c770445ad6 100644
--- a/apps/frontend/app/routes/_dashboard._index.tsx
+++ b/apps/frontend/app/routes/_dashboard._index.tsx
@@ -30,6 +30,7 @@ import {
 	LatestUserSummaryDocument,
 	MediaLot,
 	type UserPreferences,
+	UserRecommendationsDocument,
 	UserUpcomingCalendarEventsDocument,
 } from "@ryot/generated/graphql/backend/graphql";
 import {
@@ -54,11 +55,8 @@ import { $path } from "remix-routes";
 import invariant from "tiny-invariant";
 import { match } from "ts-pattern";
 import { useLocalStorage } from "usehooks-ts";
-import {
-	ApplicationGrid,
-	DisplayCollectionEntity,
-	ProRequiredAlert,
-} from "~/components/common";
+import { ApplicationGrid, ProRequiredAlert } from "~/components/common";
+import { DisplayCollectionEntity } from "~/components/common";
 import { displayWeightWithUnit } from "~/components/fitness";
 import { MetadataDisplayItem } from "~/components/media";
 import {
@@ -71,6 +69,7 @@ import {
 	queryFactory,
 } from "~/lib/generals";
 import {
+	useCoreDetails,
 	useDashboardLayoutData,
 	useUserPreferences,
 	useUserUnitSystem,
@@ -93,6 +92,20 @@ export const loader = unstable_defineLoader(async ({ request }) => {
 	const preferences = await getCachedUserPreferences(request);
 	const takeUpcoming = getTake(preferences, DashboardElementLot.Upcoming);
 	const takeInProgress = getTake(preferences, DashboardElementLot.InProgress);
+	const getRecommendations = async () => {
+		if (
+			preferences.general.dashboard.find(
+				(de) => de.section === DashboardElementLot.Recommendations,
+			)?.hidden
+		)
+			return [];
+		const { userRecommendations } = await serverGqlService.authenticatedRequest(
+			request,
+			UserRecommendationsDocument,
+			{},
+		);
+		return userRecommendations;
+	};
 	const userCollectionsList = await getCachedUserCollectionsList(request);
 	const foundInProgressCollection = userCollectionsList.find(
 		(c) => c.name === "In Progress",
@@ -100,6 +113,7 @@ export const loader = unstable_defineLoader(async ({ request }) => {
 	invariant(foundInProgressCollection);
 	const [
 		{ collectionContents: inProgressCollectionContents },
+		userRecommendations,
 		{ userUpcomingCalendarEvents },
 		{ latestUserSummary },
 	] = await Promise.all([
@@ -110,6 +124,7 @@ export const loader = unstable_defineLoader(async ({ request }) => {
 				sort: { order: GraphqlSortOrder.Desc },
 			},
 		}),
+		getRecommendations(),
 		serverGqlService.authenticatedRequest(
 			request,
 			UserUpcomingCalendarEventsDocument,
@@ -125,6 +140,7 @@ export const loader = unstable_defineLoader(async ({ request }) => {
 		latestUserSummary,
 		userUpcomingCalendarEvents,
 		inProgressCollectionContents,
+		userRecommendations,
 	};
 });
 
@@ -151,6 +167,7 @@ const MediaColors: EntityColor = {
 
 export default function Page() {
 	const loaderData = useLoaderData<typeof loader>();
+	const coreDetails = useCoreDetails();
 	const userPreferences = useUserPreferences();
 	const unitSystem = useUserUnitSystem();
 	const theme = useMantineTheme();
@@ -201,7 +218,15 @@ export default function Page() {
 						.with([DashboardElementLot.Recommendations, false], ([v, _]) => (
 							<Section key={v} lot={v}>
 								<Title>Recommendations</Title>
-								<ProRequiredAlert tooltipLabel="Get new recommendations every hour" />
+								{coreDetails.isPro ? (
+									<ApplicationGrid>
+										{loaderData.userRecommendations.map((lm) => (
+											<MetadataDisplayItem key={lm} metadataId={lm} />
+										))}
+									</ApplicationGrid>
+								) : (
+									<ProRequiredAlert tooltipLabel="Get new recommendations every hour" />
+								)}
 							</Section>
 						))
 						.with([DashboardElementLot.Activity, false], ([v, _]) => (
diff --git a/apps/frontend/app/routes/_dashboard.collections.list.tsx b/apps/frontend/app/routes/_dashboard.collections.list.tsx
index 39ee555cf4..e95428b7b6 100644
--- a/apps/frontend/app/routes/_dashboard.collections.list.tsx
+++ b/apps/frontend/app/routes/_dashboard.collections.list.tsx
@@ -1,22 +1,33 @@
+import { useAutoAnimate } from "@formkit/auto-animate/react";
 import {
 	ActionIcon,
 	Anchor,
 	Box,
 	Button,
+	Checkbox,
 	Container,
 	Flex,
 	Group,
 	Image,
 	Input,
 	Modal,
+	MultiSelect,
 	Paper,
+	Select,
 	Stack,
 	Text,
 	TextInput,
 	Textarea,
 	Title,
+	Tooltip,
 } from "@mantine/core";
-import { useDisclosure } from "@mantine/hooks";
+import {
+	useDidUpdate,
+	useDisclosure,
+	useHover,
+	useListState,
+} from "@mantine/hooks";
+import { notifications } from "@mantine/notifications";
 import { unstable_defineAction, unstable_defineLoader } from "@remix-run/node";
 import {
 	Form,
@@ -27,12 +38,30 @@ import {
 	useSearchParams,
 } from "@remix-run/react";
 import {
+	CollectionContentsDocument,
+	CollectionContentsSortBy,
+	type CollectionExtraInformation,
+	CollectionExtraInformationLot,
 	CreateOrUpdateCollectionDocument,
 	DeleteCollectionDocument,
+	EntityLot,
+	GraphqlSortOrder,
 	type UserCollectionsListQuery,
+	UsersListDocument,
 } from "@ryot/generated/graphql/backend/graphql";
-import { processSubmission, truncate } from "@ryot/ts-utils";
-import { IconEdit, IconPlus, IconTrashFilled } from "@tabler/icons-react";
+import {
+	changeCase,
+	isString,
+	processSubmission,
+	truncate,
+} from "@ryot/ts-utils";
+import {
+	IconEdit,
+	IconPlus,
+	IconTrash,
+	IconTrashFilled,
+} from "@tabler/icons-react";
+import { useQuery } from "@tanstack/react-query";
 import { ClientError } from "graphql-request";
 import { useEffect, useState } from "react";
 import { Virtuoso } from "react-virtuoso";
@@ -40,10 +69,20 @@ import { $path } from "remix-routes";
 import { namedAction } from "remix-utils/named-action";
 import { withQuery } from "ufo";
 import { z } from "zod";
+import { zx } from "zodix";
 import { DebouncedSearchInput, ProRequiredAlert } from "~/components/common";
 import { confirmWrapper } from "~/components/confirmation";
+import {
+	PRO_REQUIRED_MESSAGE,
+	clientGqlService,
+	dayjsLib,
+	getPartialMetadataDetailsQuery,
+	queryClient,
+	queryFactory,
+} from "~/lib/generals";
 import {
 	useConfirmSubmit,
+	useCoreDetails,
 	useFallbackImageUrl,
 	useUserCollections,
 	useUserDetails,
@@ -59,7 +98,10 @@ import {
 export const loader = unstable_defineLoader(async ({ request }) => {
 	const cookieName = await getEnhancedCookieName("collections.list", request);
 	await redirectUsingEnhancedCookieSearchParams(request, cookieName);
-	return { cookieName };
+	const [{ usersList }] = await Promise.all([
+		serverGqlService.authenticatedRequest(request, UsersListDocument, {}),
+	]);
+	return { usersList, cookieName };
 });
 
 export const meta = (_args: MetaArgs_SingleFetch<typeof loader>) => {
@@ -96,7 +138,7 @@ export const action = unstable_defineAction(async ({ request }) => {
 					if (err) message = err;
 				}
 				return Response.json(
-					{},
+					{ error: JSON.stringify(e) },
 					{
 						status: 400,
 						headers: await createToastHeaders({ type: "error", message }),
@@ -138,13 +180,30 @@ const createOrUpdateSchema = z.object({
 	name: z.string(),
 	description: z.string().optional(),
 	updateId: z.string().optional(),
+	collaborators: z
+		.string()
+		.optional()
+		.transform((v) => (v ? v.split(",") : undefined)),
+	informationTemplate: z
+		.array(
+			z.object({
+				name: z.string(),
+				description: z.string(),
+				lot: z.nativeEnum(CollectionExtraInformationLot),
+				defaultValue: z.string().optional(),
+				required: zx.CheckboxAsString.optional(),
+			}),
+		)
+		.optional(),
 });
 
 type UpdateCollectionInput = {
 	name: string;
 	id: string;
 	isDefault: boolean;
+	collaborators: Collection["collaborators"];
 	description?: string | null;
+	informationTemplate?: CollectionExtraInformation[] | null;
 };
 
 export default function Page() {
@@ -232,10 +291,49 @@ const DisplayCollection = (props: {
 	openModal: () => void;
 }) => {
 	const userDetails = useUserDetails();
+	const coreDetails = useCoreDetails();
 	const submit = useConfirmSubmit();
 	const fallbackImageUrl = useFallbackImageUrl(props.collection.name);
 	const additionalDisplay = [];
 
+	const { data: collectionImages } = useQuery({
+		queryKey: queryFactory.collections.images(props.collection.id).queryKey,
+		queryFn: async () => {
+			const { collectionContents } = await clientGqlService.request(
+				CollectionContentsDocument,
+				{
+					input: {
+						collectionId: props.collection.id,
+						take: 10,
+						sort: {
+							by: CollectionContentsSortBy.LastUpdatedOn,
+							order: GraphqlSortOrder.Desc,
+						},
+					},
+				},
+			);
+			const images = [];
+			for (const content of collectionContents.results.items) {
+				if (images.length === 5) break;
+				if (content.entityLot !== EntityLot.Metadata) continue;
+				const { image } = await queryClient.ensureQueryData(
+					getPartialMetadataDetailsQuery(content.entityId),
+				);
+				if (isString(image)) images.push(image);
+			}
+			return images;
+		},
+		staleTime: dayjsLib.duration(1, "hour").asMilliseconds(),
+	});
+
+	const [hoveredStates, setHoveredStates] = useListState<boolean>([]);
+
+	const setHoveredState = (index: number, state: boolean) => {
+		setHoveredStates.setItem(index, state);
+	};
+
+	const currentlyHovered = hoveredStates.findIndex((h) => h);
+
 	if (props.collection.creator.id !== userDetails.id)
 		additionalDisplay.push(`By ${props.collection.creator.name}`);
 	if (props.collection.count > 0)
@@ -245,6 +343,10 @@ const DisplayCollection = (props: {
 			`${props.collection.collaborators.length} collaborators`,
 		);
 
+	const FallBackImage = () => (
+		<Image src={fallbackImageUrl} h="100%" flex="none" mx="auto" radius="md" />
+	);
+
 	return (
 		<Paper
 			pr="md"
@@ -262,16 +364,32 @@ const DisplayCollection = (props: {
 					pos="relative"
 					style={{ overflow: "hidden" }}
 				>
-					<Image
-						src={fallbackImageUrl}
-						h="100%"
-						flex="none"
-						mx="auto"
-						radius="md"
-					/>
-					<Box pos="absolute" left={0} right={0} bottom={0}>
-						<ProRequiredAlert tooltipLabel="Collage image using collection contents" />
-					</Box>
+					{coreDetails.isPro ? (
+						collectionImages && collectionImages.length > 0 ? (
+							collectionImages.map((image, index) => {
+								const shouldCollapse = index < currentlyHovered;
+								return (
+									<CollectionImageDisplay
+										key={image}
+										image={image}
+										index={index}
+										shouldCollapse={shouldCollapse}
+										setHoveredState={setHoveredState}
+										totalImages={collectionImages.length}
+									/>
+								);
+							})
+						) : (
+							<FallBackImage />
+						)
+					) : (
+						<>
+							<FallBackImage />
+							<Box pos="absolute" left={0} right={0} bottom={0}>
+								<ProRequiredAlert tooltipLabel="Collage image using collection contents" />
+							</Box>
+						</>
+					)}
 				</Flex>
 				<Stack flex={1} py={{ md: "sm" }}>
 					<Group justify="space-between">
@@ -298,7 +416,9 @@ const DisplayCollection = (props: {
 											name: props.collection.name,
 											id: props.collection.id,
 											description: props.collection.description,
+											collaborators: props.collection.collaborators,
 											isDefault: props.collection.isDefault,
+											informationTemplate: props.collection.informationTemplate,
 										});
 										props.openModal();
 									}}
@@ -352,9 +472,54 @@ const DisplayCollection = (props: {
 	);
 };
 
+const CollectionImageDisplay = (props: {
+	image: string;
+	index: number;
+	totalImages: number;
+	shouldCollapse: boolean;
+	setHoveredState: (index: number, state: boolean) => void;
+}) => {
+	const { ref, hovered } = useHover();
+	const offset = IMAGES_CONTAINER_WIDTH / props.totalImages - 20;
+
+	useDidUpdate(() => {
+		props.setHoveredState(props.index, hovered);
+	}, [hovered]);
+
+	return (
+		<Box
+			h="100%"
+			w="120px"
+			ref={ref}
+			top={{ md: 0 }}
+			pos={{ md: "absolute" }}
+			left={{
+				md: props.index * offset - (props.shouldCollapse ? offset * 2 : 0),
+			}}
+			style={{
+				zIndex: props.totalImages - props.index,
+				transitionProperty: "left",
+				transitionDuration: "0.2s",
+				transitionTimingFunction: "ease-in-out",
+			}}
+		>
+			<Image src={props.image} h="100%" />
+		</Box>
+	);
+};
+
 const CreateOrUpdateModal = (props: {
 	toUpdateCollection: UpdateCollectionInput | undefined;
 }) => {
+	const loaderData = useLoaderData<typeof loader>();
+	const coreDetails = useCoreDetails();
+	const userDetails = useUserDetails();
+	const [parent] = useAutoAnimate();
+	const [informationTemplate, setInformationTemplate] =
+		useListState<CollectionExtraInformation>(
+			props.toUpdateCollection?.informationTemplate || [],
+		);
+
 	return (
 		<Box
 			method="POST"
@@ -389,14 +554,108 @@ const CreateOrUpdateModal = (props: {
 					}
 					autosize
 				/>
+				<Tooltip label={PRO_REQUIRED_MESSAGE} disabled={coreDetails.isPro}>
+					<MultiSelect
+						name="collaborators"
+						description="Add collaborators to this collection"
+						searchable
+						defaultValue={(props.toUpdateCollection?.collaborators || []).map(
+							(c) => c.id,
+						)}
+						data={loaderData.usersList.map((u) => ({
+							value: u.id,
+							label: u.name,
+							disabled: u.id === userDetails.id,
+						}))}
+						disabled={!coreDetails.isPro}
+					/>
+				</Tooltip>
 				<Input.Wrapper
-					label="Collaborators"
-					description={<ProRequiredAlert />}
-				/>
-				<Input.Wrapper
-					label="Information template"
-					description={<ProRequiredAlert />}
-				/>
+					labelProps={{ w: "100%" }}
+					label={
+						<Group wrap="nowrap" justify="space-between">
+							<Input.Label size="xs">Information template</Input.Label>
+							<Anchor
+								size="xs"
+								onClick={() => {
+									if (!coreDetails.isPro) {
+										notifications.show({
+											color: "red",
+											message: PRO_REQUIRED_MESSAGE,
+										});
+										return;
+									}
+									setInformationTemplate.append({
+										name: "",
+										description: "",
+										lot: CollectionExtraInformationLot.String,
+									});
+								}}
+							>
+								Add field
+							</Anchor>
+						</Group>
+					}
+					description="Associate extra information when adding an entity to this collection"
+				>
+					<Stack gap="xs" mt="xs" ref={parent}>
+						{informationTemplate.map((field, index) => (
+							<Paper withBorder key={index.toString()} p="xs">
+								<TextInput
+									label="Name"
+									required
+									name={`informationTemplate[${index}].name`}
+									size="xs"
+									defaultValue={field.name}
+								/>
+								<Textarea
+									label="Description"
+									required
+									name={`informationTemplate[${index}].description`}
+									size="xs"
+									defaultValue={field.description}
+								/>
+								<Group wrap="nowrap">
+									<Select
+										label="Input type"
+										required
+										flex={1}
+										name={`informationTemplate[${index}].lot`}
+										data={Object.values(CollectionExtraInformationLot).map(
+											(lot) => ({ value: lot, label: changeCase(lot) }),
+										)}
+										size="xs"
+										defaultValue={field.lot}
+									/>
+									<TextInput
+										label="Default value"
+										flex={1}
+										name={`informationTemplate[${index}].defaultValue`}
+										size="xs"
+										defaultValue={field.defaultValue || undefined}
+									/>
+								</Group>
+								<Group mt="xs" justify="space-around">
+									<Checkbox
+										label="Required"
+										name={`informationTemplate[${index}].required`}
+										size="sm"
+										defaultChecked={field.required || undefined}
+									/>
+									<Button
+										size="xs"
+										variant="subtle"
+										color="red"
+										leftSection={<IconTrash />}
+										onClick={() => setInformationTemplate.remove(index)}
+									>
+										Remove field
+									</Button>
+								</Group>
+							</Paper>
+						))}
+					</Stack>
+				</Input.Wrapper>
 				<Button
 					variant="outline"
 					type="submit"
diff --git a/apps/frontend/app/routes/_dashboard.fitness.$action.tsx b/apps/frontend/app/routes/_dashboard.fitness.$action.tsx
index 99af9ceefd..39fe2f6f1f 100644
--- a/apps/frontend/app/routes/_dashboard.fitness.$action.tsx
+++ b/apps/frontend/app/routes/_dashboard.fitness.$action.tsx
@@ -1,6 +1,8 @@
 import { Buffer } from "buffer";
+import "@mantine/carousel/styles.css";
 import { useAutoAnimate } from "@formkit/auto-animate/react";
 import { DragDropContext, Draggable, Droppable } from "@hello-pangea/dnd";
+import { Carousel } from "@mantine/carousel";
 import {
 	ActionIcon,
 	Anchor,
@@ -49,6 +51,7 @@ import {
 } from "@remix-run/react";
 import type { MetaArgs_SingleFetch } from "@remix-run/react";
 import {
+	CreateOrUpdateWorkoutTemplateDocument,
 	CreateUserWorkoutDocument,
 	ExerciseLot,
 	SetLot,
@@ -68,6 +71,7 @@ import {
 	IconCameraRotate,
 	IconCheck,
 	IconClipboard,
+	IconClock,
 	IconDotsVertical,
 	IconDroplet,
 	IconDropletFilled,
@@ -79,11 +83,12 @@ import {
 	IconTrash,
 	IconZzz,
 } from "@tabler/icons-react";
+import { useQuery } from "@tanstack/react-query";
 import { Howl } from "howler";
 import { produce } from "immer";
 import { RESET } from "jotai/utils";
 import Cookies from "js-cookie";
-import { useRef } from "react";
+import { useRef, useState } from "react";
 import Webcam from "react-webcam";
 import { $path } from "remix-routes";
 import { ClientOnly } from "remix-utils/client-only";
@@ -97,12 +102,16 @@ import { zx } from "zodix";
 import { confirmWrapper } from "~/components/confirmation";
 import {
 	DisplaySetStatistics,
+	ExerciseHistory,
 	displayWeightWithUnit,
 } from "~/components/fitness";
 import {
 	CurrentWorkoutKey,
+	FitnessEntity,
+	PRO_REQUIRED_MESSAGE,
 	dayjsLib,
 	getSetColor,
+	getSurroundingElements,
 	queryClient,
 	queryFactory,
 } from "~/lib/generals";
@@ -115,8 +124,11 @@ import {
 } from "~/lib/hooks";
 import {
 	type InProgressWorkout,
+	convertHistorySetToCurrentSet,
 	currentWorkoutToCreateWorkoutInput,
 	exerciseHasDetailsToShow,
+	getUserExerciseDetailsQuery,
+	getWorkoutDetails,
 	useCurrentWorkout,
 	useGetExerciseAtIndex,
 	useGetSetAtIndex,
@@ -156,7 +168,9 @@ export const loader = unstable_defineLoader(async ({ params, request }) => {
 export const meta = ({ data }: MetaArgs_SingleFetch<typeof loader>) => {
 	return [
 		{
-			title: `${data?.action === Action.LogWorkout ? "Log Workout" : "Create Template"} | Ryot`,
+			title: `${
+				data?.action === Action.LogWorkout ? "Log Workout" : "Create Template"
+			} | Ryot`,
 		},
 	];
 };
@@ -179,6 +193,21 @@ export const action = unstable_defineAction(async ({ request }) => {
 				{ message: "Workout completed successfully", type: "success" },
 			);
 		},
+		createTemplate: async () => {
+			const { createOrUpdateWorkoutTemplate } =
+				await serverGqlService.authenticatedRequest(
+					request,
+					CreateOrUpdateWorkoutTemplateDocument,
+					workout,
+				);
+			return redirectWithToast(
+				$path("/fitness/:entity/:id", {
+					entity: "templates",
+					id: createOrUpdateWorkoutTemplate,
+				}),
+				{ message: "Template created successfully", type: "success" },
+			);
+		},
 	});
 });
 
@@ -264,20 +293,39 @@ export default function Page() {
 								key={currentWorkout.exercises.toString()}
 							/>
 							<Stack ref={parent}>
-								<TextInput
-									size="sm"
-									label="Name"
-									placeholder="A name for your workout"
-									value={currentWorkout.name}
-									required
-									onChange={(e) =>
-										setCurrentWorkout(
-											produce(currentWorkout, (draft) => {
-												draft.name = e.currentTarget.value;
-											}),
-										)
-									}
-								/>
+								<Group justify="space-between">
+									<TextInput
+										w={isCreatingTemplate ? "65%" : "100%"}
+										size="sm"
+										label="Name"
+										placeholder="A name for your workout"
+										value={currentWorkout.name}
+										required
+										onChange={(e) =>
+											setCurrentWorkout(
+												produce(currentWorkout, (draft) => {
+													draft.name = e.currentTarget.value;
+												}),
+											)
+										}
+									/>
+									{isCreatingTemplate ? (
+										<NumberInput
+											w="30%"
+											suffix="s"
+											label="Rest timer"
+											value={currentWorkout.defaultRestTimer || undefined}
+											onChange={(v) => {
+												if (v)
+													setCurrentWorkout(
+														produce(currentWorkout, (draft) => {
+															draft.defaultRestTimer = Number(v);
+														}),
+													);
+											}}
+										/>
+									) : undefined}
+								</Group>
 								<Textarea
 									size="sm"
 									minRows={2}
@@ -376,7 +424,9 @@ export default function Page() {
 													if (!currentWorkout.name) {
 														notifications.show({
 															color: "red",
-															message: `Please give a name to the ${isCreatingTemplate ? "template" : "workout"}`,
+															message: `Please give a name to the ${
+																isCreatingTemplate ? "template" : "workout"
+															}`,
 														});
 														return;
 													}
@@ -429,7 +479,9 @@ export default function Page() {
 										size="compact-sm"
 										onClick={async () => {
 											const yes = await confirmWrapper({
-												confirmation: `Are you sure you want to cancel this ${isCreatingTemplate ? "template" : "workout"}?`,
+												confirmation: `Are you sure you want to cancel this ${
+													isCreatingTemplate ? "template" : "workout"
+												}?`,
 											});
 											if (yes) {
 												for (const e of currentWorkout.exercises) {
@@ -597,10 +649,7 @@ const StatInput = (props: {
 
 const fileType = "image/jpeg";
 
-const ImageDisplay = (props: {
-	imageSrc: string;
-	removeImage: () => void;
-}) => {
+const ImageDisplay = (props: { imageSrc: string; removeImage: () => void }) => {
 	return (
 		<Box pos="relative">
 			<Avatar src={props.imageSrc} size="lg" />
@@ -713,6 +762,11 @@ const ExerciseDisplay = (props: {
 	const [currentTimer] = useTimerAtom();
 	const coreDetails = useCoreDetails();
 	const fileUploadAllowed = coreDetails.fileStorageEnabled;
+	const [detailsParent] = useAutoAnimate();
+	const { data: userExerciseDetails } = useQuery(
+		getUserExerciseDetailsQuery(exercise.exerciseId),
+	);
+	const [activeHistoryIdx, setActiveHistoryIdx] = useState(0);
 
 	const playAddSetSound = () => {
 		const sound = new Howl({ src: ["/add-set.mp3"] });
@@ -736,13 +790,13 @@ const ExerciseDisplay = (props: {
 		{ close: supersetModalClose, toggle: supersetModalToggle },
 	] = useDisclosure(false);
 
+	const exerciseHistory = userExerciseDetails?.history;
 	const [durationCol, distanceCol, weightCol, repsCol] = match(exercise.lot)
 		.with(ExerciseLot.DistanceAndDuration, () => [true, true, false, false])
 		.with(ExerciseLot.Duration, () => [true, false, false, false])
 		.with(ExerciseLot.RepsAndWeight, () => [false, false, true, true])
 		.with(ExerciseLot.Reps, () => [false, false, false, true])
 		.exhaustive();
-
 	const toBeDisplayedColumns =
 		[durationCol, distanceCol, weightCol, repsCol].filter(Boolean).length + 1;
 
@@ -905,6 +959,7 @@ const ExerciseDisplay = (props: {
 				</Stack>
 			</Modal>
 			<Paper
+				style={{ scrollMargin: 16 }}
 				id={props.exerciseIdx.toString()}
 				px={{ base: 4, md: "xs", lg: "sm" }}
 			>
@@ -1034,13 +1089,113 @@ const ExerciseDisplay = (props: {
 					</Menu>
 					<Box ref={parent}>
 						{exercise.isShowDetailsOpen ? (
-							<ScrollArea mb="md" type="scroll">
-								<Group wrap="nowrap">
-									{exercise.exerciseDetails.images.map((i) => (
-										<Image key={i} radius="md" src={i} h={200} w={350} />
-									))}
-								</Group>
-							</ScrollArea>
+							<Box mb="md" ref={detailsParent} pos="relative">
+								{match(exercise.openedDetailsTab)
+									.with("images", undefined, () => (
+										<ScrollArea type="scroll">
+											<Group wrap="nowrap">
+												{exercise.exerciseDetails.images.map((i) => (
+													<Image key={i} radius="md" src={i} h={200} w={350} />
+												))}
+											</Group>
+										</ScrollArea>
+									))
+									.with("history", () => (
+										<Carousel
+											align="start"
+											slideGap="md"
+											withControls={false}
+											style={{ userSelect: "none" }}
+											onSlideChange={setActiveHistoryIdx}
+											slideSize={{ base: "100%", md: "50%" }}
+										>
+											{exerciseHistory?.map((history, idx) => (
+												<Carousel.Slide
+													key={`${history.workoutId}-${history.idx}`}
+												>
+													{getSurroundingElements(
+														exerciseHistory,
+														activeHistoryIdx,
+													).includes(idx) ? (
+														<ExerciseHistory
+															hideExerciseDetails
+															hideExtraDetailsButton
+															exerciseIdx={history.idx}
+															entityId={history.workoutId}
+															entityType={FitnessEntity.Workouts}
+															onCopyButtonClick={async () => {
+																if (!coreDetails.isPro) {
+																	notifications.show({
+																		color: "red",
+																		message:
+																			"Ryot Pro required to copy sets from other workouts",
+																	});
+																	return;
+																}
+																const workout = await getWorkoutDetails(
+																	history.workoutId,
+																);
+																const yes = await confirmWrapper({
+																	confirmation: `Are you sure you want to copy all sets from "${workout.details.name}"?`,
+																});
+																if (yes) {
+																	const sets =
+																		workout.details.information.exercises[
+																			history.idx
+																		].sets;
+																	const converted = sets.map(
+																		convertHistorySetToCurrentSet,
+																	);
+																	setCurrentWorkout(
+																		produce(currentWorkout, (draft) => {
+																			draft.exercises[
+																				props.exerciseIdx
+																			].sets.push(...converted);
+																		}),
+																	);
+																}
+															}}
+														/>
+													) : null}
+												</Carousel.Slide>
+											))}
+										</Carousel>
+									))
+									.exhaustive()}
+								{(userExerciseDetails?.history?.length || 0) > 0 ? (
+									<ActionIcon
+										right={10}
+										bottom={10}
+										variant="filled"
+										color="red"
+										size="sm"
+										pos="absolute"
+										p={2}
+										onClick={() => {
+											if (!coreDetails.isPro) {
+												notifications.show({
+													color: "red",
+													message: PRO_REQUIRED_MESSAGE,
+												});
+												return;
+											}
+											setCurrentWorkout(
+												produce(currentWorkout, (draft) => {
+													draft.exercises[props.exerciseIdx].openedDetailsTab =
+														exercise.openedDetailsTab === "images"
+															? "history"
+															: "images";
+												}),
+											);
+										}}
+									>
+										{match(exercise.openedDetailsTab)
+											.with("images", undefined, () => <IconPhoto />)
+											.with("history", () => <IconClock />)
+											.exhaustive()}
+									</ActionIcon>
+								) : null}
+							</Box>
 						) : null}
 						<Flex justify="space-between" align="center" mb="xs">
 							<Text size="xs" w="5%" ta="center">
@@ -1165,17 +1320,37 @@ const SetDisplay = (props: {
 	toBeDisplayedColumns: number;
 }) => {
 	const { isCreatingTemplate } = useLoaderData<typeof loader>();
+	const coreDetails = useCoreDetails();
 	const [currentTimer, _] = useTimerAtom();
 	const [currentWorkout, setCurrentWorkout] = useCurrentWorkout();
+	const highlightedSet = currentWorkout?.highlightedSet;
 	const exercise = useGetExerciseAtIndex(props.exerciseIdx);
 	const set = useGetSetAtIndex(props.exerciseIdx, props.setIdx);
+	const [value, setValue] = useDebouncedState(set?.note || "", 500);
+	const isHighlighted =
+		highlightedSet?.exerciseIdx === props.exerciseIdx &&
+		highlightedSet?.setIdx === props.setIdx;
 	const playCheckSound = () => {
 		const sound = new Howl({ src: ["/check.mp3"] });
 		sound.play();
 	};
 
+	useDidUpdate(() => {
+		if (currentWorkout && isString(value))
+			setCurrentWorkout(
+				produce(currentWorkout, (draft) => {
+					draft.exercises[props.exerciseIdx].sets[props.setIdx].note = value;
+				}),
+			);
+	}, [value]);
+
 	return currentWorkout && exercise && set ? (
-		<Box>
+		<Paper
+			withBorder
+			id={`${props.exerciseIdx}-${props.setIdx}`}
+			shadow={isHighlighted ? "xl" : undefined}
+			style={{ borderColor: isHighlighted ? undefined : "transparent" }}
+		>
 			<Flex justify="space-between" align="center" py={4}>
 				<Menu>
 					<Menu.Target>
@@ -1214,6 +1389,33 @@ const SetDisplay = (props: {
 						))}
 						<Menu.Divider />
 						<Menu.Label>Actions</Menu.Label>
+						<Menu.Item
+							fz="xs"
+							leftSection={<IconClipboard size={14} />}
+							onClick={() => {
+								if (!coreDetails.isPro) {
+									notifications.show({
+										color: "red",
+										message: PRO_REQUIRED_MESSAGE,
+									});
+									return;
+								}
+								setCurrentWorkout(
+									produce(currentWorkout, (draft) => {
+										const hasNote = !!set.note;
+										let currentSetNote =
+											draft.exercises[props.exerciseIdx].sets[props.setIdx]
+												.note;
+										if (!hasNote) currentSetNote = true;
+										else currentSetNote = undefined;
+										draft.exercises[props.exerciseIdx].sets[props.setIdx].note =
+											currentSetNote;
+									}),
+								);
+							}}
+						>
+							{!set.note ? "Add" : "Remove"} note
+						</Menu.Item>
 						<Menu.Item
 							color="red"
 							fz="xs"
@@ -1371,10 +1573,41 @@ const SetDisplay = (props: {
 												draft.exercises[props.exerciseIdx];
 											currentExercise.sets[props.setIdx].confirmedAt =
 												newConfirmed ? dayjsLib().toISOString() : null;
+											const isLastSet =
+												props.setIdx === currentExercise.sets.length - 1;
+											const nextExerciseIdx = props.exerciseIdx + 1;
+											const nextExercise = draft.exercises[nextExerciseIdx];
+											if (newConfirmed) {
+												draft.highlightedSet = isLastSet
+													? {
+															exerciseIdx: nextExerciseIdx,
+															setIdx: 0,
+														}
+													: {
+															exerciseIdx: props.exerciseIdx,
+															setIdx: props.setIdx + 1,
+														};
+												setTimeout(() => {
+													setCurrentWorkout((w) =>
+														produce(w, (innerDraft) => {
+															if (innerDraft)
+																innerDraft.highlightedSet = undefined;
+														}),
+													);
+												}, 2000);
+												if (isLastSet) {
+													currentExercise.isShowDetailsOpen = false;
+													const nextExerciseHasDetailsToShow =
+														nextExercise &&
+														exerciseHasDetailsToShow(nextExercise);
+													if (nextExerciseHasDetailsToShow)
+														nextExercise.isShowDetailsOpen = true;
+													focusOnExercise(nextExerciseIdx);
+												}
+											}
 										}),
 									);
 								}}
-								data-statistics={JSON.stringify(set.statistic)}
 							>
 								<IconCheck />
 							</ActionIcon>
@@ -1382,7 +1615,15 @@ const SetDisplay = (props: {
 					</Transition>
 				</Group>
 			</Flex>
-		</Box>
+			{set.note ? (
+				<TextInput
+					my={4}
+					size="xs"
+					defaultValue={isString(set.note) ? set.note : ""}
+					onChange={(v) => setValue(v.currentTarget.value)}
+				/>
+			) : undefined}
+		</Paper>
 	) : null;
 };
 
diff --git a/apps/frontend/app/routes/_dashboard.fitness.$entity.$id._index.tsx b/apps/frontend/app/routes/_dashboard.fitness.$entity.$id._index.tsx
index 92910cd884..7261b9c760 100644
--- a/apps/frontend/app/routes/_dashboard.fitness.$entity.$id._index.tsx
+++ b/apps/frontend/app/routes/_dashboard.fitness.$entity.$id._index.tsx
@@ -16,14 +16,17 @@ import { DateTimePicker } from "@mantine/dates";
 import { $path } from "remix-routes";
 import "@mantine/dates/styles.css";
 import { useDisclosure } from "@mantine/hooks";
+import { notifications } from "@mantine/notifications";
 import { unstable_defineAction, unstable_defineLoader } from "@remix-run/node";
 import type { MetaArgs_SingleFetch } from "@remix-run/react";
 import { Form, Link, useLoaderData } from "@remix-run/react";
 import {
 	DeleteUserWorkoutDocument,
+	DeleteWorkoutTemplateDocument,
 	EntityLot,
 	UpdateUserWorkoutDocument,
 	WorkoutDetailsDocument,
+	WorkoutTemplateDetailsDocument,
 } from "@ryot/generated/graphql/backend/graphql";
 import {
 	changeCase,
@@ -36,8 +39,11 @@ import {
 	IconClock,
 	IconClockEdit,
 	IconDotsVertical,
+	IconPencil,
+	IconPlayerPlay,
 	IconRepeat,
 	IconRun,
+	IconTemplate,
 	IconTrash,
 	IconTrophy,
 	IconWeight,
@@ -56,9 +62,10 @@ import {
 	displayDistanceWithUnit,
 	displayWeightWithUnit,
 } from "~/components/fitness";
-import { dayjsLib } from "~/lib/generals";
+import { FitnessEntity, PRO_REQUIRED_MESSAGE, dayjsLib } from "~/lib/generals";
 import {
 	useConfirmSubmit,
+	useCoreDetails,
 	useGetWorkoutStarter,
 	useUserUnitSystem,
 } from "~/lib/hooks";
@@ -70,17 +77,13 @@ import {
 	serverGqlService,
 } from "~/lib/utilities.server";
 
-enum Entity {
-	Workouts = "workouts",
-}
-
 export const loader = unstable_defineLoader(async ({ request, params }) => {
 	const { id: entityId, entity } = zx.parseParams(params, {
 		id: z.string(),
-		entity: z.nativeEnum(Entity),
+		entity: z.nativeEnum(FitnessEntity),
 	});
 	const resp = await match(entity)
-		.with(Entity.Workouts, async () => {
+		.with(FitnessEntity.Workouts, async () => {
 			const [{ workoutDetails }] = await Promise.all([
 				serverGqlService.authenticatedRequest(request, WorkoutDetailsDocument, {
 					workoutId: entityId,
@@ -100,6 +103,19 @@ export const loader = unstable_defineLoader(async ({ request, params }) => {
 					doneOn: repeatedWorkoutData.details.startTime,
 				};
 			}
+			let template = null;
+			if (workoutDetails.details.templateId) {
+				const { workoutTemplateDetails } =
+					await serverGqlService.authenticatedRequest(
+						request,
+						WorkoutTemplateDetailsDocument,
+						{ workoutTemplateId: workoutDetails.details.templateId },
+					);
+				template = {
+					id: workoutDetails.details.templateId,
+					name: workoutTemplateDetails.details.name,
+				};
+			}
 			return {
 				entityName: workoutDetails.details.name,
 				startTime: workoutDetails.details.startTime,
@@ -108,8 +124,29 @@ export const loader = unstable_defineLoader(async ({ request, params }) => {
 				information: workoutDetails.details.information,
 				summary: workoutDetails.details.summary,
 				repeatedWorkout: repeatedWorkout,
-				template: null,
+				template,
 				collections: workoutDetails.collections,
+				defaultRestTimer: undefined,
+			};
+		})
+		.with(FitnessEntity.Templates, async () => {
+			const [{ workoutTemplateDetails }] = await Promise.all([
+				serverGqlService.authenticatedRequest(
+					request,
+					WorkoutTemplateDetailsDocument,
+					{ workoutTemplateId: entityId },
+				),
+			]);
+			return {
+				entityName: workoutTemplateDetails.details.name,
+				startTime: workoutTemplateDetails.details.createdOn,
+				endTime: null,
+				information: workoutTemplateDetails.details.information,
+				summary: workoutTemplateDetails.details.summary,
+				repeatedWorkout: null,
+				template: null,
+				collections: workoutTemplateDetails.collections,
+				defaultRestTimer: workoutTemplateDetails.details.defaultRestTimer,
 			};
 		})
 		.exhaustive();
@@ -139,11 +176,18 @@ export const action = unstable_defineAction(async ({ request }) => {
 		},
 		delete: async () => {
 			const submission = processSubmission(formData, deleteSchema);
-			await serverGqlService.authenticatedRequest(
-				request,
-				DeleteUserWorkoutDocument,
-				{ workoutId: submission.workoutId },
-			);
+			if (submission.workoutId)
+				await serverGqlService.authenticatedRequest(
+					request,
+					DeleteUserWorkoutDocument,
+					{ workoutId: submission.workoutId },
+				);
+			else if (submission.templateId)
+				await serverGqlService.authenticatedRequest(
+					request,
+					DeleteWorkoutTemplateDocument,
+					{ workoutTemplateId: submission.templateId },
+				);
 			const { entity } = submission;
 			return redirectWithToast($path("/fitness/:entity/list", { entity }), {
 				type: "success",
@@ -154,8 +198,9 @@ export const action = unstable_defineAction(async ({ request }) => {
 });
 
 const deleteSchema = z.object({
-	workoutId: z.string(),
-	entity: z.nativeEnum(Entity),
+	workoutId: z.string().optional(),
+	templateId: z.string().optional(),
+	entity: z.nativeEnum(FitnessEntity),
 });
 
 const editWorkoutSchema = z.object({
@@ -166,6 +211,7 @@ const editWorkoutSchema = z.object({
 
 export default function Page() {
 	const loaderData = useLoaderData<typeof loader>();
+	const coreDetails = useCoreDetails();
 	const submit = useConfirmSubmit();
 	const unitSystem = useUserUnitSystem();
 	const [
@@ -176,20 +222,26 @@ export default function Page() {
 	const startWorkout = useGetWorkoutStarter();
 	const [_a, setAddEntityToCollectionData] = useAddEntityToCollection();
 	const entityLot = match(loaderData.entity)
-		.with(Entity.Workouts, () => EntityLot.Workout)
+		.with(FitnessEntity.Workouts, () => EntityLot.Workout)
+		.with(FitnessEntity.Templates, () => EntityLot.WorkoutTemplate)
 		.exhaustive();
 
 	const performDecision = async (
-		entity: Entity,
+		entity: FitnessEntity,
 		repeatedFromId?: string,
-		_templateId?: string,
-		_updateWorkoutTemplateId?: string,
+		templateId?: string,
+		updateWorkoutTemplateId?: string,
+		defaultRestTimer?: number | null,
 	) => {
 		setIsWorkoutLoading(true);
 		const workout = await duplicateOldWorkout(
 			loaderData.information,
 			loaderData.entityName,
+			coreDetails,
 			repeatedFromId,
+			templateId,
+			updateWorkoutTemplateId,
+			defaultRestTimer,
 		);
 		startWorkout(workout, entity);
 		setIsWorkoutLoading(false);
@@ -247,20 +299,76 @@ export default function Page() {
 								</ActionIcon>
 							</Menu.Target>
 							<Menu.Dropdown>
-								<Menu.Item
-									onClick={() =>
-										performDecision(Entity.Workouts, loaderData.entityId)
-									}
-									leftSection={<IconRepeat size={14} />}
-								>
-									Duplicate
-								</Menu.Item>
-								<Menu.Item
-									onClick={adjustTimeModalOpen}
-									leftSection={<IconClockEdit size={14} />}
-								>
-									Adjust time
-								</Menu.Item>
+								{match(loaderData.entity)
+									.with(FitnessEntity.Templates, () => (
+										<>
+											<Menu.Item
+												onClick={() =>
+													performDecision(
+														FitnessEntity.Workouts,
+														undefined,
+														loaderData.entityId,
+														undefined,
+														loaderData.defaultRestTimer,
+													)
+												}
+												leftSection={<IconPlayerPlay size={14} />}
+											>
+												Start workout
+											</Menu.Item>
+											<Menu.Item
+												onClick={() =>
+													performDecision(
+														FitnessEntity.Templates,
+														undefined,
+														undefined,
+														loaderData.entityId,
+														loaderData.defaultRestTimer,
+													)
+												}
+												leftSection={<IconPencil size={14} />}
+											>
+												Edit template
+											</Menu.Item>
+										</>
+									))
+									.with(FitnessEntity.Workouts, () => (
+										<>
+											<Menu.Item
+												onClick={() =>
+													performDecision(
+														FitnessEntity.Workouts,
+														loaderData.entityId,
+													)
+												}
+												leftSection={<IconRepeat size={14} />}
+											>
+												Duplicate
+											</Menu.Item>
+											<Menu.Item
+												onClick={adjustTimeModalOpen}
+												leftSection={<IconClockEdit size={14} />}
+											>
+												Adjust time
+											</Menu.Item>
+											<Menu.Item
+												onClick={() => {
+													if (!coreDetails.isPro) {
+														notifications.show({
+															color: "red",
+															message: PRO_REQUIRED_MESSAGE,
+														});
+														return;
+													}
+													performDecision(FitnessEntity.Templates);
+												}}
+												leftSection={<IconTemplate size={14} />}
+											>
+												Create template
+											</Menu.Item>
+										</>
+									))
+									.exhaustive()}
 								<Menu.Item
 									onClick={() =>
 										setAddEntityToCollectionData({
@@ -281,13 +389,16 @@ export default function Page() {
 								>
 									<input
 										type="hidden"
-										name="entity"
-										defaultValue={loaderData.entity}
+										name={match(loaderData.entity)
+											.with(FitnessEntity.Workouts, () => "workoutId")
+											.with(FitnessEntity.Templates, () => "templateId")
+											.exhaustive()}
+										value={loaderData.entityId}
 									/>
 									<input
 										type="hidden"
-										name="workoutId"
-										defaultValue={loaderData.entityId}
+										name="entity"
+										value={loaderData.entity}
 									/>
 									<Menu.Item
 										onClick={async (e) => {
@@ -341,6 +452,22 @@ export default function Page() {
 							</Text>
 						</Box>
 					) : null}
+					{loaderData.template ? (
+						<Box>
+							<Text c="dimmed" span>
+								Template used:
+							</Text>{" "}
+							<Anchor
+								component={Link}
+								to={$path("/fitness/:entity/:id", {
+									entity: "templates",
+									id: loaderData.template.id,
+								})}
+							>
+								{loaderData.template.name}
+							</Anchor>
+						</Box>
+					) : null}
 					<Box>
 						<Text c="dimmed" span>
 							Done on{" "}
@@ -361,6 +488,12 @@ export default function Page() {
 									)}
 								/>
 							) : null}
+							{loaderData.defaultRestTimer ? (
+								<DisplayStat
+									icon={<IconZzz size={16} />}
+									data={`${loaderData.defaultRestTimer}s`}
+								/>
+							) : null}
 							{loaderData.summary.total ? (
 								<>
 									{Number(loaderData.summary.total.weight) !== 0 ? (
@@ -418,6 +551,7 @@ export default function Page() {
 							exerciseIdx={idx}
 							entityId={loaderData.entityId}
 							key={`${exercise.name}-${idx}`}
+							entityType={loaderData.entity}
 						/>
 					))}
 				</Stack>
diff --git a/apps/frontend/app/routes/_dashboard.fitness.$entity.list.tsx b/apps/frontend/app/routes/_dashboard.fitness.$entity.list.tsx
index 5181dd268d..c1a98e3d42 100644
--- a/apps/frontend/app/routes/_dashboard.fitness.$entity.list.tsx
+++ b/apps/frontend/app/routes/_dashboard.fitness.$entity.list.tsx
@@ -12,6 +12,7 @@ import {
 	Text,
 	Title,
 } from "@mantine/core";
+import { notifications } from "@mantine/notifications";
 import { unstable_defineLoader } from "@remix-run/node";
 import {
 	Link,
@@ -19,6 +20,7 @@ import {
 	useLoaderData,
 } from "@remix-run/react";
 import {
+	UserWorkoutTemplatesListDocument,
 	UserWorkoutsListDocument,
 	type WorkoutSummary,
 } from "@ryot/generated/graphql/backend/graphql";
@@ -26,6 +28,7 @@ import { changeCase, humanizeDuration, truncate } from "@ryot/ts-utils";
 import {
 	IconClock,
 	IconLink,
+	IconLock,
 	IconPlus,
 	IconTrophy,
 	IconWeight,
@@ -41,9 +44,15 @@ import {
 	displayWeightWithUnit,
 	getSetStatisticsTextToDisplay,
 } from "~/components/fitness";
-import { dayjsLib, pageQueryParam } from "~/lib/generals";
+import {
+	FitnessEntity,
+	PRO_REQUIRED_MESSAGE,
+	dayjsLib,
+	pageQueryParam,
+} from "~/lib/generals";
 import {
 	useAppSearchParam,
+	useCoreDetails,
 	useGetWorkoutStarter,
 	useUserUnitSystem,
 } from "~/lib/hooks";
@@ -62,17 +71,15 @@ const searchParamsSchema = z.object({
 
 export type SearchParams = z.infer<typeof searchParamsSchema>;
 
-enum Entity {
-	Workouts = "workouts",
-}
-
 export const loader = unstable_defineLoader(async ({ params, request }) => {
-	const { entity } = zx.parseParams(params, { entity: z.nativeEnum(Entity) });
+	const { entity } = zx.parseParams(params, {
+		entity: z.nativeEnum(FitnessEntity),
+	});
 	const cookieName = await getEnhancedCookieName(`${entity}.list`, request);
 	await redirectUsingEnhancedCookieSearchParams(request, cookieName);
 	const query = zx.parseQuery(request, searchParamsSchema);
 	const itemList = await match(entity)
-		.with(Entity.Workouts, async () => {
+		.with(FitnessEntity.Workouts, async () => {
 			const { userWorkoutsList } = await serverGqlService.authenticatedRequest(
 				request,
 				UserWorkoutsListDocument,
@@ -95,6 +102,24 @@ export const loader = unstable_defineLoader(async ({ params, request }) => {
 				})),
 			};
 		})
+		.with(FitnessEntity.Templates, async () => {
+			const { userWorkoutTemplatesList } =
+				await serverGqlService.authenticatedRequest(
+					request,
+					UserWorkoutTemplatesListDocument,
+					{ input: { page: query.page, query: query.query } },
+				);
+			return {
+				details: userWorkoutTemplatesList.details,
+				items: userWorkoutTemplatesList.items.map((w) => ({
+					id: w.id,
+					name: w.name,
+					timestamp: w.createdOn,
+					detail: changeCase(w.visibility),
+					summary: w.summary,
+				})),
+			};
+		})
 		.exhaustive();
 	const totalPages = await redirectToFirstPageIfOnInvalidPage(
 		request,
@@ -110,6 +135,7 @@ export const meta = ({ data }: MetaArgs_SingleFetch<typeof loader>) => {
 
 export default function Page() {
 	const loaderData = useLoaderData<typeof loader>();
+	const coreDetails = useCoreDetails();
 	const [_, { setP }] = useAppSearchParam(loaderData.cookieName);
 	const startWorkout = useGetWorkoutStarter();
 	const unitSystem = useUserUnitSystem();
@@ -123,6 +149,16 @@ export default function Page() {
 						color="green"
 						variant="outline"
 						onClick={() => {
+							if (
+								!coreDetails.isPro &&
+								loaderData.entity === FitnessEntity.Templates
+							) {
+								notifications.show({
+									color: "red",
+									message: PRO_REQUIRED_MESSAGE,
+								});
+								return;
+							}
 							startWorkout(getDefaultWorkout(), loaderData.entity);
 						}}
 					>
@@ -157,9 +193,12 @@ export default function Page() {
 												{workout.detail ? (
 													<DisplayStat
 														icon={match(loaderData.entity)
-															.with(Entity.Workouts, () => (
+															.with(FitnessEntity.Workouts, () => (
 																<IconClock size={16} />
 															))
+															.with(FitnessEntity.Templates, () => (
+																<IconLock size={16} />
+															))
 															.exhaustive()}
 														data={workout.detail}
 													/>
@@ -204,7 +243,7 @@ export default function Page() {
 									<Accordion.Panel>
 										<Group justify="space-between">
 											<Text fw="bold">Exercise</Text>
-											{loaderData.entity === Entity.Workouts ? (
+											{loaderData.entity === FitnessEntity.Workouts ? (
 												<Text fw="bold">Best set</Text>
 											) : null}
 										</Group>
diff --git a/apps/frontend/app/routes/_dashboard.fitness.exercises.item.$id._index.tsx b/apps/frontend/app/routes/_dashboard.fitness.exercises.item.$id._index.tsx
index ed26ca0d21..362914b10c 100644
--- a/apps/frontend/app/routes/_dashboard.fitness.exercises.item.$id._index.tsx
+++ b/apps/frontend/app/routes/_dashboard.fitness.exercises.item.$id._index.tsx
@@ -61,7 +61,12 @@ import {
 	displayWeightWithUnit,
 } from "~/components/fitness";
 import { MediaScrollArea } from "~/components/media";
-import { TimeSpan, dayjsLib, getDateFromTimeSpan } from "~/lib/generals";
+import {
+	FitnessEntity,
+	TimeSpan,
+	dayjsLib,
+	getDateFromTimeSpan,
+} from "~/lib/generals";
 import {
 	useUserDetails,
 	useUserPreferences,
@@ -308,6 +313,7 @@ export default function Page() {
 											key={history.workoutId}
 											exerciseIdx={history.idx}
 											entityId={history.workoutId}
+											entityType={FitnessEntity.Workouts}
 										/>
 									</Box>
 								)}
diff --git a/apps/frontend/app/routes/_dashboard.media.genre.list.tsx b/apps/frontend/app/routes/_dashboard.media.genre.list.tsx
index 7293c969aa..6758624c77 100644
--- a/apps/frontend/app/routes/_dashboard.media.genre.list.tsx
+++ b/apps/frontend/app/routes/_dashboard.media.genre.list.tsx
@@ -7,6 +7,7 @@ import {
 	Group,
 	Image,
 	Pagination,
+	Paper,
 	Stack,
 	Text,
 	Title,
@@ -18,10 +19,12 @@ import {
 	useLoaderData,
 } from "@remix-run/react";
 import {
+	GenreDetailsDocument,
 	GenresListDocument,
 	type GenresListQuery,
 } from "@ryot/generated/graphql/backend/graphql";
-import { getInitials, truncate } from "@ryot/ts-utils";
+import { getInitials, isString, truncate } from "@ryot/ts-utils";
+import { useQuery } from "@tanstack/react-query";
 import { $path } from "remix-routes";
 import { z } from "zod";
 import { zx } from "zodix";
@@ -30,9 +33,17 @@ import {
 	DebouncedSearchInput,
 	ProRequiredAlert,
 } from "~/components/common";
+import {
+	clientGqlService,
+	dayjsLib,
+	getPartialMetadataDetailsQuery,
+	queryClient,
+	queryFactory,
+} from "~/lib/generals";
 import { pageQueryParam } from "~/lib/generals";
 import {
 	useAppSearchParam,
+	useCoreDetails,
 	useFallbackImageUrl,
 	useGetMantineColor,
 } from "~/lib/hooks";
@@ -121,7 +132,29 @@ export default function Page() {
 type Genre = GenresListQuery["genresList"]["items"][number];
 
 const DisplayGenre = (props: { genre: Genre }) => {
+	const coreDetails = useCoreDetails();
 	const getMantineColor = useGetMantineColor();
+	const fallbackImageUrl = useFallbackImageUrl(getInitials(props.genre.name));
+	const { data: genreImages } = useQuery({
+		queryKey: queryFactory.media.genreImages(props.genre.id).queryKey,
+		queryFn: async () => {
+			const { genreDetails } = await clientGqlService.request(
+				GenreDetailsDocument,
+				{ input: { genreId: props.genre.id } },
+			);
+			let images = [];
+			for (const content of genreDetails.contents.items) {
+				if (images.length === 4) break;
+				const { image } = await queryClient.ensureQueryData(
+					getPartialMetadataDetailsQuery(content),
+				);
+				if (isString(image)) images.push(image);
+			}
+			if (images.length < 4) images = images.splice(0, 1);
+			return images;
+		},
+		staleTime: dayjsLib.duration(1, "hour").asMilliseconds(),
+	});
 
 	return (
 		<Anchor
@@ -130,15 +163,33 @@ const DisplayGenre = (props: { genre: Genre }) => {
 		>
 			<Stack gap={4}>
 				<Box pos="relative">
-					<Image
-						radius="md"
-						h={260}
-						alt={props.genre.name}
-						fallbackSrc={useFallbackImageUrl(getInitials(props.genre.name))}
-					/>
-					<Box pos="absolute" left={0} right={0} bottom={0}>
-						<ProRequiredAlert tooltipLabel="Collage image using genre contents" />
-					</Box>
+					{coreDetails.isPro ? (
+						<Paper radius="md" style={{ overflow: "hidden" }}>
+							<Flex h={260} w={168} wrap="wrap">
+								{genreImages?.map((image) => (
+									<Image
+										h={genreImages.length === 1 ? "auto" : 130}
+										w={genreImages.length === 1 ? "auto" : 84}
+										key={image}
+										src={image}
+										alt={props.genre.name}
+									/>
+								))}
+							</Flex>
+						</Paper>
+					) : (
+						<>
+							<Image
+								radius="md"
+								h={260}
+								alt={props.genre.name}
+								fallbackSrc={fallbackImageUrl}
+							/>
+							<Box pos="absolute" left={0} right={0} bottom={0}>
+								<ProRequiredAlert tooltipLabel="Collage image using genre contents" />
+							</Box>
+						</>
+					)}
 				</Box>
 				<Group justify="center">
 					<Box
diff --git a/apps/frontend/app/routes/_dashboard.media.item.$id._index.tsx b/apps/frontend/app/routes/_dashboard.media.item.$id._index.tsx
index e405dfcda3..be9a68f786 100644
--- a/apps/frontend/app/routes/_dashboard.media.item.$id._index.tsx
+++ b/apps/frontend/app/routes/_dashboard.media.item.$id._index.tsx
@@ -14,12 +14,15 @@ import {
 	Group,
 	Image,
 	Indicator,
+	Input,
 	Menu,
 	Modal,
+	NumberInput,
 	Paper,
 	ScrollArea,
 	Select,
 	SimpleGrid,
+	Slider,
 	Stack,
 	Tabs,
 	Text,
@@ -28,6 +31,7 @@ import {
 } from "@mantine/core";
 import { DateInput } from "@mantine/dates";
 import { useDidUpdate, useDisclosure, useInViewport } from "@mantine/hooks";
+import { notifications } from "@mantine/notifications";
 import { unstable_defineAction, unstable_defineLoader } from "@remix-run/node";
 import {
 	Form,
@@ -76,8 +80,15 @@ import {
 	IconX,
 } from "@tabler/icons-react";
 import type { HumanizeDurationOptions } from "humanize-duration-ts";
-import { Fragment, type ReactNode, forwardRef, useState } from "react";
-import { Virtuoso, VirtuosoGrid } from "react-virtuoso";
+import {
+	Fragment,
+	type ReactNode,
+	type RefObject,
+	forwardRef,
+	useRef,
+	useState,
+} from "react";
+import { Virtuoso, VirtuosoGrid, type VirtuosoHandle } from "react-virtuoso";
 import { $path } from "remix-routes";
 import { namedAction } from "remix-utils/named-action";
 import { match } from "ts-pattern";
@@ -89,6 +100,7 @@ import {
 	DisplayThreePointReview,
 	MEDIA_DETAILS_HEIGHT,
 	MediaDetailsLayout,
+	ProRequiredAlert,
 	ReviewItemDisplay,
 } from "~/components/common";
 import { confirmWrapper } from "~/components/confirmation";
@@ -101,6 +113,7 @@ import { Verb, dayjsLib, getVerb, reviewYellow } from "~/lib/generals";
 import {
 	useApplicationEvents,
 	useConfirmSubmit,
+	useCoreDetails,
 	useGetMantineColor,
 	useUserPreferences,
 } from "~/lib/hooks";
@@ -216,6 +229,7 @@ const editSeenItem = z.object({
 	seenId: z.string(),
 	startedOn: dateString.optional(),
 	finishedOn: dateString.optional(),
+	manualTimeSpent: z.string().optional(),
 	providerWatchedOn: z.string().optional(),
 });
 
@@ -228,6 +242,7 @@ export default function Page() {
 	const [tab, setTab] = useState<string | null>(
 		loaderData.query.defaultTab || "overview",
 	);
+	const podcastVirtuosoRef = useRef<VirtuosoHandle>(null);
 	const [
 		mergeMetadataModalOpened,
 		{ open: mergeMetadataModalOpen, close: mergeMetadataModalClose },
@@ -974,9 +989,11 @@ export default function Page() {
 									data={loaderData.userMetadataDetails.history}
 									itemContent={(index, history) => (
 										<HistoryItem
-											history={history}
-											key={history.id}
 											index={index}
+											setTab={setTab}
+											key={history.id}
+											history={history}
+											podcastVirtuosoRef={podcastVirtuosoRef}
 										/>
 									)}
 								/>
@@ -1036,6 +1053,7 @@ export default function Page() {
 					{loaderData.metadataDetails.podcastSpecifics ? (
 						<Tabs.Panel value="podcastEpisodes" h={MEDIA_DETAILS_HEIGHT}>
 							<Virtuoso
+								ref={podcastVirtuosoRef}
 								data={loaderData.metadataDetails.podcastSpecifics.episodes}
 								itemContent={(podcastEpisodeIdx, podcastEpisode) => (
 									<DisplayPodcastEpisode
@@ -1223,14 +1241,31 @@ const MetadataCreator = (props: {
 type History =
 	UserMetadataDetailsQuery["userMetadataDetails"]["history"][number];
 
+const DEFAULT_STATES = [0, 3];
+
 const EditHistoryRecordModal = (props: {
 	opened: boolean;
 	onClose: () => void;
 	seen: History;
 }) => {
-	const { startedOn, finishedOn, id, providerWatchedOn } = props.seen;
-	const userPreferences = useUserPreferences();
 	const loaderData = useLoaderData<typeof loader>();
+	const { startedOn, finishedOn, id, manualTimeSpent, providerWatchedOn } =
+		props.seen;
+	const [mtv, mts] = manualTimeSpent
+		? //  IDK how to make this more readable. Should've paid more attention in math class.
+			(() => {
+				for (let i = 1; i <= 10; i++) {
+					const v = Number(manualTimeSpent) ** (1 / i);
+					if (v <= 100) return [v, i];
+				}
+				return DEFAULT_STATES;
+			})()
+		: DEFAULT_STATES;
+	const [manualTimeSpentValue, setManualTimeSpentValue] = useState(mtv);
+	const [manualTimeSpentScale, setManualTimeSpentScale] = useState(mts);
+	const manualTimeSpentInMinutes = manualTimeSpentValue ** manualTimeSpentScale;
+	const userPreferences = useUserPreferences();
+	const coreDetails = useCoreDetails();
 
 	return (
 		<Modal
@@ -1259,6 +1294,45 @@ const EditHistoryRecordModal = (props: {
 						name="finishedOn"
 						defaultValue={finishedOn ? new Date(finishedOn) : undefined}
 					/>
+					<Input.Wrapper
+						label="Time spent"
+						description="How much time did you actually spend on this media? You can also adjust the scale"
+					>
+						<Box mt="xs">
+							{coreDetails.isPro ? (
+								<>
+									<Group>
+										<Slider
+											flex={1}
+											label={null}
+											value={manualTimeSpentValue}
+											onChange={setManualTimeSpentValue}
+										/>
+										<NumberInput
+											w="20%"
+											max={10}
+											size="xs"
+											value={manualTimeSpentScale}
+											onChange={(v) => setManualTimeSpentScale(Number(v))}
+										/>
+									</Group>
+									<Text c="dimmed" size="sm" ta="center" mt="xs">
+										{humanizeDuration(manualTimeSpentInMinutes * 1000)}
+									</Text>
+									{manualTimeSpentInMinutes > 0 ? (
+										<input
+											hidden
+											readOnly
+											name="manualTimeSpent"
+											value={manualTimeSpentInMinutes}
+										/>
+									) : null}
+								</>
+							) : (
+								<ProRequiredAlert tooltipLabel="Track time spent on media" />
+							)}
+						</Box>
+					</Input.Wrapper>
 					<Select
 						data={userPreferences.general.watchProviders}
 						label={`Where did you ${getVerb(Verb.Read, loaderData.metadataDetails.lot)} it?`}
@@ -1308,8 +1382,14 @@ const MergeMetadataModal = (props: {
 	);
 };
 
-const HistoryItem = (props: { history: History; index: number }) => {
+const HistoryItem = (props: {
+	index: number;
+	history: History;
+	podcastVirtuosoRef: RefObject<VirtuosoHandle>;
+	setTab: (tab: string) => void;
+}) => {
 	const loaderData = useLoaderData<typeof loader>();
+	const coreDetails = useCoreDetails();
 	const submit = useConfirmSubmit();
 	const [opened, { open, close }] = useDisclosure(false);
 	const showExtraInformation = props.history.showExtraInformation
@@ -1322,6 +1402,21 @@ const HistoryItem = (props: { history: History; index: number }) => {
 						e.episodeNumber === props.history.showExtraInformation?.episode,
 				)
 		: null;
+	const scrollToEpisode = (index?: number) => {
+		if (!coreDetails.isPro) {
+			notifications.show({
+				color: "red",
+				message: "Ryot Pro is required to jump to episodes",
+			});
+			return;
+		}
+		props.setTab("podcastEpisodes");
+		if (!isNumber(index)) return;
+		setTimeout(() => {
+			const current = props.podcastVirtuosoRef.current;
+			current?.scrollToIndex({ index, behavior: "smooth", align: "start" });
+		}, 500);
+	};
 	const displayShowExtraInformation = showExtraInformation
 		? `S${props.history.showExtraInformation?.season}-E${props.history.showExtraInformation?.episode}: ${showExtraInformation.name}`
 		: null;
@@ -1330,9 +1425,20 @@ const HistoryItem = (props: { history: History; index: number }) => {
 				(e) => e.number === props.history.podcastExtraInformation?.episode,
 			)
 		: null;
-	const displayPodcastExtraInformation = podcastExtraInformation
-		? `EP-${props.history.podcastExtraInformation?.episode}: ${podcastExtraInformation.title}`
-		: null;
+	const displayPodcastExtraInformation = podcastExtraInformation ? (
+		<Anchor
+			onClick={() =>
+				scrollToEpisode(
+					loaderData.metadataDetails.podcastSpecifics?.episodes.findIndex(
+						(e) => e.number === podcastExtraInformation.number,
+					),
+				)
+			}
+		>
+			EP-{props.history.podcastExtraInformation?.episode}:{" "}
+			{podcastExtraInformation.title}
+		</Anchor>
+	) : null;
 	const displayAnimeExtraInformation = isNumber(
 		props.history.animeExtraInformation?.episode,
 	)
@@ -1372,7 +1478,10 @@ const HistoryItem = (props: { history: History; index: number }) => {
 					.reduce((prev, curr) => [prev, " • ", curr])
 			: null;
 
-	const timeSpentInMilliseconds = 0 * 1000;
+	const timeSpentInMilliseconds =
+		(props.history.manualTimeSpent
+			? Number(props.history.manualTimeSpent)
+			: 0) * 1000;
 	const units = ["mo", "d", "h"] as HumanizeDurationOptions["units"];
 	const isLessThanAnHour =
 		timeSpentInMilliseconds < dayjsLib.duration(1, "hour").asMilliseconds();
diff --git a/apps/frontend/app/routes/_dashboard.settings.integrations.tsx b/apps/frontend/app/routes/_dashboard.settings.integrations.tsx
index 469420fcb2..1606088ebc 100644
--- a/apps/frontend/app/routes/_dashboard.settings.integrations.tsx
+++ b/apps/frontend/app/routes/_dashboard.settings.integrations.tsx
@@ -51,7 +51,11 @@ import { z } from "zod";
 import { zx } from "zodix";
 import { confirmWrapper } from "~/components/confirmation";
 import { commaDelimitedString, dayjsLib } from "~/lib/generals";
-import { useConfirmSubmit, useUserCollections } from "~/lib/hooks";
+import {
+	useConfirmSubmit,
+	useCoreDetails,
+	useUserCollections,
+} from "~/lib/hooks";
 import { createToastHeaders, serverGqlService } from "~/lib/utilities.server";
 
 const YANK_INTEGRATIONS = [
@@ -153,6 +157,7 @@ const createSchema = z.object({
 	provider: z.nativeEnum(IntegrationProvider),
 	minimumProgress: z.string().optional(),
 	maximumProgress: z.string().optional(),
+	syncToOwnedCollection: zx.CheckboxAsString.optional(),
 	providerSpecifics: z
 		.object({
 			plexUsername: z.string().optional(),
@@ -185,6 +190,7 @@ const updateSchema = z.object({
 	minimumProgress: z.string().optional(),
 	maximumProgress: z.string().optional(),
 	isDisabled: zx.CheckboxAsString.optional(),
+	syncToOwnedCollection: zx.CheckboxAsString.optional(),
 });
 
 export default function Page() {
@@ -323,6 +329,9 @@ const DisplayIntegration = (props: {
 								{dayjsLib(props.integration.lastTriggeredOn).fromNow()}
 							</Text>
 						) : null}
+						{props.integration.syncToOwnedCollection ? (
+							<Text size="xs">Being synced to "Owned" collection</Text>
+						) : null}
 					</Box>
 					<Group>
 						{!NO_SHOW_URL.includes(props.integration.provider) ? (
@@ -381,6 +390,7 @@ const CreateIntegrationModal = (props: {
 	createModalOpened: boolean;
 	closeIntegrationModal: () => void;
 }) => {
+	const coreDetails = useCoreDetails();
 	const [provider, setProvider] = useState<IntegrationProvider | null>(null);
 
 	return (
@@ -485,6 +495,20 @@ const CreateIntegrationModal = (props: {
 						.with(IntegrationProvider.Radarr, () => <ArrInputs name="radarr" />)
 						.with(IntegrationProvider.Sonarr, () => <ArrInputs name="sonarr" />)
 						.otherwise(() => undefined)}
+					{provider && YANK_INTEGRATIONS.includes(provider) ? (
+						<Tooltip
+							label="Only available for Pro users"
+							disabled={coreDetails.isPro}
+						>
+							<Checkbox
+								label="Sync to Owned collection"
+								name="syncToOwnedCollection"
+								description={`Checking this will also sync items in your library to the "Owned" collection`}
+								styles={{ body: { display: "flex", alignItems: "center" } }}
+								disabled={!coreDetails.isPro}
+							/>
+						</Tooltip>
+					) : undefined}
 					<Button type="submit">Submit</Button>
 				</Stack>
 			</Form>
@@ -588,6 +612,19 @@ const UpdateIntegrationModal = (props: {
 								props.updateIntegrationData.isDisabled || undefined
 							}
 						/>
+						{YANK_INTEGRATIONS.includes(
+							props.updateIntegrationData.provider,
+						) ? (
+							<Checkbox
+								label="Sync to Owned collection"
+								name="syncToOwnedCollection"
+								description={`Checking this will also sync items in your library to the "Owned" collection`}
+								styles={{ body: { display: "flex", alignItems: "center" } }}
+								defaultChecked={
+									props.updateIntegrationData.syncToOwnedCollection || undefined
+								}
+							/>
+						) : null}
 						<Button type="submit">Submit</Button>
 					</Stack>
 				</Form>
diff --git a/apps/frontend/app/routes/_dashboard.settings.miscellaneous.tsx b/apps/frontend/app/routes/_dashboard.settings.miscellaneous.tsx
index 03595d5097..8b567d64fc 100644
--- a/apps/frontend/app/routes/_dashboard.settings.miscellaneous.tsx
+++ b/apps/frontend/app/routes/_dashboard.settings.miscellaneous.tsx
@@ -16,7 +16,7 @@ import {
 } from "@ryot/generated/graphql/backend/graphql";
 import { processSubmission } from "@ryot/ts-utils";
 import { z } from "zod";
-import { useUserDetails } from "~/lib/hooks";
+import { useDashboardLayoutData, useUserDetails } from "~/lib/hooks";
 import { createToastHeaders, serverGqlService } from "~/lib/utilities.server";
 
 export const meta = (_args: MetaArgs_SingleFetch) => {
@@ -45,6 +45,8 @@ const jobSchema = z.object({
 
 export default function Page() {
 	const userDetails = useUserDetails();
+	const dashboardData = useDashboardLayoutData();
+	const isEditDisabled = dashboardData.isDemo;
 
 	return (
 		<Container size="lg">
@@ -67,7 +69,11 @@ export default function Page() {
 											providers.
 										</Text>
 									</Box>
-									<Button {...btnProps} value={BackgroundJob.UpdateAllMetadata}>
+									<Button
+										disabled={isEditDisabled}
+										{...btnProps}
+										value={BackgroundJob.UpdateAllMetadata}
+									>
 										Update metadata
 									</Button>
 								</Stack>
@@ -82,6 +88,7 @@ export default function Page() {
 										</Text>
 									</Box>
 									<Button
+										disabled={isEditDisabled}
 										{...btnProps}
 										value={BackgroundJob.RecalculateCalendarEvents}
 									>
@@ -98,6 +105,7 @@ export default function Page() {
 										</Text>
 									</Box>
 									<Button
+										disabled={isEditDisabled}
 										{...btnProps}
 										value={BackgroundJob.UpdateAllExercises}
 									>
@@ -115,6 +123,7 @@ export default function Page() {
 										</Text>
 									</Box>
 									<Button
+										disabled={isEditDisabled}
 										{...btnProps}
 										value={BackgroundJob.PerformBackgroundTasks}
 									>
@@ -133,6 +142,7 @@ export default function Page() {
 								</Text>
 							</Box>
 							<Button
+								disabled={isEditDisabled}
 								{...btnProps}
 								value={BackgroundJob.CalculateUserActivitiesAndSummary}
 							>
@@ -149,6 +159,7 @@ export default function Page() {
 								</Text>
 							</Box>
 							<Button
+								disabled={isEditDisabled}
 								{...btnProps}
 								value={BackgroundJob.ReEvaluateUserWorkouts}
 							>
@@ -164,7 +175,11 @@ export default function Page() {
 									enabled, the longer this will take.
 								</Text>
 							</Box>
-							<Button {...btnProps} value={BackgroundJob.SyncIntegrationsData}>
+							<Button
+								disabled={isEditDisabled}
+								{...btnProps}
+								value={BackgroundJob.SyncIntegrationsData}
+							>
 								Synchronize
 							</Button>
 						</Stack>
diff --git a/apps/frontend/app/routes/_dashboard.settings.preferences.tsx b/apps/frontend/app/routes/_dashboard.settings.preferences.tsx
index 1603add529..ea7a6d1895 100644
--- a/apps/frontend/app/routes/_dashboard.settings.preferences.tsx
+++ b/apps/frontend/app/routes/_dashboard.settings.preferences.tsx
@@ -55,7 +55,11 @@ import { z } from "zod";
 import { zx } from "zodix";
 import { confirmWrapper } from "~/components/confirmation";
 import { queryClient, queryFactory } from "~/lib/generals";
-import { useConfirmSubmit, useUserPreferences } from "~/lib/hooks";
+import {
+	useConfirmSubmit,
+	useDashboardLayoutData,
+	useUserPreferences,
+} from "~/lib/hooks";
 import {
 	createToastHeaders,
 	isWorkoutActive,
@@ -129,7 +133,8 @@ export default function Page() {
 	const [defaultTab, setDefaultTab] = useState(
 		loaderData.query.defaultTab || "dashboard",
 	);
-	const isEditDisabled = false;
+	const dashboardData = useDashboardLayoutData();
+	const isEditDisabled = dashboardData.isDemo;
 
 	const appendPref = (property: string, value: string) => {
 		const index = toUpdatePreferences.findIndex((p) => p[0] === property);
diff --git a/apps/frontend/app/routes/_dashboard.settings.profile-and-sharing.tsx b/apps/frontend/app/routes/_dashboard.settings.profile-and-sharing.tsx
index 056de677c4..3cf9f38961 100644
--- a/apps/frontend/app/routes/_dashboard.settings.profile-and-sharing.tsx
+++ b/apps/frontend/app/routes/_dashboard.settings.profile-and-sharing.tsx
@@ -1,30 +1,73 @@
+import { useAutoAnimate } from "@formkit/auto-animate/react";
 import {
+	ActionIcon,
 	Box,
 	Button,
+	Checkbox,
 	Container,
+	Flex,
+	Group,
+	Modal,
+	NumberInput,
+	Paper,
 	PasswordInput,
+	Skeleton,
 	Stack,
 	Tabs,
+	Text,
 	TextInput,
+	ThemeIcon,
+	Title,
+	Tooltip,
 } from "@mantine/core";
-import { unstable_defineAction } from "@remix-run/node";
-import { Form } from "@remix-run/react";
-import type { MetaArgs_SingleFetch } from "@remix-run/react";
-import { UpdateUserDocument } from "@ryot/generated/graphql/backend/graphql";
-import { processSubmission } from "@ryot/ts-utils";
+import { DateTimePicker } from "@mantine/dates";
+import { useDisclosure } from "@mantine/hooks";
+import { unstable_defineAction, unstable_defineLoader } from "@remix-run/node";
+import {
+	Form,
+	type MetaArgs_SingleFetch,
+	useLoaderData,
+} from "@remix-run/react";
+import {
+	CreateAccessLinkDocument,
+	RevokeAccessLinkDocument,
+	UpdateUserDocument,
+	UserAccessLinksDocument,
+	type UserAccessLinksQuery,
+} from "@ryot/generated/graphql/backend/graphql";
+import { isNumber, isString, processSubmission } from "@ryot/ts-utils";
+import {
+	IconEye,
+	IconEyeClosed,
+	IconLock,
+	IconLockAccess,
+} from "@tabler/icons-react";
+import { ClientOnly } from "remix-utils/client-only";
 import { namedAction } from "remix-utils/named-action";
 import { withQuery } from "ufo";
 import { z } from "zod";
-import { ProRequiredAlert } from "~/components/common";
+import { zx } from "zodix";
 import { confirmWrapper } from "~/components/confirmation";
-import { queryClient, queryFactory } from "~/lib/generals";
-import { useConfirmSubmit, useUserDetails } from "~/lib/hooks";
+import { dayjsLib, queryClient, queryFactory } from "~/lib/generals";
+import {
+	useConfirmSubmit,
+	useCoreDetails,
+	useDashboardLayoutData,
+	useUserDetails,
+} from "~/lib/hooks";
 import {
 	createToastHeaders,
 	getAuthorizationCookie,
 	serverGqlService,
 } from "~/lib/utilities.server";
 
+export const loader = unstable_defineLoader(async ({ request }) => {
+	const [{ userAccessLinks }] = await Promise.all([
+		serverGqlService.authenticatedRequest(request, UserAccessLinksDocument, {}),
+	]);
+	return { userAccessLinks };
+});
+
 export const meta = (_args: MetaArgs_SingleFetch) => {
 	return [{ title: "Profile and Sharing | Ryot" }];
 };
@@ -48,6 +91,54 @@ export const action = unstable_defineAction(async ({ request }) => {
 				}),
 			});
 		},
+		revokeAccessLink: async () => {
+			const submission = processSubmission(
+				formData,
+				revokeAccessLinkFormSchema,
+			);
+			await serverGqlService.authenticatedRequest(
+				request,
+				RevokeAccessLinkDocument,
+				submission,
+			);
+			return Response.json({ status: "success" } as const, {
+				headers: await createToastHeaders({
+					type: "success",
+					message: "Access link revoked successfully",
+				}),
+			});
+		},
+		createAccessLink: async () => {
+			const submission = processSubmission(
+				formData,
+				createAccessLinkFormSchema,
+			);
+			submission.isMutationAllowed = submission.isMutationAllowed === true;
+			await serverGqlService.authenticatedRequest(
+				request,
+				CreateAccessLinkDocument,
+				{ input: submission },
+			);
+			return Response.json({ status: "success" } as const, {
+				headers: await createToastHeaders({
+					type: "success",
+					message: "Access link created successfully",
+				}),
+			});
+		},
+		createDefaultAccessLink: async () => {
+			await serverGqlService.authenticatedRequest(
+				request,
+				CreateAccessLinkDocument,
+				{ input: { name: "Account default", isAccountDefault: true } },
+			);
+			return Response.json({ status: "success" } as const, {
+				headers: await createToastHeaders({
+					type: "success",
+					message: "Account default access link created successfully",
+				}),
+			});
+		},
 	});
 });
 
@@ -58,10 +149,34 @@ const updateProfileFormSchema = z.object({
 	password: z.string().optional(),
 });
 
+const revokeAccessLinkFormSchema = z.object({
+	accessLinkId: z.string(),
+});
+
+const createAccessLinkFormSchema = z.object({
+	name: z.string(),
+	expiresOn: z.string().optional(),
+	redirectTo: z.string().optional(),
+	maximumUses: zx.IntAsString.optional(),
+	isMutationAllowed: zx.CheckboxAsString.optional(),
+});
+
 export default function Page() {
 	const userDetails = useUserDetails();
+	const loaderData = useLoaderData<typeof loader>();
 	const submit = useConfirmSubmit();
-	const isEditDisabled = false;
+	const dashboardData = useDashboardLayoutData();
+	const isEditDisabled = dashboardData.isDemo;
+	const [
+		createAccessLinkModalOpened,
+		{ open: openCreateAccessLinkModal, close: closeCreateAccessLinkModal },
+	] = useDisclosure(false);
+
+	const defaultAccountLink = loaderData.userAccessLinks.find(
+		(acl) => acl.isAccountDefault,
+	);
+	const hasDefaultAccountLink =
+		defaultAccountLink && defaultAccountLink?.isRevoked !== true;
 
 	return (
 		<Container size="xs">
@@ -113,6 +228,7 @@ export default function Page() {
 													: undefined
 										}
 									/>
+
 									<Button
 										type="submit"
 										onClick={async (e) => {
@@ -134,7 +250,73 @@ export default function Page() {
 					</Tabs.Panel>
 					<Tabs.Panel value="sharing">
 						<Stack>
-							<ProRequiredAlert tooltipLabel="Allow others to see your favorite media without signing up" />
+							<ClientOnly fallback={<Skeleton h={90} />}>
+								{() => (
+									<Paper withBorder p="md">
+										<Form
+											method="POST"
+											action={withQuery(".", {
+												intent: hasDefaultAccountLink
+													? "revokeAccessLink"
+													: "createDefaultAccessLink",
+											})}
+										>
+											{hasDefaultAccountLink ? (
+												<input
+													readOnly
+													type="hidden"
+													name="accessLinkId"
+													value={defaultAccountLink.id}
+												/>
+											) : null}
+											<Group wrap="nowrap">
+												<Box>
+													<Text>Make my account public</Text>
+													<Text size="xs" c="dimmed">
+														Anyone would be able to view your profile by
+														visiting {window.location.origin}/u/
+														{userDetails.name}
+													</Text>
+												</Box>
+												<Button
+													w="30%"
+													type="submit"
+													color={hasDefaultAccountLink ? "blue" : "green"}
+													variant="light"
+												>
+													{hasDefaultAccountLink ? "Disable" : "Enable"}
+												</Button>
+											</Group>
+										</Form>
+									</Paper>
+								)}
+							</ClientOnly>
+							{loaderData.userAccessLinks.length > 0 ? (
+								loaderData.userAccessLinks.map((link, idx) => (
+									<DisplayAccessLink
+										accessLink={link}
+										key={`${link.id}-${idx}`}
+										isEditDisabled={isEditDisabled}
+									/>
+								))
+							) : (
+								<Text>No access links configured</Text>
+							)}
+							<Flex w="100%">
+								<Button
+									size="xs"
+									variant="light"
+									radius="md"
+									onClick={openCreateAccessLinkModal}
+									ml="auto"
+								>
+									Create new access link
+								</Button>
+								<CreateAccessLinkModal
+									createModalOpened={createAccessLinkModalOpened}
+									closeModal={closeCreateAccessLinkModal}
+								/>
+							</Flex>
 						</Stack>
 					</Tabs.Panel>
 				</Box>
@@ -142,3 +324,179 @@ export default function Page() {
 		</Container>
 	);
 }
+
+type AccessLink = UserAccessLinksQuery["userAccessLinks"][number];
+
+const DisplayAccessLink = (props: {
+	accessLink: AccessLink;
+	isEditDisabled: boolean;
+}) => {
+	const userDetails = useUserDetails();
+	const [parent] = useAutoAnimate();
+	const [inputOpened, { toggle: inputToggle }] = useDisclosure(false);
+	const submit = useConfirmSubmit();
+
+	const accessLinkUrl =
+		typeof window !== "undefined"
+			? `${window.location.origin}/${
+					props.accessLink.isAccountDefault
+						? `u/${userDetails.name}`
+						: `_s/${props.accessLink.id}`
+				}`
+			: "";
+
+	const optionalDetails = [
+		props.accessLink.expiresOn
+			? `Expiry: ${dayjsLib(props.accessLink.expiresOn).fromNow()}`
+			: null,
+		isNumber(props.accessLink.maximumUses)
+			? `Maximum uses: ${props.accessLink.maximumUses}`
+			: null,
+		props.accessLink.isMutationAllowed ? "Mutation allowed" : null,
+		props.accessLink.isDemo ? "Demo access" : null,
+	]
+		.filter(isString)
+		.join(", ");
+
+	return (
+		<Paper p="xs" withBorder>
+			<Stack ref={parent}>
+				<Flex align="center" justify="space-between">
+					<Box>
+						<Text fw="bold" span>
+							{props.accessLink.name}
+						</Text>
+						<Text size="sm">
+							Created: {dayjsLib(props.accessLink.createdOn).fromNow()}, Times
+							Used: {props.accessLink.timesUsed}
+						</Text>
+						{optionalDetails ? <Text size="xs">{optionalDetails}</Text> : null}
+					</Box>
+					<Group wrap="nowrap">
+						{props.accessLink.isRevoked !== true ? (
+							<>
+								<ActionIcon color="blue" onClick={inputToggle}>
+									{inputOpened ? <IconEyeClosed /> : <IconEye />}
+								</ActionIcon>
+								<Form
+									method="POST"
+									action={withQuery("", { intent: "revokeAccessLink" })}
+								>
+									<input
+										type="hidden"
+										name="accessLinkId"
+										defaultValue={props.accessLink.id}
+									/>
+									<Tooltip
+										label="Can not revoke access links for demo user"
+										disabled={!props.isEditDisabled}
+									>
+										<ActionIcon
+											mt={4}
+											color="red"
+											type="submit"
+											variant="subtle"
+											disabled={props.isEditDisabled || undefined}
+											onClick={async (e) => {
+												const form = e.currentTarget.form;
+												e.preventDefault();
+												const conf = await confirmWrapper({
+													confirmation:
+														"Are you sure you want to revoke this access link?",
+												});
+												if (conf && form) submit(form);
+											}}
+										>
+											<IconLock />
+										</ActionIcon>
+									</Tooltip>
+								</Form>
+							</>
+						) : (
+							<>
+								<ThemeIcon color="red" size="lg" variant="outline">
+									<IconLockAccess />
+								</ThemeIcon>
+								<Text size="xs" c="dimmed">
+									Revoked
+								</Text>
+							</>
+						)}
+					</Group>
+				</Flex>
+				{inputOpened ? (
+					<TextInput
+						readOnly
+						value={accessLinkUrl}
+						onClick={(e) => e.currentTarget.select()}
+						description="Share this link with others to give them access to your data"
+					/>
+				) : null}
+			</Stack>
+		</Paper>
+	);
+};
+
+const CreateAccessLinkModal = (props: {
+	createModalOpened: boolean;
+	closeModal: () => void;
+}) => {
+	const coreDetails = useCoreDetails();
+	const defaultExpiresAtValue = dayjsLib()
+		.add(coreDetails.tokenValidForDays, "day")
+		.toDate();
+
+	return (
+		<Modal
+			opened={props.createModalOpened}
+			onClose={props.closeModal}
+			centered
+			withCloseButton={false}
+		>
+			<Form
+				replace
+				method="POST"
+				onSubmit={() => props.closeModal()}
+				action={withQuery("", { intent: "createAccessLink" })}
+			>
+				<Stack>
+					<Title order={3}>Create new access link</Title>
+					<Text size="xs" c="dimmed">
+						Once a link has become invalid or been revoked, it will be
+						automatically deleted. If none of the below are provided, the link
+						will never expire and have unlimited uses.
+					</Text>
+					<TextInput
+						required
+						name="name"
+						label="Name"
+						description="A descriptive name for this link"
+					/>
+					<DateTimePicker
+						clearable
+						name="expiresOn"
+						label="Expires at"
+						description="This link will become invalid after this timestamp"
+						defaultValue={defaultExpiresAtValue}
+					/>
+					<NumberInput
+						name="maximumUses"
+						label="Maximum uses"
+						description="This link will become invalid after this number of uses"
+					/>
+					<TextInput
+						name="redirectTo"
+						label="Redirect to"
+						description="Users will be redirected to this URL after accessing the URL"
+					/>
+					<Checkbox
+						label="Allow mutation"
+						name="isMutationAllowed"
+						description="Users will be able to change your data if this is checked"
+					/>
+					<Button type="submit">Submit</Button>
+				</Stack>
+			</Form>
+		</Modal>
+	);
+};
diff --git a/apps/frontend/app/routes/_dashboard.settings.users.tsx b/apps/frontend/app/routes/_dashboard.settings.users.tsx
index 926702bc68..5e63beb7d6 100644
--- a/apps/frontend/app/routes/_dashboard.settings.users.tsx
+++ b/apps/frontend/app/routes/_dashboard.settings.users.tsx
@@ -237,11 +237,10 @@ export default function Page() {
 					</Form>
 				</Modal>
 				<DebouncedSearchInput
-					placeholder="Search for users"
+					placeholder="Search by name or ID"
 					initialValue={loaderData.query.query}
 					enhancedQueryParams={loaderData.cookieName}
 				/>
-
 				<VirtuosoGrid
 					components={{
 						List: forwardRef((props, ref) => (
diff --git a/apps/frontend/app/routes/_dashboard.tsx b/apps/frontend/app/routes/_dashboard.tsx
index a879b0c663..82cc60502f 100644
--- a/apps/frontend/app/routes/_dashboard.tsx
+++ b/apps/frontend/app/routes/_dashboard.tsx
@@ -85,6 +85,7 @@ import {
 	IconClock,
 	IconDeviceSpeaker,
 	IconDeviceTv,
+	IconEyeglass,
 	IconHome2,
 	IconLogout,
 	IconMoodEmpty,
@@ -97,6 +98,7 @@ import {
 	IconSun,
 } from "@tabler/icons-react";
 import { produce } from "immer";
+import { jwtDecode } from "jwt-decode";
 import { type FormEvent, useState } from "react";
 import { Fragment } from "react/jsx-runtime";
 import { $path } from "remix-routes";
@@ -115,6 +117,7 @@ import {
 import {
 	useApplicationEvents,
 	useConfirmSubmit,
+	useCoreDetails,
 	useMetadataDetails,
 	useUserCollections,
 	useUserDetails,
@@ -130,6 +133,7 @@ import {
 } from "~/lib/state/media";
 import {
 	serverVariables as envData,
+	getAuthorizationCookie,
 	getCachedCoreDetails,
 	getCachedUserCollectionsList,
 	getCachedUserPreferences,
@@ -238,14 +242,22 @@ export const loader = unstable_defineLoader(async ({ request }) => {
 		request.headers.get("cookie") || "",
 	);
 
+	const decodedCookie = jwtDecode<{
+		access_link?: { id: string; is_demo?: boolean };
+	}>(getAuthorizationCookie(request));
+	const isAccessLinkSession = Boolean(decodedCookie?.access_link);
+	const isDemo = Boolean(decodedCookie?.access_link?.is_demo);
+
 	const shouldHaveUmami =
 		envData.FRONTEND_UMAMI_SCRIPT_URL &&
 		envData.FRONTEND_UMAMI_WEBSITE_ID &&
-		!envData.DISABLE_TELEMETRY;
+		!envData.DISABLE_TELEMETRY &&
+		!isDemo;
 
 	const workoutInProgress = isWorkoutActive(request);
 
 	return {
+		isDemo,
 		envData,
 		mediaLinks,
 		userDetails,
@@ -257,6 +269,7 @@ export const loader = unstable_defineLoader(async ({ request }) => {
 		userCollections,
 		workoutInProgress,
 		currentColorScheme,
+		isAccessLinkSession,
 	};
 });
 
@@ -339,6 +352,7 @@ export function ErrorBoundary() {
 
 export default function Layout() {
 	const loaderData = useLoaderData<typeof loader>();
+	const userDetails = useUserDetails();
 	const [parent] = useAutoAnimate();
 	const submit = useConfirmSubmit();
 	const [openedLinkGroups, setOpenedLinkGroups] = useLocalStorage<
@@ -540,6 +554,13 @@ export default function Layout() {
 						/>
 					</Box>
 					<Flex direction="column" justify="center" gap="md">
+						{loaderData.isAccessLinkSession ? (
+							<Tooltip label={`You are viewing ${userDetails.name}'s data.`}>
+								<Button leftSection={<IconEyeglass />} disabled>
+									Visitor
+								</Button>
+							</Tooltip>
+						) : null}
 						<Form
 							method="POST"
 							action={withQuery("/actions", { intent: "toggleColorScheme" })}
@@ -722,13 +743,13 @@ const LinksGroup = ({
 };
 
 const Footer = () => {
-	const loaderData = useLoaderData<typeof loader>();
+	const coreDetails = useCoreDetails();
 
 	return (
 		<Stack>
 			<Flex gap={80} justify="center">
-				{!loaderData.coreDetails.isPro ? (
-					<Anchor href={loaderData.coreDetails.websiteUrl} target="_blank">
+				{!coreDetails.isPro ? (
+					<Anchor href={coreDetails.websiteUrl} target="_blank">
 						<Text c="red" fw="bold">
 							Ryot Pro
 						</Text>
@@ -740,9 +761,9 @@ const Footer = () => {
 					</Text>
 				</Anchor>
 				<Text c="grape" fw="bold" visibleFrom="md">
-					{loaderData.coreDetails.version}
+					{coreDetails.version}
 				</Text>
-				<Anchor href={loaderData.coreDetails.repositoryLink} target="_blank">
+				<Anchor href={coreDetails.repositoryLink} target="_blank">
 					<Text c="orange" fw="bold">
 						Github
 					</Text>
diff --git a/apps/frontend/app/routes/api.sharing.$accessLinkId.tsx b/apps/frontend/app/routes/api.sharing.$accessLinkId.tsx
new file mode 100644
index 0000000000..0e00ae33f4
--- /dev/null
+++ b/apps/frontend/app/routes/api.sharing.$accessLinkId.tsx
@@ -0,0 +1,56 @@
+import { redirect, unstable_defineLoader } from "@remix-run/node";
+import {
+	ProcessAccessLinkDocument,
+	type ProcessAccessLinkInput,
+} from "@ryot/generated/graphql/backend/graphql";
+import { $path } from "remix-routes";
+import { safeRedirect } from "remix-utils/safe-redirect";
+import { z } from "zod";
+import { zx } from "zodix";
+import { redirectToQueryParam } from "~/lib/generals";
+import {
+	createToastHeaders,
+	getCookiesForApplication,
+	serverGqlService,
+} from "~/lib/utilities.server";
+
+const paramsSchema = z.object({ accessLinkId: z.string() });
+
+const searchParamsSchema = z.object({
+	isAccountDefault: zx.BoolAsString.optional(),
+	[redirectToQueryParam]: z.string().optional(),
+});
+
+export const loader = unstable_defineLoader(async ({ request, params }) => {
+	const query = zx.parseQuery(request, searchParamsSchema);
+	const routeParams = zx.parseParams(params, paramsSchema);
+	const input: ProcessAccessLinkInput = {};
+	if (query.isAccountDefault) input.username = routeParams.accessLinkId;
+	else input.id = routeParams.accessLinkId;
+	const { processAccessLink } = await serverGqlService.request(
+		ProcessAccessLinkDocument,
+		{ input },
+	);
+	if (processAccessLink.__typename === "ProcessAccessLinkResponse") {
+		const queryParams = zx.parseQuery(request, searchParamsSchema);
+		const headers = await getCookiesForApplication(
+			processAccessLink.apiKey,
+			processAccessLink.tokenValidForDays,
+		);
+		return redirect(
+			safeRedirect(
+				queryParams[redirectToQueryParam] ||
+					processAccessLink.redirectTo ||
+					$path("/"),
+			),
+			{ headers },
+		);
+	}
+	return redirect($path("/auth"), {
+		headers: await createToastHeaders({
+			type: "error",
+			title: "Error processing access link",
+			message: `Encountered: ${processAccessLink.error}`,
+		}),
+	});
+});
diff --git a/apps/frontend/package.json b/apps/frontend/package.json
index b05f090049..4e10652f31 100644
--- a/apps/frontend/package.json
+++ b/apps/frontend/package.json
@@ -43,6 +43,7 @@
 		"isbot": "5.1.17",
 		"jotai": "2.9.3",
 		"js-cookie": "3.0.5",
+		"jwt-decode": "4.0.0",
 		"mantine-datatable": "7.11.3",
 		"react": "18.3.1",
 		"react-confirm": "0.3.0-7",
diff --git a/apps/website/Dockerfile b/apps/website/Dockerfile
new file mode 100644
index 0000000000..85a6b7b638
--- /dev/null
+++ b/apps/website/Dockerfile
@@ -0,0 +1,32 @@
+ARG NODE_BASE_IMAGE=node:20.10.0-bookworm-slim
+
+FROM --platform=$BUILDPLATFORM $NODE_BASE_IMAGE AS node-build-base
+
+FROM node-build-base AS website-build-base
+ENV MOON_TOOLCHAIN_FORCE_GLOBALS=true
+WORKDIR /app
+RUN apt update && apt install -y --no-install-recommends git curl ca-certificates xz-utils
+RUN npm install -g @moonrepo/cli && moon --version
+
+FROM website-build-base AS website-workspace
+WORKDIR /app
+COPY . .
+RUN moon docker scaffold website
+
+FROM website-build-base AS website-builder
+WORKDIR /app
+COPY --from=website-workspace /app/.moon/docker/workspace .
+RUN moon docker setup
+COPY --from=website-workspace /app/.moon/docker/sources .
+RUN moon run website:build
+RUN moon docker prune
+
+FROM $NODE_BASE_IMAGE
+ENV NODE_ENV=production
+RUN useradd -m -u 1001 ryot
+WORKDIR /home/ryot
+COPY --from=website-builder --chown=ryot:ryot /app/apps/website/node_modules ./node_modules
+COPY --from=website-builder --chown=ryot:ryot /app/apps/website/package.json ./package.json
+COPY --from=website-builder --chown=ryot:ryot /app/apps/website/build ./build
+COPY --chown=ryot:ryot apps/website/app/drizzle/migrations app/drizzle/migrations
+CMD npx remix-serve ./build/server/index.js
diff --git a/apps/website/app/drizzle/migrations/0000_perfect_reavers.sql b/apps/website/app/drizzle/migrations/0000_perfect_reavers.sql
new file mode 100644
index 0000000000..cef02637ce
--- /dev/null
+++ b/apps/website/app/drizzle/migrations/0000_perfect_reavers.sql
@@ -0,0 +1,29 @@
+DO $$ BEGIN
+ CREATE TYPE "public"."plan_type" AS ENUM('monthly', 'yearly', 'lifetime');
+EXCEPTION
+ WHEN duplicate_object THEN null;
+END $$;
+--> statement-breakpoint
+DO $$ BEGIN
+ CREATE TYPE "public"."product_type" AS ENUM('cloud', 'self_hosted');
+EXCEPTION
+ WHEN duplicate_object THEN null;
+END $$;
+--> statement-breakpoint
+CREATE TABLE IF NOT EXISTS "customer" (
+	"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
+	"email" text NOT NULL,
+	"created_on" timestamp with time zone DEFAULT now() NOT NULL,
+	"oidc_issuer_id" text,
+	"paddle_customer_id" text,
+	"paddle_first_transaction_id" text,
+	"product_type" "product_type",
+	"plan_type" "plan_type",
+	"renew_on" date,
+	"unkey_key_id" text,
+	"ryot_user_id" text,
+	CONSTRAINT "customer_email_unique" UNIQUE("email"),
+	CONSTRAINT "customer_oidc_issuer_id_unique" UNIQUE("oidc_issuer_id"),
+	CONSTRAINT "customer_paddle_customer_id_unique" UNIQUE("paddle_customer_id"),
+	CONSTRAINT "customer_paddle_first_transaction_id_unique" UNIQUE("paddle_first_transaction_id")
+);
diff --git a/apps/website/app/drizzle/migrations/0001_next_human_cannonball.sql b/apps/website/app/drizzle/migrations/0001_next_human_cannonball.sql
new file mode 100644
index 0000000000..18eb2c7168
--- /dev/null
+++ b/apps/website/app/drizzle/migrations/0001_next_human_cannonball.sql
@@ -0,0 +1,8 @@
+CREATE TABLE IF NOT EXISTS "contact_submission" (
+	"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
+	"created_at" timestamp DEFAULT now() NOT NULL,
+	"email" text NOT NULL,
+	"message" text NOT NULL,
+	"is_spam" boolean,
+	"is_addressed" boolean
+);
diff --git a/apps/website/app/drizzle/migrations/0002_sad_pixie.sql b/apps/website/app/drizzle/migrations/0002_sad_pixie.sql
new file mode 100644
index 0000000000..55d32ed2b7
--- /dev/null
+++ b/apps/website/app/drizzle/migrations/0002_sad_pixie.sql
@@ -0,0 +1,1 @@
+ALTER TABLE "customer" ADD COLUMN "has_cancelled" boolean;
\ No newline at end of file
diff --git a/apps/website/app/drizzle/migrations/meta/0000_snapshot.json b/apps/website/app/drizzle/migrations/meta/0000_snapshot.json
new file mode 100644
index 0000000000..66170de477
--- /dev/null
+++ b/apps/website/app/drizzle/migrations/meta/0000_snapshot.json
@@ -0,0 +1,128 @@
+{
+	"id": "8eff47f3-d2ea-49ee-a85e-2e4689c410f1",
+	"prevId": "00000000-0000-0000-0000-000000000000",
+	"version": "7",
+	"dialect": "postgresql",
+	"tables": {
+		"public.customer": {
+			"name": "customer",
+			"schema": "",
+			"columns": {
+				"id": {
+					"name": "id",
+					"type": "uuid",
+					"primaryKey": true,
+					"notNull": true,
+					"default": "gen_random_uuid()"
+				},
+				"email": {
+					"name": "email",
+					"type": "text",
+					"primaryKey": false,
+					"notNull": true
+				},
+				"created_on": {
+					"name": "created_on",
+					"type": "timestamp with time zone",
+					"primaryKey": false,
+					"notNull": true,
+					"default": "now()"
+				},
+				"oidc_issuer_id": {
+					"name": "oidc_issuer_id",
+					"type": "text",
+					"primaryKey": false,
+					"notNull": false
+				},
+				"paddle_customer_id": {
+					"name": "paddle_customer_id",
+					"type": "text",
+					"primaryKey": false,
+					"notNull": false
+				},
+				"paddle_first_transaction_id": {
+					"name": "paddle_first_transaction_id",
+					"type": "text",
+					"primaryKey": false,
+					"notNull": false
+				},
+				"product_type": {
+					"name": "product_type",
+					"type": "product_type",
+					"typeSchema": "public",
+					"primaryKey": false,
+					"notNull": false
+				},
+				"plan_type": {
+					"name": "plan_type",
+					"type": "plan_type",
+					"typeSchema": "public",
+					"primaryKey": false,
+					"notNull": false
+				},
+				"renew_on": {
+					"name": "renew_on",
+					"type": "date",
+					"primaryKey": false,
+					"notNull": false
+				},
+				"unkey_key_id": {
+					"name": "unkey_key_id",
+					"type": "text",
+					"primaryKey": false,
+					"notNull": false
+				},
+				"ryot_user_id": {
+					"name": "ryot_user_id",
+					"type": "text",
+					"primaryKey": false,
+					"notNull": false
+				}
+			},
+			"indexes": {},
+			"foreignKeys": {},
+			"compositePrimaryKeys": {},
+			"uniqueConstraints": {
+				"customer_email_unique": {
+					"name": "customer_email_unique",
+					"nullsNotDistinct": false,
+					"columns": ["email"]
+				},
+				"customer_oidc_issuer_id_unique": {
+					"name": "customer_oidc_issuer_id_unique",
+					"nullsNotDistinct": false,
+					"columns": ["oidc_issuer_id"]
+				},
+				"customer_paddle_customer_id_unique": {
+					"name": "customer_paddle_customer_id_unique",
+					"nullsNotDistinct": false,
+					"columns": ["paddle_customer_id"]
+				},
+				"customer_paddle_first_transaction_id_unique": {
+					"name": "customer_paddle_first_transaction_id_unique",
+					"nullsNotDistinct": false,
+					"columns": ["paddle_first_transaction_id"]
+				}
+			}
+		}
+	},
+	"enums": {
+		"public.plan_type": {
+			"name": "plan_type",
+			"schema": "public",
+			"values": ["monthly", "yearly", "lifetime"]
+		},
+		"public.product_type": {
+			"name": "product_type",
+			"schema": "public",
+			"values": ["cloud", "self_hosted"]
+		}
+	},
+	"schemas": {},
+	"sequences": {},
+	"_meta": {
+		"columns": {},
+		"schemas": {},
+		"tables": {}
+	}
+}
diff --git a/apps/website/app/drizzle/migrations/meta/0001_snapshot.json b/apps/website/app/drizzle/migrations/meta/0001_snapshot.json
new file mode 100644
index 0000000000..60e5d4668f
--- /dev/null
+++ b/apps/website/app/drizzle/migrations/meta/0001_snapshot.json
@@ -0,0 +1,176 @@
+{
+	"id": "e0646c17-004b-4a17-a8b5-4eb18c2f7637",
+	"prevId": "8eff47f3-d2ea-49ee-a85e-2e4689c410f1",
+	"version": "7",
+	"dialect": "postgresql",
+	"tables": {
+		"public.contact_submission": {
+			"name": "contact_submission",
+			"schema": "",
+			"columns": {
+				"id": {
+					"name": "id",
+					"type": "uuid",
+					"primaryKey": true,
+					"notNull": true,
+					"default": "gen_random_uuid()"
+				},
+				"created_at": {
+					"name": "created_at",
+					"type": "timestamp",
+					"primaryKey": false,
+					"notNull": true,
+					"default": "now()"
+				},
+				"email": {
+					"name": "email",
+					"type": "text",
+					"primaryKey": false,
+					"notNull": true
+				},
+				"message": {
+					"name": "message",
+					"type": "text",
+					"primaryKey": false,
+					"notNull": true
+				},
+				"is_spam": {
+					"name": "is_spam",
+					"type": "boolean",
+					"primaryKey": false,
+					"notNull": false
+				},
+				"is_addressed": {
+					"name": "is_addressed",
+					"type": "boolean",
+					"primaryKey": false,
+					"notNull": false
+				}
+			},
+			"indexes": {},
+			"foreignKeys": {},
+			"compositePrimaryKeys": {},
+			"uniqueConstraints": {}
+		},
+		"public.customer": {
+			"name": "customer",
+			"schema": "",
+			"columns": {
+				"id": {
+					"name": "id",
+					"type": "uuid",
+					"primaryKey": true,
+					"notNull": true,
+					"default": "gen_random_uuid()"
+				},
+				"email": {
+					"name": "email",
+					"type": "text",
+					"primaryKey": false,
+					"notNull": true
+				},
+				"created_on": {
+					"name": "created_on",
+					"type": "timestamp with time zone",
+					"primaryKey": false,
+					"notNull": true,
+					"default": "now()"
+				},
+				"oidc_issuer_id": {
+					"name": "oidc_issuer_id",
+					"type": "text",
+					"primaryKey": false,
+					"notNull": false
+				},
+				"paddle_customer_id": {
+					"name": "paddle_customer_id",
+					"type": "text",
+					"primaryKey": false,
+					"notNull": false
+				},
+				"paddle_first_transaction_id": {
+					"name": "paddle_first_transaction_id",
+					"type": "text",
+					"primaryKey": false,
+					"notNull": false
+				},
+				"product_type": {
+					"name": "product_type",
+					"type": "product_type",
+					"typeSchema": "public",
+					"primaryKey": false,
+					"notNull": false
+				},
+				"plan_type": {
+					"name": "plan_type",
+					"type": "plan_type",
+					"typeSchema": "public",
+					"primaryKey": false,
+					"notNull": false
+				},
+				"renew_on": {
+					"name": "renew_on",
+					"type": "date",
+					"primaryKey": false,
+					"notNull": false
+				},
+				"unkey_key_id": {
+					"name": "unkey_key_id",
+					"type": "text",
+					"primaryKey": false,
+					"notNull": false
+				},
+				"ryot_user_id": {
+					"name": "ryot_user_id",
+					"type": "text",
+					"primaryKey": false,
+					"notNull": false
+				}
+			},
+			"indexes": {},
+			"foreignKeys": {},
+			"compositePrimaryKeys": {},
+			"uniqueConstraints": {
+				"customer_email_unique": {
+					"name": "customer_email_unique",
+					"nullsNotDistinct": false,
+					"columns": ["email"]
+				},
+				"customer_oidc_issuer_id_unique": {
+					"name": "customer_oidc_issuer_id_unique",
+					"nullsNotDistinct": false,
+					"columns": ["oidc_issuer_id"]
+				},
+				"customer_paddle_customer_id_unique": {
+					"name": "customer_paddle_customer_id_unique",
+					"nullsNotDistinct": false,
+					"columns": ["paddle_customer_id"]
+				},
+				"customer_paddle_first_transaction_id_unique": {
+					"name": "customer_paddle_first_transaction_id_unique",
+					"nullsNotDistinct": false,
+					"columns": ["paddle_first_transaction_id"]
+				}
+			}
+		}
+	},
+	"enums": {
+		"public.plan_type": {
+			"name": "plan_type",
+			"schema": "public",
+			"values": ["monthly", "yearly", "lifetime"]
+		},
+		"public.product_type": {
+			"name": "product_type",
+			"schema": "public",
+			"values": ["cloud", "self_hosted"]
+		}
+	},
+	"schemas": {},
+	"sequences": {},
+	"_meta": {
+		"columns": {},
+		"schemas": {},
+		"tables": {}
+	}
+}
diff --git a/apps/website/app/drizzle/migrations/meta/0002_snapshot.json b/apps/website/app/drizzle/migrations/meta/0002_snapshot.json
new file mode 100644
index 0000000000..a17ae5bc01
--- /dev/null
+++ b/apps/website/app/drizzle/migrations/meta/0002_snapshot.json
@@ -0,0 +1,182 @@
+{
+	"id": "706194d8-7177-4603-9a70-49955b7b2c2f",
+	"prevId": "e0646c17-004b-4a17-a8b5-4eb18c2f7637",
+	"version": "7",
+	"dialect": "postgresql",
+	"tables": {
+		"public.contact_submission": {
+			"name": "contact_submission",
+			"schema": "",
+			"columns": {
+				"id": {
+					"name": "id",
+					"type": "uuid",
+					"primaryKey": true,
+					"notNull": true,
+					"default": "gen_random_uuid()"
+				},
+				"created_at": {
+					"name": "created_at",
+					"type": "timestamp",
+					"primaryKey": false,
+					"notNull": true,
+					"default": "now()"
+				},
+				"email": {
+					"name": "email",
+					"type": "text",
+					"primaryKey": false,
+					"notNull": true
+				},
+				"message": {
+					"name": "message",
+					"type": "text",
+					"primaryKey": false,
+					"notNull": true
+				},
+				"is_spam": {
+					"name": "is_spam",
+					"type": "boolean",
+					"primaryKey": false,
+					"notNull": false
+				},
+				"is_addressed": {
+					"name": "is_addressed",
+					"type": "boolean",
+					"primaryKey": false,
+					"notNull": false
+				}
+			},
+			"indexes": {},
+			"foreignKeys": {},
+			"compositePrimaryKeys": {},
+			"uniqueConstraints": {}
+		},
+		"public.customer": {
+			"name": "customer",
+			"schema": "",
+			"columns": {
+				"id": {
+					"name": "id",
+					"type": "uuid",
+					"primaryKey": true,
+					"notNull": true,
+					"default": "gen_random_uuid()"
+				},
+				"email": {
+					"name": "email",
+					"type": "text",
+					"primaryKey": false,
+					"notNull": true
+				},
+				"created_on": {
+					"name": "created_on",
+					"type": "timestamp with time zone",
+					"primaryKey": false,
+					"notNull": true,
+					"default": "now()"
+				},
+				"oidc_issuer_id": {
+					"name": "oidc_issuer_id",
+					"type": "text",
+					"primaryKey": false,
+					"notNull": false
+				},
+				"paddle_customer_id": {
+					"name": "paddle_customer_id",
+					"type": "text",
+					"primaryKey": false,
+					"notNull": false
+				},
+				"paddle_first_transaction_id": {
+					"name": "paddle_first_transaction_id",
+					"type": "text",
+					"primaryKey": false,
+					"notNull": false
+				},
+				"product_type": {
+					"name": "product_type",
+					"type": "product_type",
+					"typeSchema": "public",
+					"primaryKey": false,
+					"notNull": false
+				},
+				"plan_type": {
+					"name": "plan_type",
+					"type": "plan_type",
+					"typeSchema": "public",
+					"primaryKey": false,
+					"notNull": false
+				},
+				"renew_on": {
+					"name": "renew_on",
+					"type": "date",
+					"primaryKey": false,
+					"notNull": false
+				},
+				"unkey_key_id": {
+					"name": "unkey_key_id",
+					"type": "text",
+					"primaryKey": false,
+					"notNull": false
+				},
+				"ryot_user_id": {
+					"name": "ryot_user_id",
+					"type": "text",
+					"primaryKey": false,
+					"notNull": false
+				},
+				"has_cancelled": {
+					"name": "has_cancelled",
+					"type": "boolean",
+					"primaryKey": false,
+					"notNull": false
+				}
+			},
+			"indexes": {},
+			"foreignKeys": {},
+			"compositePrimaryKeys": {},
+			"uniqueConstraints": {
+				"customer_email_unique": {
+					"name": "customer_email_unique",
+					"nullsNotDistinct": false,
+					"columns": ["email"]
+				},
+				"customer_oidc_issuer_id_unique": {
+					"name": "customer_oidc_issuer_id_unique",
+					"nullsNotDistinct": false,
+					"columns": ["oidc_issuer_id"]
+				},
+				"customer_paddle_customer_id_unique": {
+					"name": "customer_paddle_customer_id_unique",
+					"nullsNotDistinct": false,
+					"columns": ["paddle_customer_id"]
+				},
+				"customer_paddle_first_transaction_id_unique": {
+					"name": "customer_paddle_first_transaction_id_unique",
+					"nullsNotDistinct": false,
+					"columns": ["paddle_first_transaction_id"]
+				}
+			}
+		}
+	},
+	"enums": {
+		"public.plan_type": {
+			"name": "plan_type",
+			"schema": "public",
+			"values": ["monthly", "yearly", "lifetime"]
+		},
+		"public.product_type": {
+			"name": "product_type",
+			"schema": "public",
+			"values": ["cloud", "self_hosted"]
+		}
+	},
+	"schemas": {},
+	"sequences": {},
+	"_meta": {
+		"columns": {},
+		"schemas": {},
+		"tables": {}
+	}
+}
diff --git a/apps/website/app/drizzle/migrations/meta/_journal.json b/apps/website/app/drizzle/migrations/meta/_journal.json
new file mode 100644
index 0000000000..47f22e1cc9
--- /dev/null
+++ b/apps/website/app/drizzle/migrations/meta/_journal.json
@@ -0,0 +1,27 @@
+{
+	"version": "7",
+	"dialect": "postgresql",
+	"entries": [
+		{
+			"idx": 0,
+			"version": "7",
+			"when": 1722090857716,
+			"tag": "0000_perfect_reavers",
+			"breakpoints": true
+		},
+		{
+			"idx": 1,
+			"version": "7",
+			"when": 1722314753615,
+			"tag": "0001_next_human_cannonball",
+			"breakpoints": true
+		},
+		{
+			"idx": 2,
+			"version": "7",
+			"when": 1723715178311,
+			"tag": "0002_sad_pixie",
+			"breakpoints": true
+		}
+	]
+}
diff --git a/apps/website/app/drizzle/schema.server.ts b/apps/website/app/drizzle/schema.server.ts
new file mode 100644
index 0000000000..900e28d3a0
--- /dev/null
+++ b/apps/website/app/drizzle/schema.server.ts
@@ -0,0 +1,44 @@
+import {
+	boolean,
+	date,
+	pgEnum,
+	pgTable,
+	text,
+	timestamp,
+	uuid,
+} from "drizzle-orm/pg-core";
+import { z } from "zod";
+
+export const productTypes = pgEnum("product_type", ["cloud", "self_hosted"]);
+
+export const ProductTypes = z.enum(productTypes.enumValues);
+
+export const planTypes = pgEnum("plan_type", ["monthly", "yearly", "lifetime"]);
+
+export const PlanTypes = z.enum(planTypes.enumValues);
+
+export const customers = pgTable("customer", {
+	id: uuid("id").notNull().primaryKey().defaultRandom(),
+	email: text("email").notNull().unique(),
+	createdOn: timestamp("created_on", { withTimezone: true })
+		.defaultNow()
+		.notNull(),
+	oidcIssuerId: text("oidc_issuer_id").unique(),
+	paddleCustomerId: text("paddle_customer_id").unique(),
+	paddleFirstTransactionId: text("paddle_first_transaction_id").unique(),
+	productType: productTypes("product_type"),
+	planType: planTypes("plan_type"),
+	renewOn: date("renew_on"),
+	unkeyKeyId: text("unkey_key_id"),
+	ryotUserId: text("ryot_user_id"),
+	hasCancelled: boolean("has_cancelled"),
+});
+
+export const contactSubmissions = pgTable("contact_submission", {
+	id: uuid("id").defaultRandom().primaryKey(),
+	createdAt: timestamp("created_at").defaultNow().notNull(),
+	email: text("email").notNull(),
+	message: text("message").notNull(),
+	isSpam: boolean("is_spam"),
+	isAddressed: boolean("is_addressed"),
+});
diff --git a/apps/website/app/entry.client.tsx b/apps/website/app/entry.client.tsx
new file mode 100644
index 0000000000..92d15859fe
--- /dev/null
+++ b/apps/website/app/entry.client.tsx
@@ -0,0 +1,12 @@
+import { RemixBrowser } from "@remix-run/react";
+import { StrictMode, startTransition } from "react";
+import { hydrateRoot } from "react-dom/client";
+
+startTransition(() => {
+	hydrateRoot(
+		document,
+		<StrictMode>
+			<RemixBrowser />
+		</StrictMode>,
+	);
+});
diff --git a/apps/website/app/entry.server.tsx b/apps/website/app/entry.server.tsx
new file mode 100644
index 0000000000..1106cf9cd2
--- /dev/null
+++ b/apps/website/app/entry.server.tsx
@@ -0,0 +1,145 @@
+import { PassThrough } from "node:stream";
+import type { AppLoadContext, EntryContext } from "@remix-run/node";
+import { createReadableStreamFromReadable } from "@remix-run/node";
+import { RemixServer } from "@remix-run/react";
+import { drizzle } from "drizzle-orm/postgres-js";
+import { migrate } from "drizzle-orm/postgres-js/migrator";
+import { isbot } from "isbot";
+import postgres from "postgres";
+import { renderToPipeableStream } from "react-dom/server";
+import { serverVariables } from "./lib/config.server";
+
+const migrationClient = postgres(serverVariables.DATABASE_URL, { max: 1 });
+
+migrate(drizzle(migrationClient), {
+	migrationsFolder: "app/drizzle/migrations",
+})
+	.then(() => {
+		migrationClient.end();
+		console.log("Database migrations complete");
+	})
+	.catch((error) => {
+		console.error("Database migrations failed", error);
+		migrationClient.end();
+		process.exit(1);
+	});
+
+const ABORT_DELAY = 5_000;
+
+export default function handleRequest(
+	request: Request,
+	responseStatusCode: number,
+	responseHeaders: Headers,
+	remixContext: EntryContext,
+	_loadContext: AppLoadContext,
+) {
+	return isbot(request.headers.get("user-agent") || "")
+		? handleBotRequest(
+				request,
+				responseStatusCode,
+				responseHeaders,
+				remixContext,
+			)
+		: handleBrowserRequest(
+				request,
+				responseStatusCode,
+				responseHeaders,
+				remixContext,
+			);
+}
+
+function handleBotRequest(
+	request: Request,
+	responseStatusCode: number,
+	responseHeaders: Headers,
+	remixContext: EntryContext,
+) {
+	return new Promise((resolve, reject) => {
+		let shellRendered = false;
+		const { pipe, abort } = renderToPipeableStream(
+			<RemixServer
+				context={remixContext}
+				url={request.url}
+				abortDelay={ABORT_DELAY}
+			/>,
+			{
+				onAllReady() {
+					shellRendered = true;
+					const body = new PassThrough();
+					const stream = createReadableStreamFromReadable(body);
+
+					responseHeaders.set("Content-Type", "text/html");
+
+					resolve(
+						new Response(stream, {
+							headers: responseHeaders,
+							status: responseStatusCode,
+						}),
+					);
+
+					pipe(body);
+				},
+				onShellError(error: unknown) {
+					reject(error);
+				},
+				onError(error: unknown) {
+					// biome-ignore lint/style/noParameterAssign: part of the starter template
+					responseStatusCode = 500;
+					if (shellRendered) {
+						console.error(error);
+					}
+				},
+			},
+		);
+
+		setTimeout(abort, ABORT_DELAY);
+	});
+}
+
+function handleBrowserRequest(
+	request: Request,
+	responseStatusCode: number,
+	responseHeaders: Headers,
+	remixContext: EntryContext,
+) {
+	return new Promise((resolve, reject) => {
+		let shellRendered = false;
+		const { pipe, abort } = renderToPipeableStream(
+			<RemixServer
+				context={remixContext}
+				url={request.url}
+				abortDelay={ABORT_DELAY}
+			/>,
+			{
+				onShellReady() {
+					shellRendered = true;
+					const body = new PassThrough();
+					const stream = createReadableStreamFromReadable(body);
+
+					responseHeaders.set("Content-Type", "text/html");
+
+					resolve(
+						new Response(stream, {
+							headers: responseHeaders,
+							status: responseStatusCode,
+						}),
+					);
+
+					pipe(body);
+				},
+				onShellError(error: unknown) {
+					reject(error);
+				},
+				onError(error: unknown) {
+					// biome-ignore lint/style/noParameterAssign: part of the starter template
+					responseStatusCode = 500;
+					if (shellRendered) {
+						console.error(error);
+					}
+				},
+			},
+		);
+
+		setTimeout(abort, ABORT_DELAY);
+	});
+}
diff --git a/apps/website/app/lib/components/Pricing.tsx b/apps/website/app/lib/components/Pricing.tsx
new file mode 100644
index 0000000000..6118103e8e
--- /dev/null
+++ b/apps/website/app/lib/components/Pricing.tsx
@@ -0,0 +1,124 @@
+import { Link } from "@remix-run/react";
+import { changeCase } from "@ryot/ts-utils";
+import { IconPlayerPlay } from "@tabler/icons-react";
+import { useState } from "react";
+import { $path } from "remix-routes";
+import type { TPrices } from "../config.server";
+import { Button } from "./ui/button";
+import {
+	Tooltip,
+	TooltipContent,
+	TooltipProvider,
+	TooltipTrigger,
+} from "./ui/tooltip";
+
+export default function Pricing(props: {
+	prices: TPrices;
+	isLoggedIn: boolean;
+	onClick?: (priceId: string) => void;
+}) {
+	const [selectedProductTypeIndex, setSelectedProductTypeIndex] = useState(0);
+	const selectedProductType = props.prices[selectedProductTypeIndex];
+
+	return (
+		<section id="pricing" className="w-full py-12 md:py-24 lg:py-32">
+			<div className="container space-y-12 px-4 md:px-6">
+				<div className="flex flex-col items-center justify-center space-y-4 text-center">
+					<div className="space-y-2">
+						<div className="inline-block rounded-lg bg-muted px-3 py-1 text-sm">
+							Pricing
+						</div>
+						<h2 className="text-3xl font-bold tracking-tighter sm:text-5xl">
+							Simple, Transparent Pricing
+						</h2>
+						<p className="max-w-[900px] text-muted-foreground md:text-xl/relaxed lg:text-base/relaxed xl:text-xl/relaxed">
+							Ryot Pro is available in two product types:{" "}
+							<ProductType
+								title="Cloud"
+								description="Hosted on our secure servers, with automatic updates and backups."
+								onClick={() => setSelectedProductTypeIndex(0)}
+							/>{" "}
+							and{" "}
+							<ProductType
+								title="Self Hosted"
+								description="Hosted on your own server, with full control over your data."
+								onClick={() => setSelectedProductTypeIndex(1)}
+							/>
+							. Choose the one that best fits your needs.
+						</p>
+					</div>
+				</div>
+				<div className="space-y-4">
+					<p className="text-center">
+						You have chosen:{" "}
+						<span className="underline">
+							{changeCase(selectedProductType.type)}
+						</span>
+						.{" "}
+						{selectedProductType.type === "self_hosted" ? (
+							<Link
+								to={$path("/comparison-to-community")}
+								className="text-blue-400 underline"
+							>
+								See differences.
+							</Link>
+						) : null}
+					</p>
+					<div className="mx-auto flex justify-center items-center gap-8 text-center flex-wrap">
+						{selectedProductType.prices.map((p) => (
+							<div
+								className="grid gap-y-3 border rounded-xl py-3 w-72"
+								key={p.name}
+							>
+								<p className="text-3xl">{changeCase(p.name)}</p>
+								<p className="text-xl font-bold text-muted-foreground">
+									{p.amount ? `$${p.amount}` : "Community Edition"}
+									{p.trial ? ` with a ${p.trial} days trial` : null}
+								</p>
+								<Link
+									to={
+										p.linkToGithub
+											? "https://docs.ryot.io"
+											: props.isLoggedIn
+												? $path("/me")
+												: "#start-here"
+									}
+									onClick={(e) => {
+										if (props.onClick && p.priceId) {
+											e.preventDefault();
+											props.onClick(p.priceId);
+										}
+									}}
+								>
+									<Button variant="outline" size="sm">
+										<IconPlayerPlay size={16} className="mr-2" />
+										<span>
+											{props.isLoggedIn ? "Choose this" : "Get started"}
+										</span>
+									</Button>
+								</Link>
+							</div>
+						))}
+					</div>
+				</div>
+			</div>
+		</section>
+	);
+}
+
+type ProductTypeProps = {
+	title: string;
+	description: string;
+	onClick: () => void;
+};
+
+const ProductType = ({ title, description, onClick }: ProductTypeProps) => (
+	<TooltipProvider>
+		<Tooltip>
+			<TooltipTrigger className="text-blue-400" onClick={onClick}>
+				{title}
+			</TooltipTrigger>
+			<TooltipContent>{description}</TooltipContent>
+		</Tooltip>
+	</TooltipProvider>
+);
diff --git a/apps/website/app/lib/components/ui/button.tsx b/apps/website/app/lib/components/ui/button.tsx
new file mode 100644
index 0000000000..ba912f2723
--- /dev/null
+++ b/apps/website/app/lib/components/ui/button.tsx
@@ -0,0 +1,55 @@
+import { Slot } from "@radix-ui/react-slot";
+import { type VariantProps, cva } from "class-variance-authority";
+import * as React from "react";
+import { cn } from "~/lib/utils";
+
+const buttonVariants = cva(
+	"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
+	{
+		variants: {
+			variant: {
+				default: "bg-primary text-primary-foreground hover:bg-primary/90",
+				destructive:
+					"bg-destructive text-destructive-foreground hover:bg-destructive/90",
+				outline:
+					"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
+				secondary:
+					"bg-secondary text-secondary-foreground hover:bg-secondary/80",
+				ghost: "hover:bg-accent hover:text-accent-foreground",
+				link: "text-primary underline-offset-4 hover:underline",
+			},
+			size: {
+				default: "h-10 px-4 py-2",
+				sm: "h-9 rounded-md px-3",
+				lg: "h-11 rounded-md px-8",
+				icon: "h-10 w-10",
+			},
+		},
+		defaultVariants: {
+			variant: "default",
+			size: "default",
+		},
+	},
+);
+
+export interface ButtonProps
+	extends React.ButtonHTMLAttributes<HTMLButtonElement>,
+		VariantProps<typeof buttonVariants> {
+	asChild?: boolean;
+}
+
+const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
+	({ className, variant, size, asChild = false, ...props }, ref) => {
+		const Comp = asChild ? Slot : "button";
+		return (
+			<Comp
+				className={cn(buttonVariants({ variant, size, className }))}
+				ref={ref}
+				{...props}
+			/>
+		);
+	},
+);
+Button.displayName = "Button";
+
+export { Button, buttonVariants };
diff --git a/apps/website/app/lib/components/ui/card.tsx b/apps/website/app/lib/components/ui/card.tsx
new file mode 100644
index 0000000000..ea25b83094
--- /dev/null
+++ b/apps/website/app/lib/components/ui/card.tsx
@@ -0,0 +1,86 @@
+import * as React from "react";
+
+import { cn } from "app/lib/utils";
+
+const Card = React.forwardRef<
+	HTMLDivElement,
+	React.HTMLAttributes<HTMLDivElement>
+>(({ className, ...props }, ref) => (
+	<div
+		ref={ref}
+		className={cn(
+			"rounded-lg border bg-card text-card-foreground shadow-sm",
+			className,
+		)}
+		{...props}
+	/>
+));
+Card.displayName = "Card";
+
+const CardHeader = React.forwardRef<
+	HTMLDivElement,
+	React.HTMLAttributes<HTMLDivElement>
+>(({ className, ...props }, ref) => (
+	<div
+		ref={ref}
+		className={cn("flex flex-col space-y-1.5 p-6", className)}
+		{...props}
+	/>
+));
+CardHeader.displayName = "CardHeader";
+
+const CardTitle = React.forwardRef<
+	HTMLParagraphElement,
+	React.HTMLAttributes<HTMLHeadingElement>
+>(({ className, ...props }, ref) => (
+	<h3
+		ref={ref}
+		className={cn(
+			"text-2xl font-semibold leading-none tracking-tight",
+			className,
+		)}
+		{...props}
+	/>
+));
+CardTitle.displayName = "CardTitle";
+
+const CardDescription = React.forwardRef<
+	HTMLParagraphElement,
+	React.HTMLAttributes<HTMLParagraphElement>
+>(({ className, ...props }, ref) => (
+	<p
+		ref={ref}
+		className={cn("text-sm text-muted-foreground", className)}
+		{...props}
+	/>
+));
+CardDescription.displayName = "CardDescription";
+
+const CardContent = React.forwardRef<
+	HTMLDivElement,
+	React.HTMLAttributes<HTMLDivElement>
+>(({ className, ...props }, ref) => (
+	<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
+));
+CardContent.displayName = "CardContent";
+
+const CardFooter = React.forwardRef<
+	HTMLDivElement,
+	React.HTMLAttributes<HTMLDivElement>
+>(({ className, ...props }, ref) => (
+	<div
+		ref={ref}
+		className={cn("flex items-center p-6 pt-0", className)}
+		{...props}
+	/>
+));
+CardFooter.displayName = "CardFooter";
+
+export {
+	Card,
+	CardHeader,
+	CardFooter,
+	CardTitle,
+	CardDescription,
+	CardContent,
+};
diff --git a/apps/website/app/lib/components/ui/carousel.tsx b/apps/website/app/lib/components/ui/carousel.tsx
new file mode 100644
index 0000000000..dffc3cb36e
--- /dev/null
+++ b/apps/website/app/lib/components/ui/carousel.tsx
@@ -0,0 +1,260 @@
+import useEmblaCarousel, {
+	type UseEmblaCarouselType,
+} from "embla-carousel-react";
+import { ArrowLeft, ArrowRight } from "lucide-react";
+import * as React from "react";
+
+import { Button } from "app/lib/components/ui/button";
+import { cn } from "app/lib/utils";
+
+type CarouselApi = UseEmblaCarouselType[1];
+type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
+type CarouselOptions = UseCarouselParameters[0];
+type CarouselPlugin = UseCarouselParameters[1];
+
+type CarouselProps = {
+	opts?: CarouselOptions;
+	plugins?: CarouselPlugin;
+	orientation?: "horizontal" | "vertical";
+	setApi?: (api: CarouselApi) => void;
+};
+
+type CarouselContextProps = {
+	carouselRef: ReturnType<typeof useEmblaCarousel>[0];
+	api: ReturnType<typeof useEmblaCarousel>[1];
+	scrollPrev: () => void;
+	scrollNext: () => void;
+	canScrollPrev: boolean;
+	canScrollNext: boolean;
+} & CarouselProps;
+
+const CarouselContext = React.createContext<CarouselContextProps | null>(null);
+
+function useCarousel() {
+	const context = React.useContext(CarouselContext);
+
+	if (!context) {
+		throw new Error("useCarousel must be used within a <Carousel />");
+	}
+
+	return context;
+}
+
+const Carousel = React.forwardRef<
+	HTMLDivElement,
+	React.HTMLAttributes<HTMLDivElement> & CarouselProps
+>(
+	(
+		{
+			orientation = "horizontal",
+			opts,
+			setApi,
+			plugins,
+			className,
+			children,
+			...props
+		},
+		ref,
+	) => {
+		const [carouselRef, api] = useEmblaCarousel(
+			{
+				...opts,
+				axis: orientation === "horizontal" ? "x" : "y",
+			},
+			plugins,
+		);
+		const [canScrollPrev, setCanScrollPrev] = React.useState(false);
+		const [canScrollNext, setCanScrollNext] = React.useState(false);
+
+		const onSelect = React.useCallback((api: CarouselApi) => {
+			if (!api) {
+				return;
+			}
+
+			setCanScrollPrev(api.canScrollPrev());
+			setCanScrollNext(api.canScrollNext());
+		}, []);
+
+		const scrollPrev = React.useCallback(() => {
+			api?.scrollPrev();
+		}, [api]);
+
+		const scrollNext = React.useCallback(() => {
+			api?.scrollNext();
+		}, [api]);
+
+		const handleKeyDown = React.useCallback(
+			(event: React.KeyboardEvent<HTMLDivElement>) => {
+				if (event.key === "ArrowLeft") {
+					event.preventDefault();
+					scrollPrev();
+				} else if (event.key === "ArrowRight") {
+					event.preventDefault();
+					scrollNext();
+				}
+			},
+			[scrollPrev, scrollNext],
+		);
+
+		React.useEffect(() => {
+			if (!api || !setApi) {
+				return;
+			}
+
+			setApi(api);
+		}, [api, setApi]);
+
+		React.useEffect(() => {
+			if (!api) {
+				return;
+			}
+
+			onSelect(api);
+			api.on("reInit", onSelect);
+			api.on("select", onSelect);
+
+			return () => {
+				api?.off("select", onSelect);
+			};
+		}, [api, onSelect]);
+
+		return (
+			<CarouselContext.Provider
+				value={{
+					carouselRef,
+					api: api,
+					opts,
+					orientation:
+						orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
+					scrollPrev,
+					scrollNext,
+					canScrollPrev,
+					canScrollNext,
+				}}
+			>
+				<div
+					ref={ref}
+					onKeyDownCapture={handleKeyDown}
+					className={cn("relative", className)}
+					role="region"
+					aria-roledescription="carousel"
+					{...props}
+				>
+					{children}
+				</div>
+			</CarouselContext.Provider>
+		);
+	},
+);
+Carousel.displayName = "Carousel";
+
+const CarouselContent = React.forwardRef<
+	HTMLDivElement,
+	React.HTMLAttributes<HTMLDivElement>
+>(({ className, ...props }, ref) => {
+	const { carouselRef, orientation } = useCarousel();
+
+	return (
+		<div ref={carouselRef} className="overflow-hidden">
+			<div
+				ref={ref}
+				className={cn(
+					"flex",
+					orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
+					className,
+				)}
+				{...props}
+			/>
+		</div>
+	);
+});
+CarouselContent.displayName = "CarouselContent";
+
+const CarouselItem = React.forwardRef<
+	HTMLDivElement,
+	React.HTMLAttributes<HTMLDivElement>
+>(({ className, ...props }, ref) => {
+	const { orientation } = useCarousel();
+
+	return (
+		<div
+			ref={ref}
+			role="group"
+			aria-roledescription="slide"
+			className={cn(
+				"min-w-0 shrink-0 grow-0 basis-full",
+				orientation === "horizontal" ? "pl-4" : "pt-4",
+				className,
+			)}
+			{...props}
+		/>
+	);
+});
+CarouselItem.displayName = "CarouselItem";
+
+const CarouselPrevious = React.forwardRef<
+	HTMLButtonElement,
+	React.ComponentProps<typeof Button>
+>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
+	const { orientation, scrollPrev, canScrollPrev } = useCarousel();
+
+	return (
+		<Button
+			ref={ref}
+			variant={variant}
+			size={size}
+			className={cn(
+				"absolute  h-8 w-8 rounded-full",
+				orientation === "horizontal"
+					? "-left-12 top-1/2 -translate-y-1/2"
+					: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
+				className,
+			)}
+			disabled={!canScrollPrev}
+			onClick={scrollPrev}
+			{...props}
+		>
+			<ArrowLeft className="h-4 w-4" />
+			<span className="sr-only">Previous slide</span>
+		</Button>
+	);
+});
+CarouselPrevious.displayName = "CarouselPrevious";
+
+const CarouselNext = React.forwardRef<
+	HTMLButtonElement,
+	React.ComponentProps<typeof Button>
+>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
+	const { orientation, scrollNext, canScrollNext } = useCarousel();
+
+	return (
+		<Button
+			ref={ref}
+			variant={variant}
+			size={size}
+			className={cn(
+				"absolute h-8 w-8 rounded-full",
+				orientation === "horizontal"
+					? "-right-12 top-1/2 -translate-y-1/2"
+					: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
+				className,
+			)}
+			disabled={!canScrollNext}
+			onClick={scrollNext}
+			{...props}
+		>
+			<ArrowRight className="h-4 w-4" />
+			<span className="sr-only">Next slide</span>
+		</Button>
+	);
+});
+CarouselNext.displayName = "CarouselNext";
+
+export {
+	type CarouselApi,
+	Carousel,
+	CarouselContent,
+	CarouselItem,
+	CarouselPrevious,
+	CarouselNext,
+};
diff --git a/apps/website/app/lib/components/ui/input-otp.tsx b/apps/website/app/lib/components/ui/input-otp.tsx
new file mode 100644
index 0000000000..9ab935b92c
--- /dev/null
+++ b/apps/website/app/lib/components/ui/input-otp.tsx
@@ -0,0 +1,76 @@
+import { OTPInput, OTPInputContext } from "input-otp";
+import { Dot } from "lucide-react";
+import * as React from "react";
+
+import { cn } from "app/lib/utils";
+
+const InputOTP = React.forwardRef<
+	React.ElementRef<typeof OTPInput>,
+	React.ComponentPropsWithoutRef<typeof OTPInput>
+>(({ className, containerClassName, ...props }, ref) => (
+	<OTPInput
+		ref={ref}
+		containerClassName={cn(
+			"flex items-center gap-2 has-[:disabled]:opacity-50",
+			containerClassName,
+		)}
+		className={cn("disabled:cursor-not-allowed", className)}
+		{...props}
+	/>
+));
+InputOTP.displayName = "InputOTP";
+
+const InputOTPGroup = React.forwardRef<
+	React.ElementRef<"div">,
+	React.ComponentPropsWithoutRef<"div">
+>(({ className, ...props }, ref) => (
+	<div ref={ref} className={cn("flex items-center", className)} {...props} />
+));
+InputOTPGroup.displayName = "InputOTPGroup";
+
+const InputOTPSlot = React.forwardRef<
+	React.ElementRef<"div">,
+	React.ComponentPropsWithoutRef<"div"> & { index: number }
+>(({ index, className, ...props }, ref) => {
+	const inputOTPContext = React.useContext(OTPInputContext);
+	const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index];
+
+	return (
+		<div
+			ref={ref}
+			className={cn(
+				"relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
+				isActive && "z-10 ring-2 ring-ring ring-offset-background",
+				className,
+			)}
+			{...props}
+		>
+			{char}
+			{hasFakeCaret && (
+				<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
+					<div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
+				</div>
+			)}
+		</div>
+	);
+});
+InputOTPSlot.displayName = "InputOTPSlot";
+
+const InputOTPSeparator = React.forwardRef<
+	React.ElementRef<"div">,
+	React.ComponentPropsWithoutRef<"div">
+>(({ ...props }, ref) => (
+	<div
+		ref={ref}
+		role="separator"
+		aria-valuemax={0}
+		aria-valuemin={0}
+		aria-valuenow={0}
+		{...props}
+	>
+		<Dot />
+	</div>
+));
+InputOTPSeparator.displayName = "InputOTPSeparator";
+
+export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };
diff --git a/apps/website/app/lib/components/ui/input.tsx b/apps/website/app/lib/components/ui/input.tsx
new file mode 100644
index 0000000000..8a5c0fcaf3
--- /dev/null
+++ b/apps/website/app/lib/components/ui/input.tsx
@@ -0,0 +1,25 @@
+import * as React from "react";
+
+import { cn } from "app/lib/utils";
+
+export interface InputProps
+	extends React.InputHTMLAttributes<HTMLInputElement> {}
+
+const Input = React.forwardRef<HTMLInputElement, InputProps>(
+	({ className, type, ...props }, ref) => {
+		return (
+			<input
+				type={type}
+				className={cn(
+					"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
+					className,
+				)}
+				ref={ref}
+				{...props}
+			/>
+		);
+	},
+);
+Input.displayName = "Input";
+
+export { Input };
diff --git a/apps/website/app/lib/components/ui/label.tsx b/apps/website/app/lib/components/ui/label.tsx
new file mode 100644
index 0000000000..b1826a3e78
--- /dev/null
+++ b/apps/website/app/lib/components/ui/label.tsx
@@ -0,0 +1,24 @@
+import * as LabelPrimitive from "@radix-ui/react-label";
+import { type VariantProps, cva } from "class-variance-authority";
+import * as React from "react";
+
+import { cn } from "app/lib/utils";
+
+const labelVariants = cva(
+	"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
+);
+
+const Label = React.forwardRef<
+	React.ElementRef<typeof LabelPrimitive.Root>,
+	React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
+		VariantProps<typeof labelVariants>
+>(({ className, ...props }, ref) => (
+	<LabelPrimitive.Root
+		ref={ref}
+		className={cn(labelVariants(), className)}
+		{...props}
+	/>
+));
+Label.displayName = LabelPrimitive.Root.displayName;
+
+export { Label };
diff --git a/apps/website/app/lib/components/ui/sonner.tsx b/apps/website/app/lib/components/ui/sonner.tsx
new file mode 100644
index 0000000000..0c7e9d4753
--- /dev/null
+++ b/apps/website/app/lib/components/ui/sonner.tsx
@@ -0,0 +1,29 @@
+import { useTheme } from "next-themes";
+import { Toaster as Sonner } from "sonner";
+
+type ToasterProps = React.ComponentProps<typeof Sonner>;
+
+const Toaster = ({ ...props }: ToasterProps) => {
+	const { theme = "system" } = useTheme();
+
+	return (
+		<Sonner
+			theme={theme as ToasterProps["theme"]}
+			className="toaster group"
+			toastOptions={{
+				classNames: {
+					toast:
+						"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
+					description: "group-[.toast]:text-muted-foreground",
+					actionButton:
+						"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
+					cancelButton:
+						"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
+				},
+			}}
+			{...props}
+		/>
+	);
+};
+
+export { Toaster };
diff --git a/apps/website/app/lib/components/ui/textarea.tsx b/apps/website/app/lib/components/ui/textarea.tsx
new file mode 100644
index 0000000000..3d5d49598d
--- /dev/null
+++ b/apps/website/app/lib/components/ui/textarea.tsx
@@ -0,0 +1,24 @@
+import * as React from "react";
+
+import { cn } from "app/lib/utils";
+
+export interface TextareaProps
+	extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
+
+const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
+	({ className, ...props }, ref) => {
+		return (
+			<textarea
+				className={cn(
+					"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
+					className,
+				)}
+				ref={ref}
+				{...props}
+			/>
+		);
+	},
+);
+Textarea.displayName = "Textarea";
+
+export { Textarea };
diff --git a/apps/website/app/lib/components/ui/tooltip.tsx b/apps/website/app/lib/components/ui/tooltip.tsx
new file mode 100644
index 0000000000..9743001a0f
--- /dev/null
+++ b/apps/website/app/lib/components/ui/tooltip.tsx
@@ -0,0 +1,28 @@
+import * as TooltipPrimitive from "@radix-ui/react-tooltip";
+import * as React from "react";
+
+import { cn } from "app/lib/utils";
+
+const TooltipProvider = TooltipPrimitive.Provider;
+
+const Tooltip = TooltipPrimitive.Root;
+
+const TooltipTrigger = TooltipPrimitive.Trigger;
+
+const TooltipContent = React.forwardRef<
+	React.ElementRef<typeof TooltipPrimitive.Content>,
+	React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
+>(({ className, sideOffset = 4, ...props }, ref) => (
+	<TooltipPrimitive.Content
+		ref={ref}
+		sideOffset={sideOffset}
+		className={cn(
+			"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
+			className,
+		)}
+		{...props}
+	/>
+));
+TooltipContent.displayName = TooltipPrimitive.Content.displayName;
+
+export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
diff --git a/apps/website/app/lib/config.server.ts b/apps/website/app/lib/config.server.ts
new file mode 100644
index 0000000000..bfd09686ac
--- /dev/null
+++ b/apps/website/app/lib/config.server.ts
@@ -0,0 +1,141 @@
+import { Environment, Paddle } from "@paddle/paddle-node-sdk";
+import { render } from "@react-email/render";
+import { createCookie } from "@remix-run/node";
+import { drizzle } from "drizzle-orm/postgres-js";
+import { GraphQLClient } from "graphql-request";
+import { createTransport } from "nodemailer";
+import { Issuer } from "openid-client";
+import postgres from "postgres";
+import { Honeypot } from "remix-utils/honeypot/server";
+import { z } from "zod";
+import { zx } from "zodix";
+import * as schema from "~/drizzle/schema.server";
+import { PlanTypes, ProductTypes } from "~/drizzle/schema.server";
+
+// The number of days after a subscription expires that we allow access
+export const GRACE_PERIOD = 7;
+
+export const serverVariablesSchema = z.object({
+	FRONTEND_URL: z.string(),
+	UNKEY_API_ID: z.string(),
+	DATABASE_URL: z.string(),
+	RYOT_BASE_URL: z.string(),
+	UNKEY_ROOT_KEY: z.string(),
+	PADDLE_PRICE_IDS: z.string(),
+	SERVER_SMTP_USER: z.string(),
+	SERVER_SMTP_SERVER: z.string(),
+	PADDLE_CLIENT_TOKEN: z.string(),
+	PADDLE_SERVER_TOKEN: z.string(),
+	SERVER_SMTP_MAILBOX: z.string(),
+	NODE_ENV: z.string().optional(),
+	SERVER_SMTP_PASSWORD: z.string(),
+	SERVER_OIDC_CLIENT_ID: z.string(),
+	SERVER_OIDC_ISSUER_URL: z.string(),
+	SERVER_OIDC_CLIENT_SECRET: z.string(),
+	SERVER_ADMIN_ACCESS_TOKEN: z.string(),
+	PADDLE_SANDBOX: zx.BoolAsString.optional(),
+});
+
+export const serverVariables = serverVariablesSchema.parse(process.env);
+
+export const OAUTH_CALLBACK_URL = `${serverVariables.FRONTEND_URL}/callback`;
+
+export const pricesSchema = z.array(
+	z.object({
+		type: z.string(),
+		prices: z.array(
+			z.object({
+				name: z.string(),
+				linkToGithub: z.boolean().optional(),
+				priceId: z.string().optional(),
+				amount: z.number().optional(),
+				trial: z.number().optional(),
+			}),
+		),
+	}),
+);
+
+export type TPrices = z.infer<typeof pricesSchema>;
+
+export const prices = pricesSchema.parse(
+	JSON.parse(serverVariables.PADDLE_PRICE_IDS),
+);
+
+export const getProductAndPlanTypeByPriceId = (priceId: string) => {
+	for (const product of prices) {
+		for (const price of product.prices) {
+			if (price.priceId === priceId) {
+				return {
+					productType: ProductTypes.parse(product.type),
+					planType: PlanTypes.parse(price.name),
+				};
+			}
+		}
+	}
+	throw new Error("Price ID not found");
+};
+
+export const db = drizzle(postgres(serverVariables.DATABASE_URL), {
+	schema,
+	logger: serverVariables.NODE_ENV === "development",
+});
+
+export const oauthClient = async () => {
+	const issuer = await Issuer.discover(serverVariables.SERVER_OIDC_ISSUER_URL);
+	const client = new issuer.Client({
+		client_id: serverVariables.SERVER_OIDC_CLIENT_ID,
+		client_secret: serverVariables.SERVER_OIDC_CLIENT_SECRET,
+		redirect_uris: [OAUTH_CALLBACK_URL],
+	});
+	return client;
+};
+
+export const getPaddleServerClient = () =>
+	new Paddle(serverVariables.PADDLE_SERVER_TOKEN, {
+		environment: serverVariables.PADDLE_SANDBOX
+			? Environment.sandbox
+			: undefined,
+	});
+
+export const sendEmail = async (
+	recipient: string,
+	subject: string,
+	element: JSX.Element,
+) => {
+	const client = createTransport({
+		host: serverVariables.SERVER_SMTP_SERVER,
+		auth: {
+			user: serverVariables.SERVER_SMTP_USER,
+			pass: serverVariables.SERVER_SMTP_PASSWORD,
+		},
+		secure: true,
+	});
+	const html = render(element, { pretty: true });
+	const text = render(element, { plainText: true });
+	const resp = await client.sendMail({
+		from: '"Ryot" <no-reply@ryot.io>',
+		to: recipient,
+		subject,
+		text,
+		html,
+	});
+	return resp.messageId;
+};
+
+export const authCookie = createCookie("WebsiteAuth", {
+	maxAge: 60 * 60 * 24 * 365,
+	path: "/",
+});
+
+export const getUserIdFromCookie = async (request: Request) => {
+	const cookie = await authCookie.parse(request.headers.get("cookie"));
+	if (!cookie) return null;
+	return z.string().parse(cookie);
+};
+
+export const serverGqlService = new GraphQLClient(
+	`${serverVariables.RYOT_BASE_URL}/graphql`,
+	{ headers: { Connection: "keep-alive" } },
+);
+
+export const honeypot = new Honeypot();
diff --git a/apps/website/app/lib/utils.ts b/apps/website/app/lib/utils.ts
new file mode 100644
index 0000000000..ac680b303c
--- /dev/null
+++ b/apps/website/app/lib/utils.ts
@@ -0,0 +1,6 @@
+import { type ClassValue, clsx } from "clsx";
+import { twMerge } from "tailwind-merge";
+
+export function cn(...inputs: ClassValue[]) {
+	return twMerge(clsx(inputs));
+}
diff --git a/apps/website/app/root.tsx b/apps/website/app/root.tsx
new file mode 100644
index 0000000000..ecdac9ce54
--- /dev/null
+++ b/apps/website/app/root.tsx
@@ -0,0 +1,135 @@
+import {
+	Link,
+	Links,
+	Meta,
+	Outlet,
+	Scripts,
+	ScrollRestoration,
+	useLoaderData,
+} from "@remix-run/react";
+import { HoneypotProvider } from "remix-utils/honeypot/react";
+import "./tailwind.css";
+import {
+	type LinksFunction,
+	type MetaFunction,
+	json,
+	unstable_defineLoader,
+} from "@remix-run/node";
+import { $path } from "remix-routes";
+import { withFragment } from "ufo";
+import { Toaster } from "./lib/components/ui/sonner";
+import { getUserIdFromCookie, honeypot } from "./lib/config.server";
+
+export const links: LinksFunction = () => {
+	return [
+		{
+			rel: "icon",
+			type: "image/png",
+			sizes: "16x16",
+			href: "https://raw.githubusercontent.com/IgnisDa/ryot/main/libs/assets/favicon-16x16.png",
+		},
+		{
+			rel: "icon",
+			type: "image/png",
+			sizes: "32x32",
+			href: "https://raw.githubusercontent.com/IgnisDa/ryot/main/libs/assets/favicon-32x32.png",
+		},
+		{
+			rel: "stylesheet",
+			href: "https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap",
+		},
+	];
+};
+
+export const loader = unstable_defineLoader(async ({ request }) => {
+	const userId = await getUserIdFromCookie(request);
+	return json({
+		isLoggedIn: !!userId,
+		honeypotInputProps: honeypot.getInputProps(),
+	});
+});
+
+export const meta: MetaFunction = () => {
+	return [{ title: "Ryot" }];
+};
+
+export default function App() {
+	const loaderData = useLoaderData<typeof loader>();
+
+	return (
+		<html lang="en" className="scroll-smooth">
+			<head>
+				<meta charSet="utf-8" />
+				<meta name="viewport" content="width=device-width, initial-scale=1" />
+				<Meta />
+				<Links />
+				<script
+					defer
+					data-website-id="65cf9bb3-381b-4ea4-b87d-b40a23a0204f"
+					src="https://umami.diptesh.me/script.js"
+					data-domains="ryot.io,www.ryot.io"
+				/>
+			</head>
+			<body>
+				<Toaster />
+				<div className="flex flex-col min-h-dvh">
+					<header className="px-4 lg:px-6 h-14 flex items-center">
+						<Link to={$path("/")} className="flex items-center justify-center">
+							<img
+								src="https://raw.githubusercontent.com/IgnisDa/ryot/main/libs/assets/icon-512x512.png"
+								alt="Ryot"
+								className="size-10 mr-2"
+							/>
+							<span className="text-xl hidden md:block">Ryot</span>
+						</Link>
+						<nav className="ml-auto flex gap-4 sm:gap-6">
+							<Link
+								to={
+									loaderData.isLoggedIn
+										? $path("/me")
+										: withFragment($path("/"), "start-here")
+								}
+								className="text-sm font-medium hover:underline underline-offset-4"
+							>
+								Your account
+							</Link>
+							<Link
+								to={withFragment($path("/"), "pricing")}
+								className="text-sm font-medium hover:underline underline-offset-4"
+							>
+								Pricing
+							</Link>
+							<a
+								target="_blank"
+								href="https://docs.ryot.io"
+								rel="noopener noreferrer"
+								className="text-sm font-medium hover:underline underline-offset-4 hidden md:block"
+							>
+								Documentation
+							</a>
+							<Link
+								to={withFragment($path("/"), "contact")}
+								className="text-sm font-medium hover:underline underline-offset-4"
+							>
+								Contact Us
+							</Link>
+							<Link
+								to={$path("/terms")}
+								className="text-sm font-medium hover:underline underline-offset-4 hidden md:block"
+							>
+								Terms
+							</Link>
+						</nav>
+					</header>
+					<main className="flex-1">
+						<HoneypotProvider {...loaderData.honeypotInputProps}>
+							<Outlet />
+						</HoneypotProvider>
+					</main>
+				</div>
+				<ScrollRestoration />
+				<Scripts />
+			</body>
+		</html>
+	);
+}
diff --git a/apps/website/app/routes/_index.tsx b/apps/website/app/routes/_index.tsx
new file mode 100644
index 0000000000..dbcbd04acc
--- /dev/null
+++ b/apps/website/app/routes/_index.tsx
@@ -0,0 +1,582 @@
+import TTLCache from "@isaacs/ttlcache";
+import {
+	redirect,
+	unstable_defineAction,
+	unstable_defineLoader,
+} from "@remix-run/node";
+import { Form, Link, useLoaderData } from "@remix-run/react";
+import LoginCodeEmail from "@ryot/transactional/emails/LoginCode";
+import { processSubmission, randomString } from "@ryot/ts-utils";
+import {
+	IconBrandDiscord,
+	IconBrandGithub,
+	IconBrandGoogleFilled,
+	IconPlayerPlay,
+} from "@tabler/icons-react";
+import dayjs from "dayjs";
+import duration from "dayjs/plugin/duration";
+import Autoplay from "embla-carousel-autoplay";
+import { REGEXP_ONLY_DIGITS_AND_CHARS } from "input-otp";
+import { $path } from "remix-routes";
+import { HoneypotInputs } from "remix-utils/honeypot/react";
+import { SpamError } from "remix-utils/honeypot/server";
+import { namedAction } from "remix-utils/named-action";
+import { withFragment, withQuery } from "ufo";
+import { z } from "zod";
+import { zx } from "zodix";
+import { contactSubmissions, customers } from "~/drizzle/schema.server";
+import Pricing from "~/lib/components/Pricing";
+import { Button } from "~/lib/components/ui/button";
+import {
+	Carousel,
+	CarouselContent,
+	CarouselItem,
+	CarouselNext,
+	CarouselPrevious,
+} from "~/lib/components/ui/carousel";
+import { Input } from "~/lib/components/ui/input";
+import {
+	InputOTP,
+	InputOTPGroup,
+	InputOTPSlot,
+} from "~/lib/components/ui/input-otp";
+import { Textarea } from "~/lib/components/ui/textarea";
+import {
+	authCookie,
+	db,
+	getUserIdFromCookie,
+	honeypot,
+	oauthClient,
+	prices,
+	sendEmail,
+} from "~/lib/config.server";
+import { cn } from "~/lib/utils";
+
+dayjs.extend(duration);
+
+const searchParamsSchema = z.object({
+	email: z.string().email().optional(),
+	contactSubmission: zx.BoolAsString.optional(),
+});
+
+export type SearchParams = z.infer<typeof searchParamsSchema>;
+
+export const loader = unstable_defineLoader(async ({ request }) => {
+	const query = zx.parseQuery(request, searchParamsSchema);
+	const userId = await getUserIdFromCookie(request);
+	return { prices, query, isLoggedIn: !!userId };
+});
+
+const otpCodesCache = new TTLCache<string, string>({
+	ttl: dayjs.duration(5, "minutes").asMilliseconds(),
+	max: 1000,
+});
+
+export const action = unstable_defineAction(async ({ request }) => {
+	const formData = await request.clone().formData();
+	return await namedAction(request, {
+		sendLoginCode: async () => {
+			const { email } = processSubmission(formData, emailSchema);
+			const otpCode = randomString(6);
+			otpCodesCache.set(email, otpCode);
+			console.log(`OTP code for ${email} is ${otpCode}`);
+			await sendEmail(
+				email,
+				LoginCodeEmail.subject,
+				LoginCodeEmail({ code: otpCode }),
+			);
+			return redirect(withQuery(withFragment(".", "start-here"), { email }));
+		},
+		registerWithEmail: async () => {
+			const submission = processSubmission(formData, registerSchema);
+			const otpCode = otpCodesCache.get(submission.email);
+			if (otpCode !== submission.otpCode) throw new Error("Invalid OTP code.");
+			const dbCustomer = await db
+				.insert(customers)
+				.values({ email: submission.email })
+				.returning({ id: customers.id })
+				.onConflictDoUpdate({
+					target: customers.email,
+					set: { email: submission.email },
+				});
+			const customerId = dbCustomer.at(0)?.id;
+			if (!customerId)
+				throw new Error("There was an error registering the user.");
+			return redirect($path("/me"), {
+				headers: { "set-cookie": await authCookie.serialize(customerId) },
+			});
+		},
+		registerWithOidc: async () => {
+			const client = await oauthClient();
+			const redirectUrl = client.authorizationUrl({ scope: "openid email" });
+			return redirect(redirectUrl);
+		},
+		contactSubmission: async () => {
+			let isSpam = false;
+			try {
+				honeypot.check(formData);
+			} catch (e) {
+				if (e instanceof SpamError) isSpam = true;
+			}
+			const submission = await zx.parseForm(request, contactSubmissionSchema);
+			await db
+				.insert(contactSubmissions)
+				.values({
+					isSpam: isSpam,
+					email: submission.email,
+					message: submission.message,
+				})
+				.execute();
+			return redirect(
+				withQuery(withFragment(".", "contact"), { contactSubmission: true }),
+			);
+		},
+	});
+});
+
+const intentSchema = z.object({ intent: z.string() });
+
+const emailSchema = z.object({ email: z.string().email() }).merge(intentSchema);
+
+const registerSchema = z
+	.object({ otpCode: z.string().length(6) })
+	.merge(emailSchema);
+
+const contactSubmissionSchema = z.object({
+	email: z.string().email(),
+	message: z.string(),
+});
+
+export default function Page() {
+	const loaderData = useLoaderData<typeof loader>();
+
+	return (
+		<>
+			<section id="hero" className="w-full py-12 md:py-24 lg:py-32 border-y">
+				<div className="container space-y-10 xl:space-y-16">
+					<div className="grid max-w-[1300px] mx-auto gap-4 sm:px-6 md:px-10 md:grid-cols-2 md:gap-16">
+						<div className="space-y-8">
+							<h1 className="lg:leading-tighter text-3xl font-bold tracking-tighter sm:text-4xl md:text-5xl xl:text-[3.4rem] 2xl:text-[3.75rem]">
+								Track Your Life, Your Way with Ryot
+							</h1>
+							<p className="mx-auto max-w-[700px] text-muted-foreground md:text-xl">
+								Ryot is a versatile platform that helps you effortlessly track
+								and manage your media, fitness, and more. Say goodbye to manual
+								tracking methods like Notion, Google Notes, and Excel.
+							</p>
+							<div className="space-x-4 mt-6">
+								<Link
+									to="#start-here"
+									className="inline-flex h-9 items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
+								>
+									Start Your Free Trial
+								</Link>
+								<a
+									href={demoLink}
+									target="_blank"
+									rel="noopener noreferrer"
+									className="inline-flex h-9 items-center justify-center rounded-md border border-input bg-background px-4 py-2 text-sm font-medium shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
+								>
+									Try a demo
+								</a>
+							</div>
+						</div>
+						<Image
+							src="/cta-image.png"
+							alt="CTA image"
+							className="hidden md:block"
+						/>
+					</div>
+				</div>
+			</section>
+			<section id="advantages" className="w-full py-12 md:py-24 lg:py-32">
+				<div className="container space-y-12 px-4 md:px-6">
+					<div className="flex flex-col items-center justify-center space-y-4 text-center">
+						<div className="space-y-2">
+							<div className="inline-block rounded-lg bg-muted px-3 py-1 text-sm">
+								Effortless Tracking
+							</div>
+							<h2 className="text-3xl font-bold tracking-tighter sm:text-5xl">
+								Ditch the Spreadsheets, Embrace Ryot
+							</h2>
+							<p className="max-w-[900px] text-muted-foreground md:text-xl/relaxed lg:text-base/relaxed xl:text-xl/relaxed">
+								Ryot's intuitive interface and seamless data integration make it
+								a breeze to track your media, fitness, and more. No more manual
+								data entry or juggling multiple apps.
+							</p>
+						</div>
+					</div>
+					<div className="mx-auto grid items-start gap-8 sm:max-w-4xl sm:grid-cols-2 md:gap-12 lg:max-w-5xl lg:grid-cols-3">
+						{advantages.map(({ title, description }) => (
+							<div className="grid gap-1" key={title}>
+								<h3 className="text-lg font-bold">{title}</h3>
+								<p className="text-sm text-muted-foreground">{description}</p>
+							</div>
+						))}
+					</div>
+					<div className="flex justify-center flex-col sm:flex-row items-start gap-4">
+						<Link
+							to="#start-here"
+							className="inline-flex h-10 items-center justify-center rounded-md bg-primary px-8 text-sm font-medium text-primary-foreground shadow transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
+						>
+							Start Your Free Trial
+						</Link>
+					</div>
+				</div>
+			</section>
+			<section
+				id="showcase"
+				className="w-full py-12 md:py-24 lg:py-32 bg-muted"
+			>
+				<div className="container space-y-12 px-4 md:px-6">
+					<div className="flex flex-col items-center justify-center space-y-4 text-center">
+						<div className="space-y-2">
+							<div className="inline-block rounded-lg bg-muted px-3 py-1 text-sm bg-white">
+								Ease of Use
+							</div>
+							<h1 className="lg:leading-tighter text-3xl font-bold tracking-tighter sm:text-4xl md:text-5xl xl:text-[3.4rem] 2xl:text-[3.75rem]">
+								Focus on What Matters
+							</h1>
+						</div>
+					</div>
+					<Carousel plugins={[Autoplay({ delay: 5000 })]}>
+						<CarouselContent>
+							{showCarouselContents.map((carousel) => (
+								<CarouselItem key={carousel.text} className="flex flex-col">
+									<p className="mx-auto max-w-[900px] text-muted-foreground md:text-xl/relaxed lg:text-base/relaxed xl:text-xl/relaxed">
+										{carousel.text}
+									</p>
+									<div className="my-auto">
+										<Image
+											src={carousel.image}
+											alt={carousel.alt}
+											className={cn(
+												carousel.smallImage && "hidden md:block",
+												"max-h-96 md:max-w-3xl lg:max-w-4xl xl:max-w-6xl",
+											)}
+										/>
+										{carousel.smallImage ? (
+											<Image
+												src={carousel.smallImage}
+												alt={carousel.alt}
+												className="md:hidden"
+											/>
+										) : null}
+									</div>
+								</CarouselItem>
+							))}
+						</CarouselContent>
+						<div className="hidden md:block">
+							<CarouselPrevious />
+							<CarouselNext />
+						</div>
+					</Carousel>
+				</div>
+			</section>
+			<section id="start-here" className="w-full py-12 md:py-24 lg:py-32">
+				<div className="container grid items-center justify-center gap-4 px-4 text-center md:px-6">
+					<div className="space-y-3">
+						<h2 className="text-3xl font-bold tracking-tighter md:text-4xl/tight">
+							Upgrade Your Tracking Experience
+						</h2>
+						<p className="mx-auto max-w-[600px] text-muted-foreground md:text-xl/relaxed lg:text-base/relaxed xl:text-xl/relaxed">
+							Ryot's powerful features make it the ultimate solution for
+							effortless tracking and data management.
+						</p>
+					</div>
+					<div className="mx-auto w-full max-w-sm space-y-2">
+						{loaderData.isLoggedIn ? (
+							<Link to={$path("/me")}>
+								<Button>
+									<IconPlayerPlay size={16} className="mr-2" />
+									<span>Get started</span>
+								</Button>
+							</Link>
+						) : (
+							<>
+								<Form method="POST" className="flex gap-2 flex-none">
+									<input
+										readOnly
+										type="hidden"
+										name="intent"
+										value={
+											loaderData.query.email
+												? "registerWithEmail"
+												: "sendLoginCode"
+										}
+									/>
+									{loaderData.query.email ? (
+										<>
+											<input
+												readOnly
+												name="email"
+												type="hidden"
+												value={loaderData.query.email}
+											/>
+											<InputOTP
+												maxLength={6}
+												pattern={REGEXP_ONLY_DIGITS_AND_CHARS}
+												name="otpCode"
+											>
+												<InputOTPGroup>
+													<InputOTPSlot index={0} />
+													<InputOTPSlot index={1} />
+													<InputOTPSlot index={2} />
+													<InputOTPSlot index={3} />
+													<InputOTPSlot index={4} />
+													<InputOTPSlot index={5} />
+												</InputOTPGroup>
+											</InputOTP>
+										</>
+									) : (
+										<Input
+											type="email"
+											name="email"
+											placeholder="Enter your email"
+											className="max-w-lg flex-1"
+										/>
+									)}
+									<Button type="submit">
+										{loaderData.query.email
+											? "Verify login code"
+											: "Start Your Free Trial"}
+									</Button>
+								</Form>
+								<p className="text-xs">OR</p>
+								<Form method="POST">
+									<input
+										type="hidden"
+										name="intent"
+										defaultValue="registerWithOidc"
+									/>
+									<Button
+										variant="outline"
+										className="inline-flex h-10 items-center justify-center rounded-md px-8 text-sm font-medium"
+									>
+										<IconBrandGoogleFilled className="mr-2 h-4 w-4" />
+										Sign in with Google
+									</Button>
+								</Form>
+								<p className="text-xs text-muted-foreground">
+									Sign up to get started with Ryot.{" "}
+									<Link
+										to={$path("/terms")}
+										className="underline underline-offset-2"
+									>
+										Terms &amp; Conditions
+									</Link>
+								</p>
+							</>
+						)}
+					</div>
+				</div>
+			</section>
+			<section
+				id="testimonials"
+				className="w-full py-12 md:py-24 lg:py-32 bg-muted"
+			>
+				<div className="container grid items-center justify-center gap-4 px-4 text-center md:px-6 lg:gap-10">
+					<div className="space-y-3">
+						<h2 className="text-3xl font-bold tracking-tighter sm:text-4xl md:text-5xl">
+							Trusted by Thousands
+						</h2>
+						<p className="mx-auto max-w-[700px] text-muted-foreground md:text-xl/relaxed lg:text-base/relaxed xl:text-xl/relaxed">
+							Ryot is trusted by thousands of users worldwide to track their
+							media, fitness, and more. Join them and upgrade your tracking
+							experience today.
+						</p>
+					</div>
+					<div className="divide-y rounded-lg border">
+						<div className="mx-auto md:flex w-full items-center justify-center p-4 sm:p-8 space-y-4 gap-x-4">
+							<Image
+								src="https://cdn.fosstodon.org/accounts/avatars/110/575/489/884/720/641/original/7312eb6f27068401.jpeg"
+								alt="Fosstodon logo"
+								className="size-20 rounded-full flex-none"
+							/>
+							<p>
+								I love how easy it is to quickly add a game, book, movie or show
+								after I'm finished and write a short review. It's probably the
+								most used software on my home server, and has greatly encouraged
+								me to watch more movies! IgnisDa has put a great amount of
+								attention to detail into this software, and is always available
+								for help any step of the way! -{" "}
+								<a
+									href="https://fosstodon.org/@beppi"
+									target="_blank"
+									rel="noopener noreferrer"
+									className="hover:underline"
+								>
+									<strong>@beppi</strong>
+								</a>
+							</p>
+						</div>
+						<div className="grid w-full grid-cols-3 items-stretch justify-center divide" />
+					</div>
+				</div>
+			</section>
+			<Pricing prices={loaderData.prices} isLoggedIn={loaderData.isLoggedIn} />
+			<section id="contact" className="w-full py-12 md:py-24 lg:py-32 bg-muted">
+				<div className="container px-4 md:px-6">
+					<div className="flex flex-col items-center justify-center space-y-4 text-center">
+						<div className="space-y-2">
+							<div className="inline-block rounded-lg px-3 py-1 text-sm bg-white">
+								Contact Us
+							</div>
+							<h2 className="text-3xl font-bold tracking-tighter sm:text-4xl md:text-5xl">
+								Get in Touch
+							</h2>
+							<p className="max-w-[900px] text-muted-foreground md:text-xl/relaxed lg:text-base/relaxed xl:text-xl/relaxed">
+								Have a question or need help with Ryot? Reach out to us and
+								we'll get back to you as soon as possible. Alternatively, join
+								the Discord community to chat with other users.
+							</p>
+						</div>
+					</div>
+					{loaderData.query.contactSubmission ? (
+						<p className="text-center text-lg text-primary mt-10">
+							Your message has been submitted. We'll get back to you soon!
+						</p>
+					) : (
+						<Form
+							method="POST"
+							className="flex flex-col items-center justify-center pt-12 gap-y-4 gap-x-4"
+						>
+							<input
+								type="hidden"
+								name="intent"
+								defaultValue="contactSubmission"
+							/>
+							<HoneypotInputs />
+							<Input
+								type="email"
+								name="email"
+								placeholder="Email"
+								className="max-w-lg"
+								required
+							/>
+							<Textarea
+								name="message"
+								placeholder="Type your message here"
+								className="max-w-lg"
+								required
+							/>
+							<Button type="submit">Submit</Button>
+						</Form>
+					)}
+				</div>
+			</section>
+			<section id="community" className="w-full py-12 md:py-24 lg:py-32">
+				<div className="container px-4 md:px-6">
+					<div className="flex flex-col items-center justify-center space-y-4 text-center">
+						<div className="space-y-2">
+							<div className="inline-block rounded-lg px-3 py-1 text-sm bg-muted">
+								Open Source
+							</div>
+							<h2 className="text-3xl font-bold tracking-tighter sm:text-5xl">
+								Be Part of the Community
+							</h2>
+							<p className="max-w-[900px] text-muted-foreground md:text-xl/relaxed lg:text-base/relaxed xl:text-xl/relaxed">
+								The community version of Ryot is open source and is available
+								for free. Contribute to the project and help shape the future of
+								tracking.
+							</p>
+						</div>
+					</div>
+					<div className="flex flex-col sm:flex-row items-center justify-center pt-12 gap-y-4 gap-x-4">
+						<a
+							href="https://discord.gg/D9XTg2a7R8"
+							target="_blank"
+							rel="noopener noreferrer"
+						>
+							<Button>
+								<IconBrandDiscord className="mr-2" />
+								<span>Join the Discord</span>
+							</Button>
+						</a>
+						<a
+							href="https://github.com/IgnisDa/ryot"
+							target="_blank"
+							rel="noopener noreferrer"
+						>
+							<Button variant="outline">
+								<IconBrandGithub className="mr-2" />
+								<span>Explore the Code</span>
+							</Button>
+						</a>
+					</div>
+				</div>
+			</section>
+		</>
+	);
+}
+
+const demoLink = "https://pro.ryot.io/_s/acl_vUMPnPirkHlT";
+
+const advantages = [
+	{
+		title: "Ditch the Spreadsheets",
+		description:
+			"Say goodbye to the hassle of manual data entry in Notion, Excel or Google Sheets. Ryot automates the process, giving you more time to focus on what matters.",
+	},
+	{
+		title: "Seamless Data Integration",
+		description:
+			"Ryot seamlessly integrates with your favorite apps and services, allowing you to centralize all your data in one place.",
+	},
+	{
+		title: "Advanced Analytics",
+		description:
+			"Gain valuable insights with Ryot's advanced analytics features. Visualize your data, identify trends, and make informed decisions.",
+	},
+	{
+		title: "Stay informed",
+		description:
+			"Ryot keeps you informed with timely notifications and reminders. Never miss your favorite show's new episode or actor's latest release.",
+	},
+	{
+		title: "Customizable",
+		description:
+			"Ryot is highly customizable and adapts to your unique tracking needs. Enable or disable facets to tailor the platform to your preferences.",
+	},
+	{
+		title: "Multi Platform",
+		description:
+			"Ryot is available on all platforms, including web, mobile, and desktop. Access your data anytime, anywhere.",
+	},
+];
+
+type ImageProps = {
+	src: string;
+	alt: string;
+	className: string;
+};
+
+const Image = ({ src, alt, className }: ImageProps) => (
+	<img
+		src={src}
+		alt={alt}
+		className={cn(
+			className,
+			"mx-auto aspect-[16/9] overflow-hidden rounded-xl object-cover",
+		)}
+	/>
+);
+
+const showCarouselContents = [
+	{
+		text: "You can selectively enable or disable the facets you want to track. Spend less time learning the platform and more time tracking what matters to you. Get started in minutes.",
+		image: "/group.png",
+		smallImage: "/desktop.png",
+		alt: "Grouped images",
+	},
+	{
+		text: "Share your profile data with friends and family without compromising your privacy. Ryot allows you to create access links with limited access so that others can view your favorite movies without logging in.",
+		image: "/sharing.png",
+		alt: "Sharing images",
+	},
+	{
+		text: "Browse your favorite genres and get recommendations based on your preferences. Ryot uses advanced algorithms to suggest movies, shows, and books you might like.",
+		image: "/genres.png",
+		alt: "Genres images",
+	},
+];
diff --git a/apps/website/app/routes/auth.tsx b/apps/website/app/routes/auth.tsx
new file mode 100644
index 0000000000..37b5642bcf
--- /dev/null
+++ b/apps/website/app/routes/auth.tsx
@@ -0,0 +1,154 @@
+import TTLCache from "@isaacs/ttlcache";
+import {
+	redirect,
+	unstable_defineAction,
+	unstable_defineLoader,
+} from "@remix-run/node";
+import { Form, useLoaderData } from "@remix-run/react";
+import LoginCodeEmail from "@ryot/transactional/emails/LoginCode";
+import { processSubmission, randomString } from "@ryot/ts-utils";
+import dayjs from "dayjs";
+import duration from "dayjs/plugin/duration";
+import { REGEXP_ONLY_DIGITS_AND_CHARS } from "input-otp";
+import { $path } from "remix-routes";
+import { namedAction } from "remix-utils/named-action";
+import { withQuery } from "ufo";
+import { z } from "zod";
+import { zx } from "zodix";
+import { customers } from "~/drizzle/schema.server";
+import { Button } from "~/lib/components/ui/button";
+import { Input } from "~/lib/components/ui/input";
+import {
+	InputOTP,
+	InputOTPGroup,
+	InputOTPSlot,
+} from "~/lib/components/ui/input-otp";
+import { Label } from "~/lib/components/ui/label";
+import { authCookie, db, oauthClient, sendEmail } from "~/lib/config.server";
+
+dayjs.extend(duration);
+
+const searchParamsSchema = z.object({
+	email: z.string().email().optional(),
+});
+
+export type SearchParams = z.infer<typeof searchParamsSchema>;
+
+const otpCodesCache = new TTLCache<string, string>({
+	ttl: dayjs.duration(5, "minutes").asMilliseconds(),
+	max: 1000,
+});
+
+export const loader = unstable_defineLoader(async ({ request }) => {
+	const query = zx.parseQuery(request, searchParamsSchema);
+	return { query };
+});
+
+export const action = unstable_defineAction(async ({ request }) => {
+	const formData = await request.clone().formData();
+	return await namedAction(request, {
+		sendLoginCode: async () => {
+			const { email } = processSubmission(formData, emailSchema);
+			const otpCode = randomString(6);
+			otpCodesCache.set(email, otpCode);
+			await sendEmail(
+				email,
+				LoginCodeEmail.subject,
+				LoginCodeEmail({ code: otpCode }),
+			);
+			return redirect(withQuery(".", { email }));
+		},
+		registerWithEmail: async () => {
+			const submission = processSubmission(formData, registerSchema);
+			const otpCode = otpCodesCache.get(submission.email);
+			if (otpCode !== submission.otpCode) throw new Error("Invalid OTP code.");
+			const dbCustomer = await db
+				.insert(customers)
+				.values({ email: submission.email })
+				.returning({ id: customers.id })
+				.onConflictDoUpdate({
+					target: customers.email,
+					set: { email: submission.email },
+				});
+			const customerId = dbCustomer.at(0)?.id;
+			if (!customerId)
+				throw new Error("There was an error registering the user.");
+			return redirect($path("/"), {
+				headers: { "set-cookie": await authCookie.serialize(customerId) },
+			});
+		},
+		registerWithOidc: async () => {
+			const client = await oauthClient();
+			const redirectUrl = client.authorizationUrl({ scope: "openid email" });
+			return redirect(redirectUrl);
+		},
+	});
+});
+
+const emailSchema = z.object({
+	email: z.string().email(),
+});
+
+const registerSchema = z
+	.object({ otpCode: z.string().length(6) })
+	.merge(emailSchema);
+
+export default function Index() {
+	const loaderData = useLoaderData<typeof loader>();
+
+	return (
+		<div>
+			<Form
+				method="POST"
+				action={withQuery(".", {
+					intent: loaderData.query.email
+						? "registerWithEmail"
+						: "sendLoginCode",
+				})}
+				className="rounded-2xl border space-y-4 m-4 p-4"
+			>
+				<div>
+					<Label htmlFor="email">
+						{loaderData.query.email ? "Enter the code" : "Email"}
+					</Label>
+					{loaderData.query.email ? (
+						<>
+							<input
+								readOnly
+								name="email"
+								type="hidden"
+								value={loaderData.query.email}
+							/>
+							<InputOTP
+								maxLength={6}
+								pattern={REGEXP_ONLY_DIGITS_AND_CHARS}
+								name="otpCode"
+							>
+								<InputOTPGroup>
+									<InputOTPSlot index={0} />
+									<InputOTPSlot index={1} />
+									<InputOTPSlot index={2} />
+									<InputOTPSlot index={3} />
+									<InputOTPSlot index={4} />
+									<InputOTPSlot index={5} />
+								</InputOTPGroup>
+							</InputOTP>
+						</>
+					) : (
+						<Input id="email" name="email" type="email" />
+					)}
+				</div>
+				<Button type="submit">
+					{loaderData.query.email ? "Register" : "Send Login Code"}
+				</Button>
+			</Form>
+			<Form
+				method="POST"
+				action={withQuery("?index", { intent: "registerWithOidc" })}
+				className="rounded-2xl border space-y-4 m-4 p-4"
+			>
+				<Button type="submit">Sign up with Google</Button>
+			</Form>
+		</div>
+	);
+}
diff --git a/apps/website/app/routes/callback.tsx b/apps/website/app/routes/callback.tsx
new file mode 100644
index 0000000000..6298bf5b0b
--- /dev/null
+++ b/apps/website/app/routes/callback.tsx
@@ -0,0 +1,42 @@
+import { redirect, unstable_defineLoader } from "@remix-run/node";
+import { eq } from "drizzle-orm";
+import { $path } from "remix-routes";
+import { match } from "ts-pattern";
+import { customers } from "~/drizzle/schema.server";
+import {
+	OAUTH_CALLBACK_URL,
+	authCookie,
+	db,
+	oauthClient,
+} from "~/lib/config.server";
+
+export const loader = unstable_defineLoader(async ({ request }) => {
+	const client = await oauthClient();
+	const params = client.callbackParams(request.url);
+	const tokenSet = await client.callback(OAUTH_CALLBACK_URL, params, {
+		state: params.state,
+	});
+	const claims = tokenSet.claims();
+	const email = claims.email;
+	if (!email || !claims.sub) throw new Error("Invalid claims");
+	const alreadyCustomer = await db.query.customers.findFirst({
+		where: eq(customers.email, email),
+	});
+	const customerId = await match(alreadyCustomer)
+		.with(undefined, async () => {
+			const dbCustomer = await db
+				.insert(customers)
+				.values({ email, oidcIssuerId: claims.sub })
+				.returning({ id: customers.id })
+				.onConflictDoUpdate({
+					target: customers.oidcIssuerId,
+					set: { oidcIssuerId: claims.sub },
+				});
+			return dbCustomer.at(0)?.id;
+		})
+		.otherwise((value) => value.id);
+	if (!customerId) throw new Error("There was an error registering the user.");
+	return redirect($path("/me"), {
+		headers: { "set-cookie": await authCookie.serialize(customerId) },
+	});
+});
diff --git a/apps/website/app/routes/comparison-to-community.tsx b/apps/website/app/routes/comparison-to-community.tsx
new file mode 100644
index 0000000000..86197617fc
--- /dev/null
+++ b/apps/website/app/routes/comparison-to-community.tsx
@@ -0,0 +1,97 @@
+import Autoplay from "embla-carousel-autoplay";
+import {
+	Carousel,
+	CarouselContent,
+	CarouselItem,
+	CarouselNext,
+	CarouselPrevious,
+} from "~/lib/components/ui/carousel";
+import { cn } from "~/lib/utils";
+
+export default function Page() {
+	return (
+		<section
+			id="comparison"
+			className="w-full py-12 md:py-24 lg:py-32 bg-muted"
+		>
+			<div className="container space-y-4 px-4 md:px-6 text-center">
+				<div className="space-y-2">
+					<div className="inline-block rounded-lg bg-muted px-3 py-1 text-sm bg-white">
+						Comparison
+					</div>
+				</div>
+				<Carousel plugins={[Autoplay({ delay: 5000 })]}>
+					<CarouselContent>
+						{features.map((feature) => (
+							<CarouselItem
+								key={feature.name}
+								className="flex flex-col space-y-4"
+							>
+								<h1 className="lg:leading-tighter text-3xl font-bold tracking-tighter sm:text-4xl md:text-5xl xl:text-[3.4rem] 2xl:text-[3.75rem]">
+									{feature.name}
+								</h1>
+								<p className="mx-auto max-w-[900px] text-muted-foreground md:text-xl/relaxed lg:text-base/relaxed xl:text-xl/relaxed">
+									{feature.points.join(" ")}
+								</p>
+								<div className="my-auto flex items-center justify-center">
+									<img
+										src={feature.image}
+										alt={feature.name}
+										className={cn(
+											"rounded-xl max-h-96 md:max-w-3xl lg:max-w-4xl xl:max-w-6xl",
+											!feature.noBorder && "border rounded-lg shadow-lg",
+										)}
+									/>
+								</div>
+							</CarouselItem>
+						))}
+					</CarouselContent>
+					<div className="hidden md:block">
+						<CarouselPrevious />
+						<CarouselNext />
+					</div>
+				</Carousel>
+			</div>
+		</section>
+	);
+}
+
+const features = [
+	{
+		name: "Sharing",
+		image: "/sharing-form.png",
+		points: [
+			"Share your profile with your friends and family.",
+			"Set an expiry (or don't!) and revoke access at any time.",
+			"Anonymous users can only view your data, not change it.",
+		],
+	},
+	{
+		name: "Recommendations",
+		image: "/recommendations.png",
+		points: [
+			'A new section called "Recommendations" is added to the Dashboard where you can view personalized suggestions based on your recent media consumption. They are refreshed every hour.',
+		],
+	},
+	{
+		name: "Supercharged Collections",
+		image: "/supercharged-collections.png",
+		noBorder: true,
+		points: [
+			'Create information templates for your collections. These templates will be used when you add media to your collection. You can see an example of this on the community edition with the "Owned" and "Reminders" collection.',
+			"Add collaborators to your collections. This feature is useful when you want to share your collection with your friends or family. They can also add or remove media from it.",
+		],
+	},
+	{
+		name: "Other Enhancements",
+		image: "/other-enhancements.png",
+		points: [
+			"Create templates to schedule future workouts.",
+			'Option to automatically sync media items from integrations to the "Owned" collection.',
+			'Easier navigation for the "History" tab in media details to your favorite episode.',
+			"Easily view history for an exercise and copy it to your current workout.",
+			"Add notes to individual sets in a workout.",
+			"Set time spent manually on seen entries, allowing for more accurate tracking of media consumption.",
+		],
+	},
+];
diff --git a/apps/website/app/routes/me.tsx b/apps/website/app/routes/me.tsx
new file mode 100644
index 0000000000..c5196241c8
--- /dev/null
+++ b/apps/website/app/routes/me.tsx
@@ -0,0 +1,266 @@
+import {
+	CheckoutEventNames,
+	type Paddle,
+	initializePaddle,
+} from "@paddle/paddle-js";
+import { unstable_defineAction, unstable_defineLoader } from "@remix-run/node";
+import { Form, redirect, useLoaderData, useSubmit } from "@remix-run/react";
+import { RegisterUserDocument } from "@ryot/generated/graphql/backend/graphql";
+import PurchaseCompleteEmail from "@ryot/transactional/emails/PurchaseComplete";
+import { changeCase, randomString } from "@ryot/ts-utils";
+import { Unkey } from "@unkey/api";
+import dayjs from "dayjs";
+import { eq } from "drizzle-orm";
+import { useEffect, useState } from "react";
+import { $path } from "remix-routes";
+import { namedAction } from "remix-utils/named-action";
+import { P, match } from "ts-pattern";
+import { withFragment, withQuery } from "ufo";
+import { customers } from "~/drizzle/schema.server";
+import Pricing from "~/lib/components/Pricing";
+import { Button } from "~/lib/components/ui/button";
+import { Card } from "~/lib/components/ui/card";
+import { Label } from "~/lib/components/ui/label";
+import {
+	GRACE_PERIOD,
+	authCookie,
+	db,
+	getPaddleServerClient,
+	getProductAndPlanTypeByPriceId,
+	getUserIdFromCookie,
+	prices,
+	sendEmail,
+	serverGqlService,
+	serverVariables,
+} from "~/lib/config.server";
+
+export const loader = unstable_defineLoader(async ({ request }) => {
+	const userId = await getUserIdFromCookie(request);
+	const isLoggedIn = !!userId;
+	if (!isLoggedIn) return redirect(withFragment($path("/"), "start-here"));
+	const planDetails = await match(userId)
+		.with(P.string, async (userId) => {
+			const customer = await db.query.customers.findFirst({
+				where: eq(customers.id, userId),
+				columns: {
+					email: true,
+					renewOn: true,
+					planType: true,
+					productType: true,
+					unkeyKeyId: true,
+					ryotUserId: true,
+				},
+			});
+			if (!customer) return undefined;
+			return customer;
+		})
+		.otherwise(() => undefined);
+	return {
+		prices,
+		planDetails,
+		isSandbox: !!serverVariables.PADDLE_SANDBOX,
+		clientToken: serverVariables.PADDLE_CLIENT_TOKEN,
+	};
+});
+
+export const action = unstable_defineAction(async ({ request }) => {
+	return await namedAction(request.clone(), {
+		logout: async () => {
+			const cookies = await authCookie.serialize("", { expires: new Date(0) });
+			return Response.json({}, { headers: { "set-cookie": cookies } });
+		},
+		processPurchase: async () => {
+			const userId = await getUserIdFromCookie(request);
+			if (!userId)
+				throw new Error("You must be logged in to buy a subscription");
+			const { transactionId }: { transactionId: string } = await request.json();
+			const paddleClient = getPaddleServerClient();
+			const transaction = await paddleClient.transactions.get(transactionId);
+			const priceId = transaction.details?.lineItems[0].priceId;
+			if (!priceId) throw new Error("Price ID not found");
+			const paddleCustomerId = transaction.customerId;
+			if (!paddleCustomerId) throw new Error("Paddle customer ID not found");
+			const customer = await db.query.customers.findFirst({
+				where: eq(customers.id, userId),
+			});
+			if (!customer) throw new Error("Customer not found");
+			const { email, oidcIssuerId } = customer;
+			const { planType, productType } = getProductAndPlanTypeByPriceId(priceId);
+			const renewOn = match(planType)
+				.with("lifetime", () => undefined)
+				.with("yearly", () => dayjs().add(1, "year"))
+				.with("monthly", () => dayjs().add(1, "month"))
+				.exhaustive();
+			const { ryotUserId, unkeyKeyId, data } = await match(productType)
+				.with("cloud", async () => {
+					const password = randomString(10);
+					const { registerUser } = await serverGqlService.request(
+						RegisterUserDocument,
+						{
+							input: {
+								adminAccessToken: serverVariables.SERVER_ADMIN_ACCESS_TOKEN,
+								data: oidcIssuerId
+									? { oidc: { email: email, issuerId: oidcIssuerId } }
+									: { password: { username: email, password: password } },
+							},
+						},
+					);
+					if (registerUser.__typename === "RegisterError") {
+						console.error(registerUser);
+						throw new Error("Failed to register user");
+					}
+					return {
+						ryotUserId: registerUser.id,
+						unkeyKeyId: null,
+						data: {
+							__typename: "cloud" as const,
+							auth: oidcIssuerId ? email : { username: email, password },
+						},
+					};
+				})
+				.with("self_hosted", async () => {
+					const unkey = new Unkey({ rootKey: serverVariables.UNKEY_ROOT_KEY });
+					const created = await unkey.keys.create({
+						apiId: serverVariables.UNKEY_API_ID,
+						name: email,
+						meta: renewOn
+							? {
+									expiry: renewOn
+										.add(GRACE_PERIOD, "days")
+										.format("YYYY-MM-DD"),
+								}
+							: undefined,
+					});
+					if (created.error) throw new Error(created.error.message);
+					return {
+						ryotUserId: null,
+						unkeyKeyId: created.result.keyId,
+						data: {
+							__typename: "self_hosted" as const,
+							key: created.result.key,
+						},
+					};
+				})
+				.exhaustive();
+			const renewal = renewOn ? renewOn.format("YYYY-MM-DD") : undefined;
+			await sendEmail(
+				customer.email,
+				PurchaseCompleteEmail.subject,
+				PurchaseCompleteEmail({ planType, renewOn: renewal, data }),
+			);
+			await db
+				.update(customers)
+				.set({
+					planType,
+					ryotUserId,
+					unkeyKeyId,
+					productType,
+					renewOn: renewal,
+					paddleCustomerId,
+					paddleFirstTransactionId: transactionId,
+				})
+				.where(eq(customers.id, userId));
+			return Response.json({});
+		},
+	});
+});
+
+export default function Index() {
+	const loaderData = useLoaderData<typeof loader>();
+	const submit = useSubmit();
+	const [paddle, setPaddle] = useState<Paddle>();
+
+	useEffect(() => {
+		initializePaddle({
+			environment: loaderData.isSandbox ? "sandbox" : undefined,
+			token: loaderData.clientToken,
+			eventCallback: (data) => {
+				if (data.name === CheckoutEventNames.CHECKOUT_COMPLETED) {
+					const transactionId = data.data?.transaction_id;
+					if (!transactionId) throw new Error("Transaction ID not found");
+					submit(
+						{ transactionId },
+						{
+							method: "POST",
+							encType: "application/json",
+							action: withQuery("?index", { intent: "processPurchase" }),
+						},
+					);
+				}
+			},
+		}).then((paddleInstance) => {
+			if (paddleInstance) setPaddle(paddleInstance);
+		});
+	}, []);
+
+	return (
+		<>
+			{loaderData.planDetails?.planType &&
+			loaderData.planDetails.productType ? (
+				<Card className="w-full max-w-md p-6 grid gap-6 m-auto mt-40">
+					<div className="grid grid-cols-2 gap-4">
+						<div>
+							<Label>Email</Label>
+							<p className="text-muted-foreground">
+								{loaderData.planDetails.email}
+							</p>
+						</div>
+						{loaderData.planDetails.renewOn ? (
+							<div>
+								<Label>Renewal Status</Label>
+								<p className="text-muted-foreground">
+									Renews on {loaderData.planDetails.renewOn}
+								</p>
+							</div>
+						) : null}
+						<div>
+							<Label>Plan Type</Label>
+							<p className="text-muted-foreground">
+								{changeCase(loaderData.planDetails.planType)}
+							</p>
+						</div>
+						<div>
+							<Label>Product Type</Label>
+							<p className="text-muted-foreground">
+								{changeCase(loaderData.planDetails.productType)}
+							</p>
+						</div>
+						{loaderData.planDetails.ryotUserId ? (
+							<div>
+								<Label>User ID</Label>
+								<p className="text-muted-foreground">
+									{loaderData.planDetails.ryotUserId}
+								</p>
+							</div>
+						) : null}
+						{loaderData.planDetails.unkeyKeyId ? (
+							<div>
+								<Label>Key ID</Label>
+								<p className="text-muted-foreground">
+									{loaderData.planDetails.unkeyKeyId}
+								</p>
+							</div>
+						) : null}
+					</div>
+				</Card>
+			) : (
+				<Pricing
+					prices={loaderData.prices}
+					isLoggedIn
+					onClick={(priceId) => {
+						paddle?.Checkout.open({
+							items: [{ priceId, quantity: 1 }],
+						});
+					}}
+				/>
+			)}
+			<Form
+				method="POST"
+				className="flex w-full items-end justify-end px-4 md:px-10 pb-6"
+			>
+				<input type="hidden" name="intent" defaultValue="logout" />
+				<Button type="submit">Sign out</Button>
+			</Form>
+		</>
+	);
+}
diff --git a/apps/website/app/routes/terms.tsx b/apps/website/app/routes/terms.tsx
new file mode 100644
index 0000000000..0a83bbc872
--- /dev/null
+++ b/apps/website/app/routes/terms.tsx
@@ -0,0 +1,115 @@
+const email = "ignisda2001@gmail.com";
+
+export default function Index() {
+	return (
+		<div className="my-48 prose lg:prose-lg container mx-auto">
+			<h1>Terms of Service</h1>
+
+			<h2>1. Introduction</h2>
+			<p>
+				Welcome to Ryot! These Terms of Service ("Terms") govern your use of the
+				Ryot software ("Service"). By accessing or using the Service, you agree
+				to be bound by these Terms.
+			</p>
+
+			<h2>2. Company Information</h2>
+			<p>
+				<strong>Ryot</strong>, Pocket A-3, Kalkaji Extension, New Delhi 110019,
+				Delhi, India
+				<br />
+				Email: <a href={`mailto:${email}`}>{email}</a>
+			</p>
+
+			<h2>3. User Eligibility</h2>
+			<p>
+				The Service is available to users worldwide with no specific age or
+				residency restrictions.
+			</p>
+
+			<h2>4. User Accounts and Pro Key</h2>
+			<p>
+				Users can self-host the software and create accounts on their own
+				infrastructure. A "Pro Key" is required to verify the purchase of the
+				software. Users are responsible for securing their Pro Keys and ensuring
+				their accounts are used in accordance with these Terms.
+			</p>
+
+			<h2>5. Subscription and Payments</h2>
+			<p>
+				The Service offers monthly and yearly subscription plans. Payments must
+				be made on time to maintain access to the Pro features. Failure to pay
+				on time may result in the revocation of your Pro Key and the
+				deactivation of your server.
+			</p>
+
+			<h2>6. User Conduct</h2>
+			<p>
+				Users must use the Service in a lawful and respectful manner. Any misuse
+				of the Service, including but not limited to distributing illegal
+				content, harassing other users, or attempting to breach the security of
+				the Service, is prohibited.
+			</p>
+
+			<h2>7. Content Ownership</h2>
+			<p>
+				All content uploaded by users remains their property. By using the
+				Service, users grant Ryot a limited, non-exclusive license to use,
+				store, and display this content solely to provide the Service. Users
+				must have the necessary rights to any content they upload.
+			</p>
+
+			<h2>8. Data Privacy</h2>
+			<p>
+				Ryot is committed to protecting user data. We collect, store, and use
+				personal data in accordance with our Privacy Policy. By using the
+				Service, you consent to such collection and use.
+			</p>
+
+			<h2>9. Dispute Resolution</h2>
+			<p>
+				Any disputes arising from or relating to these Terms will be resolved
+				through binding arbitration in accordance with the rules of the Indian
+				Arbitration and Conciliation Act, 1996.
+			</p>
+
+			<h2>10. Changes to Terms</h2>
+			<p>
+				Ryot reserves the right to update these Terms at any time. Users will be
+				notified of any changes via email or through the Service. Continued use
+				of the Service after such changes will constitute acceptance of the new
+				Terms.
+			</p>
+
+			<h2>11. Termination</h2>
+			<p>
+				Ryot reserves the right to revoke your Pro Key and deactivate your
+				server if subscription payments are not made on time. This action will
+				prevent your server from starting up and accessing Pro features.
+			</p>
+
+			<h2>12. Cancellation and Refund Policies</h2>
+			<p>
+				Users may cancel their subscription at any time. To cancel, please
+				contact us at <a href={`mailto:${email}`}>{email}</a>. Refunds will be
+				processed in accordance with Indian laws and regulations.
+			</p>
+			<p>
+				If you cancel within the first 14 days of a monthly subscription, you
+				are eligible for a full refund. For yearly subscriptions, cancellations
+				within the first 30 days are eligible for a full refund. After these
+				periods, refunds will not be provided.
+			</p>
+			<p>
+				Refund requests should be made by contacting our support team. We
+				reserve the right to review and approve or deny refund requests based on
+				individual circumstances and in compliance with applicable laws.
+			</p>
+
+			<p>
+				By using Ryot, you acknowledge that you have read, understood, and agree
+				to these Terms of Service. If you do not agree to these Terms, please do
+				not use the Service.
+			</p>
+		</div>
+	);
+}
diff --git a/apps/website/app/tailwind.css b/apps/website/app/tailwind.css
new file mode 100644
index 0000000000..d6356b63a6
--- /dev/null
+++ b/apps/website/app/tailwind.css
@@ -0,0 +1,72 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer base {
+  :root {
+    --background: 0 0% 100%;
+    --foreground: 240 10% 3.9%;
+    --card: 0 0% 100%;
+    --card-foreground: 240 10% 3.9%;
+    --popover: 0 0% 100%;
+    --popover-foreground: 240 10% 3.9%;
+    --primary: 240 5.9% 10%;
+    --primary-foreground: 0 0% 98%;
+    --secondary: 240 4.8% 95.9%;
+    --secondary-foreground: 240 5.9% 10%;
+    --muted: 240 4.8% 95.9%;
+    --muted-foreground: 240 3.8% 46.1%;
+    --accent: 240 4.8% 95.9%;
+    --accent-foreground: 240 5.9% 10%;
+    --destructive: 0 84.2% 60.2%;
+    --destructive-foreground: 0 0% 98%;
+    --border: 240 5.9% 90%;
+    --input: 240 5.9% 90%;
+    --ring: 240 10% 3.9%;
+    --radius: 0.5rem;
+    --chart-1: 12 76% 61%;
+    --chart-2: 173 58% 39%;
+    --chart-3: 197 37% 24%;
+    --chart-4: 43 74% 66%;
+    --chart-5: 27 87% 67%;
+  }
+
+  .dark {
+    --background: 240 10% 3.9%;
+    --foreground: 0 0% 98%;
+    --card: 240 10% 3.9%;
+    --card-foreground: 0 0% 98%;
+    --popover: 240 10% 3.9%;
+    --popover-foreground: 0 0% 98%;
+    --primary: 0 0% 98%;
+    --primary-foreground: 240 5.9% 10%;
+    --secondary: 240 3.7% 15.9%;
+    --secondary-foreground: 0 0% 98%;
+    --muted: 240 3.7% 15.9%;
+    --muted-foreground: 240 5% 64.9%;
+    --accent: 240 3.7% 15.9%;
+    --accent-foreground: 0 0% 98%;
+    --destructive: 0 62.8% 30.6%;
+    --destructive-foreground: 0 0% 98%;
+    --border: 240 3.7% 15.9%;
+    --input: 240 3.7% 15.9%;
+    --ring: 240 4.9% 83.9%;
+    --chart-1: 220 70% 50%;
+    --chart-2: 160 60% 45%;
+    --chart-3: 30 80% 55%;
+    --chart-4: 280 65% 60%;
+    --chart-5: 340 75% 55%;
+  }
+}
+
+@layer base {
+  * {
+    @apply border-border;
+  }
+  body {
+    @apply bg-background text-foreground font-body;
+  }
+  h1, h2, h3, h4, h5, h6 {
+    @apply font-heading;
+  }
+}
diff --git a/apps/website/components.json b/apps/website/components.json
new file mode 100644
index 0000000000..1a68a06f76
--- /dev/null
+++ b/apps/website/components.json
@@ -0,0 +1,16 @@
+{
+	"$schema": "https://ui.shadcn.com/schema.json",
+	"style": "default",
+	"rsc": false,
+	"tsx": true,
+	"tailwind": {
+		"config": "tailwind.config.ts",
+		"css": "app/tailwind.css",
+		"baseColor": "zinc",
+		"cssVariables": true
+	},
+	"aliases": {
+		"components": "app/lib/components",
+		"utils": "app/lib/utils"
+	}
+}
diff --git a/apps/website/drizzle.config.ts b/apps/website/drizzle.config.ts
new file mode 100644
index 0000000000..0fc17d2cb3
--- /dev/null
+++ b/apps/website/drizzle.config.ts
@@ -0,0 +1,11 @@
+import { defineConfig } from "drizzle-kit";
+
+const DATABASE_URL = process.env.DATABASE_URL;
+if (!DATABASE_URL) throw new Error("DATABASE_URL is not set");
+
+export default defineConfig({
+	dialect: "postgresql",
+	schema: "app/drizzle/schema.server.ts",
+	out: "app/drizzle/migrations",
+	dbCredentials: { url: DATABASE_URL },
+});
diff --git a/apps/website/env.d.ts b/apps/website/env.d.ts
new file mode 100644
index 0000000000..78ed2345c6
--- /dev/null
+++ b/apps/website/env.d.ts
@@ -0,0 +1,2 @@
+/// <reference types="@remix-run/node" />
+/// <reference types="vite/client" />
diff --git a/apps/website/moon.yml b/apps/website/moon.yml
new file mode 100644
index 0000000000..525695f6b4
--- /dev/null
+++ b/apps/website/moon.yml
@@ -0,0 +1,15 @@
+dependsOn:
+  - 'assets'
+
+tasks:
+  dev:
+    command: 'remix vite:dev --port 5200 --host'
+    local: true
+
+  build:
+    command: 'remix vite:build'
+    inputs: ['@group(remix)']
+    outputs: ['build']
+
+  typecheck:
+    inputs: ['@group(remix)']
diff --git a/apps/website/package.json b/apps/website/package.json
new file mode 100644
index 0000000000..8c96636c1d
--- /dev/null
+++ b/apps/website/package.json
@@ -0,0 +1,77 @@
+{
+	"name": "@ryot/website",
+	"private": true,
+	"sideEffects": false,
+	"type": "module",
+	"installConfig": {
+		"hoistingLimits": "workspaces"
+	},
+	"dependencies": {
+		"@conform-to/zod": "1.1.5",
+		"@isaacs/ttlcache": "1.4.1",
+		"@paddle/paddle-js": "1.2.0",
+		"@paddle/paddle-node-sdk": "1.4.1",
+		"@radix-ui/react-label": "2.1.0",
+		"@radix-ui/react-slot": "1.1.0",
+		"@radix-ui/react-tooltip": "1.1.2",
+		"@react-email/components": "0.0.22",
+		"@remix-run/node": "2.10.3",
+		"@remix-run/react": "2.10.3",
+		"@remix-run/serve": "2.10.3",
+		"@ryot/generated": "workspace:*",
+		"@ryot/graphql": "workspace:*",
+		"@ryot/transactional": "workspace:*",
+		"@ryot/ts-utils": "workspace:*",
+		"@tabler/icons-react": "3.11.0",
+		"@unkey/api": "0.23.0",
+		"class-variance-authority": "0.7.0",
+		"clsx": "2.1.1",
+		"crypto-js": "4.2.0",
+		"dayjs": "1.11.12",
+		"drizzle-orm": "0.32.1",
+		"embla-carousel-autoplay": "8.1.7",
+		"embla-carousel-react": "8.1.7",
+		"graphql": "16.9.0",
+		"graphql-request": "7.1.0",
+		"humanize-duration-ts": "2.1.1",
+		"input-otp": "1.2.4",
+		"isbot": "4.1.0",
+		"lodash": "4.17.21",
+		"lucide-react": "0.414.0",
+		"next-themes": "0.3.0",
+		"nodemailer": "6.9.14",
+		"openid-client": "5.6.5",
+		"postgres": "3.4.4",
+		"react": "18.2.0",
+		"react-dom": "18.2.0",
+		"remix-routes": "1.7.6",
+		"remix-utils": "7.6.0",
+		"sonner": "1.5.0",
+		"tailwind-merge": "2.4.0",
+		"tailwindcss-animate": "1.0.7",
+		"ts-pattern": "5.2.0",
+		"ufo": "1.5.4",
+		"zod": "3.23.8",
+		"zodix": "0.4.4"
+	},
+	"devDependencies": {
+		"@remix-run/dev": "2.10.3",
+		"@tailwindcss/typography": "0.5.13",
+		"@types/crypto-js": "^4",
+		"@types/lodash": "4.17.7",
+		"@types/nodemailer": "6",
+		"@types/react": "18.2.20",
+		"@types/react-dom": "18.2.7",
+		"autoprefixer": "10.4.19",
+		"drizzle-kit": "0.23.0",
+		"postcss": "8.4.38",
+		"tailwindcss": "3.4.4",
+		"typescript": "5.1.6",
+		"typescript-remix-routes-plugin": "1.0.1",
+		"vite": "5.1.0",
+		"vite-tsconfig-paths": "4.2.1"
+	},
+	"engines": {
+		"node": ">=20.0.0"
+	}
+}
diff --git a/apps/website/postcss.config.js b/apps/website/postcss.config.js
new file mode 100644
index 0000000000..7b75c83aff
--- /dev/null
+++ b/apps/website/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+	plugins: {
+		tailwindcss: {},
+		autoprefixer: {},
+	},
+};
diff --git a/apps/website/public/cta-image.png b/apps/website/public/cta-image.png
new file mode 100644
index 0000000000..ac0e2b5bb4
Binary files /dev/null and b/apps/website/public/cta-image.png differ
diff --git a/apps/website/public/desktop.png b/apps/website/public/desktop.png
new file mode 100644
index 0000000000..09a2850885
Binary files /dev/null and b/apps/website/public/desktop.png differ
diff --git a/apps/website/public/genres.png b/apps/website/public/genres.png
new file mode 100644
index 0000000000..7fbfc7ffbb
Binary files /dev/null and b/apps/website/public/genres.png differ
diff --git a/apps/website/public/google3b42b4eb7053ce19.html b/apps/website/public/google3b42b4eb7053ce19.html
new file mode 100644
index 0000000000..6583cbc980
--- /dev/null
+++ b/apps/website/public/google3b42b4eb7053ce19.html
@@ -0,0 +1,1 @@
+google-site-verification: google3b42b4eb7053ce19.html
\ No newline at end of file
diff --git a/apps/website/public/group.png b/apps/website/public/group.png
new file mode 100644
index 0000000000..76ab8c4484
Binary files /dev/null and b/apps/website/public/group.png differ
diff --git a/apps/website/public/other-enhancements.png b/apps/website/public/other-enhancements.png
new file mode 100644
index 0000000000..551c4ba484
Binary files /dev/null and b/apps/website/public/other-enhancements.png differ
diff --git a/apps/website/public/recommendations.png b/apps/website/public/recommendations.png
new file mode 100644
index 0000000000..2f5ce2ef7c
Binary files /dev/null and b/apps/website/public/recommendations.png differ
diff --git a/apps/website/public/sharing-form.png b/apps/website/public/sharing-form.png
new file mode 100644
index 0000000000..89774d001e
Binary files /dev/null and b/apps/website/public/sharing-form.png differ
diff --git a/apps/website/public/sharing.png b/apps/website/public/sharing.png
new file mode 100644
index 0000000000..6fa79571be
Binary files /dev/null and b/apps/website/public/sharing.png differ
diff --git a/apps/website/public/supercharged-collections.png b/apps/website/public/supercharged-collections.png
new file mode 100644
index 0000000000..d1f7a22465
Binary files /dev/null and b/apps/website/public/supercharged-collections.png differ
diff --git a/apps/website/tailwind.config.ts b/apps/website/tailwind.config.ts
new file mode 100644
index 0000000000..1b11ab7685
--- /dev/null
+++ b/apps/website/tailwind.config.ts
@@ -0,0 +1,84 @@
+import type { Config } from "tailwindcss";
+import { fontFamily } from "tailwindcss/defaultTheme";
+
+const config = {
+	darkMode: ["class"],
+	content: ["app/**/*.{ts,tsx}"],
+	theme: {
+		fontFamily: {
+			heading: ["Inter", ...fontFamily.sans],
+			body: ["Inter", ...fontFamily.sans],
+		},
+		container: {
+			center: true,
+			padding: "2rem",
+			screens: {
+				"2xl": "1400px",
+			},
+		},
+		extend: {
+			colors: {
+				border: "hsl(var(--border))",
+				input: "hsl(var(--input))",
+				ring: "hsl(var(--ring))",
+				background: "hsl(var(--background))",
+				foreground: "hsl(var(--foreground))",
+				primary: {
+					DEFAULT: "hsl(var(--primary))",
+					foreground: "hsl(var(--primary-foreground))",
+				},
+				secondary: {
+					DEFAULT: "hsl(var(--secondary))",
+					foreground: "hsl(var(--secondary-foreground))",
+				},
+				destructive: {
+					DEFAULT: "hsl(var(--destructive))",
+					foreground: "hsl(var(--destructive-foreground))",
+				},
+				muted: {
+					DEFAULT: "hsl(var(--muted))",
+					foreground: "hsl(var(--muted-foreground))",
+				},
+				accent: {
+					DEFAULT: "hsl(var(--accent))",
+					foreground: "hsl(var(--accent-foreground))",
+				},
+				popover: {
+					DEFAULT: "hsl(var(--popover))",
+					foreground: "hsl(var(--popover-foreground))",
+				},
+				card: {
+					DEFAULT: "hsl(var(--card))",
+					foreground: "hsl(var(--card-foreground))",
+				},
+			},
+			borderRadius: {
+				lg: "var(--radius)",
+				md: "calc(var(--radius) - 2px)",
+				sm: "calc(var(--radius) - 4px)",
+			},
+			keyframes: {
+				"accordion-down": {
+					from: { height: "0" },
+					to: { height: "var(--radix-accordion-content-height)" },
+				},
+				"accordion-up": {
+					from: { height: "var(--radix-accordion-content-height)" },
+					to: { height: "0" },
+				},
+				"caret-blink": {
+					"0%,70%,100%": { opacity: "1" },
+					"20%,50%": { opacity: "0" },
+				},
+			},
+			animation: {
+				"accordion-down": "accordion-down 0.2s ease-out",
+				"accordion-up": "accordion-up 0.2s ease-out",
+				"caret-blink": "caret-blink 1.25s ease-out infinite",
+			},
+		},
+	},
+	plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")],
+} satisfies Config;
+
+export default config;
diff --git a/apps/website/tsconfig.json b/apps/website/tsconfig.json
new file mode 100644
index 0000000000..0848217427
--- /dev/null
+++ b/apps/website/tsconfig.json
@@ -0,0 +1,48 @@
+{
+	"include": ["env.d.ts", "**/*.ts", "**/*.tsx"],
+	"compilerOptions": {
+		"lib": ["DOM", "DOM.Iterable", "ES2022"],
+		"isolatedModules": true,
+		"esModuleInterop": true,
+		"jsx": "react-jsx",
+		"module": "ESNext",
+		"moduleResolution": "Bundler",
+		"resolveJsonModule": true,
+		"noUnusedLocals": true,
+		"target": "es2022",
+		"strict": true,
+		"allowJs": true,
+		"forceConsistentCasingInFileNames": true,
+		"baseUrl": ".",
+		"paths": {
+			"@ryot/generated/*": ["../../libs/generated/src/*"],
+			"@ryot/graphql/*": ["../../libs/graphql/src/*"],
+			"@ryot/ts-utils": ["../../libs/ts-utils/src/index.ts"],
+			"@ryot/ts-utils/*": ["../../libs/ts-utils/src/*"],
+			"~/*": ["./app/*"],
+			"@ryot/transactional/*": ["../../libs/transactional/*"]
+		},
+		"skipLibCheck": true,
+		"noEmit": true,
+		"outDir": "../../.moon/cache/types/apps/website",
+		"plugins": [
+			{
+				"name": "typescript-remix-routes-plugin"
+			}
+		]
+	},
+	"references": [
+		{
+			"path": "../../libs/generated"
+		},
+		{
+			"path": "../../libs/graphql"
+		},
+		{
+			"path": "../../libs/transactional"
+		},
+		{
+			"path": "../../libs/ts-utils"
+		}
+	]
+}
diff --git a/apps/website/vite.config.ts b/apps/website/vite.config.ts
new file mode 100644
index 0000000000..1180757683
--- /dev/null
+++ b/apps/website/vite.config.ts
@@ -0,0 +1,19 @@
+import { vitePlugin as remix } from "@remix-run/dev";
+import { remixRoutes } from "remix-routes/vite";
+import { defineConfig } from "vite";
+import tsconfigPaths from "vite-tsconfig-paths";
+
+export default defineConfig({
+	plugins: [
+		remix({
+			future: {
+				unstable_singleFetch: true,
+				v3_fetcherPersist: true,
+				v3_relativeSplatPath: true,
+				v3_throwAbortReason: true,
+			},
+		}),
+		remixRoutes(),
+		tsconfigPaths(),
+	],
+});
diff --git a/ci/Caddyfile b/ci/Caddyfile
index 0d46960602..f71b50a1fe 100644
--- a/ci/Caddyfile
+++ b/ci/Caddyfile
@@ -13,6 +13,14 @@
 		rewrite * /webhooks/integrations{path}
 		reverse_proxy {vars.backend_url}
 	}
+	handle_path /u/* {
+		rewrite * /api/sharing{path}?isAccountDefault=true
+		reverse_proxy {vars.frontend_url}
+	}
+	handle_path /_s/* {
+		rewrite * /api/sharing{path}
+		reverse_proxy {vars.frontend_url}
+	}
 	handle_path /backend* {
 		reverse_proxy {vars.backend_url}
 	}
diff --git a/crates/config/Cargo.toml b/crates/config/Cargo.toml
index e0c31ca34c..60c21dca7e 100644
--- a/crates/config/Cargo.toml
+++ b/crates/config/Cargo.toml
@@ -5,7 +5,7 @@ edition = "2021"
 
 [dependencies]
 anyhow = { workspace = true }
-dotenvy_macro = { workspace = true }
 common-utils = { path = "../utils/common" }
+env-utils = { path = "../utils/env" }
 schematic = { workspace = true }
 serde = { workspace = true }
diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs
index 3b4428f0c1..3c01764fff 100644
--- a/crates/config/src/lib.rs
+++ b/crates/config/src/lib.rs
@@ -2,25 +2,16 @@ use std::path::PathBuf;
 
 use anyhow::Result;
 use common_utils::{IsFeatureEnabled, PROJECT_NAME};
+use env_utils::{DEFAULT_MAL_CLIENT_ID, DEFAULT_TMDB_ACCESS_TOKEN};
 use schematic::{derive_enum, validate::not_empty, Config, ConfigEnum, ConfigLoader, HandlerError};
 use serde::{Deserialize, Serialize};
 
-#[cfg(debug_assertions)]
-pub const DEFAULT_TMDB_ACCESS_TOKEN: &str = dotenvy_macro::dotenv!("DEFAULT_TMDB_ACCESS_TOKEN");
-#[cfg(not(debug_assertions))]
-pub const DEFAULT_TMDB_ACCESS_TOKEN: &str = env!("DEFAULT_TMDB_ACCESS_TOKEN");
-
 fn default_tmdb_access_token(_ctx: &()) -> Result<Option<String>, HandlerError> {
-    Ok(Some(DEFAULT_TMDB_ACCESS_TOKEN.to_owned()))
+    Ok(Some(DEFAULT_TMDB_ACCESS_TOKEN.to_string()))
 }
 
-#[cfg(debug_assertions)]
-pub const DEFAULT_MAL_CLIENT_ID: &str = dotenvy_macro::dotenv!("DEFAULT_MAL_CLIENT_ID");
-#[cfg(not(debug_assertions))]
-pub const DEFAULT_MAL_CLIENT_ID: &str = env!("DEFAULT_MAL_CLIENT_ID");
-
 fn default_mal_client_id(_ctx: &()) -> Result<Option<String>, HandlerError> {
-    Ok(Some(DEFAULT_MAL_CLIENT_ID.to_owned()))
+    Ok(Some(DEFAULT_MAL_CLIENT_ID.to_string()))
 }
 
 #[derive(Debug, Serialize, Deserialize, Clone, Config)]
@@ -369,6 +360,8 @@ pub struct ServerConfig {
     /// The OIDC related settings.
     #[setting(nested)]
     pub oidc: OidcConfig,
+    /// The pro key assigned to the user.
+    pub pro_key: String,
     /// An array of URLs for CORS.
     #[setting(default = vec![], parse_env = schematic::env::split_comma)]
     pub cors_origins: Vec<String>,
@@ -497,6 +490,7 @@ impl AppConfig {
         cl.server.oidc.client_id = gt();
         cl.server.oidc.client_secret = gt();
         cl.server.oidc.issuer_url = gt();
+        cl.server.pro_key = gt();
         cl.server.admin_access_token = gt();
         cl
     }
diff --git a/crates/enums/src/lib.rs b/crates/enums/src/lib.rs
index f217e7c2bc..76c09a1ea2 100644
--- a/crates/enums/src/lib.rs
+++ b/crates/enums/src/lib.rs
@@ -104,6 +104,9 @@ pub enum UserLot {
     Enum,
     Display,
     Default,
+    PartialOrd,
+    Ord,
+    Hash,
 )]
 #[sea_orm(
     rs_type = "String",
@@ -118,6 +121,7 @@ pub enum EntityLot {
     Exercise,
     Collection,
     Workout,
+    WorkoutTemplate,
 }
 
 // The different possible states of a seen item.
diff --git a/crates/migrations/src/lib.rs b/crates/migrations/src/lib.rs
index 6cff3b5982..a5c133a5b5 100644
--- a/crates/migrations/src/lib.rs
+++ b/crates/migrations/src/lib.rs
@@ -11,6 +11,7 @@ mod m20230502_create_genre;
 mod m20230504_create_collection;
 mod m20230505_create_review;
 mod m20230509_create_import_report;
+mod m20230818_create_workout_template;
 mod m20230819_create_workout;
 mod m20230820_create_user_measurement;
 mod m20230822_create_exercise;
@@ -23,15 +24,20 @@ mod m20240531_create_queued_notification;
 mod m20240607_create_integration;
 mod m20240712_create_notification_platform;
 mod m20240713_create_user_summary;
+mod m20240714_create_access_link;
 mod m20240825_is_v7_migration;
 mod m20240827_create_daily_user_activity;
 mod m20240827_zz_changes_for_daily_user_activity;
 mod m20240828_add_last_login_on_column_to_user;
 mod m20240828_zz_add_columns_to_daily_user_activity;
 mod m20240829_change_structure_for_exercise_extra_information;
+mod m20240831_add_is_account_default_column_to_access_link;
+mod m20240831_add_templates_key_to_preferences;
 mod m20240903_add_changes_for_user_to_collection_removal;
 mod m20240904_create_monitored_entity;
+mod m20240918_add_default_rest_timer_to_workout_template;
 mod m20240923_remove_extra_columns_for_daily_user_activities;
+mod m20240926_add_columns_for_open_sourcing_pro_version;
 
 pub use m20230410_create_metadata::Metadata as AliasedMetadata;
 pub use m20230413_create_person::Person as AliasedPerson;
@@ -62,6 +68,7 @@ impl MigratorTrait for Migrator {
             Box::new(m20230502_create_genre::Migration),
             Box::new(m20230504_create_collection::Migration),
             Box::new(m20230822_create_exercise::Migration),
+            Box::new(m20230818_create_workout_template::Migration),
             Box::new(m20230819_create_workout::Migration),
             Box::new(m20230505_create_review::Migration),
             Box::new(m20230509_create_import_report::Migration),
@@ -75,15 +82,20 @@ impl MigratorTrait for Migrator {
             Box::new(m20240607_create_integration::Migration),
             Box::new(m20240712_create_notification_platform::Migration),
             Box::new(m20240713_create_user_summary::Migration),
+            Box::new(m20240714_create_access_link::Migration),
             Box::new(m20240825_is_v7_migration::Migration),
             Box::new(m20240827_create_daily_user_activity::Migration),
             Box::new(m20240827_zz_changes_for_daily_user_activity::Migration),
             Box::new(m20240828_add_last_login_on_column_to_user::Migration),
             Box::new(m20240828_zz_add_columns_to_daily_user_activity::Migration),
             Box::new(m20240829_change_structure_for_exercise_extra_information::Migration),
+            Box::new(m20240831_add_templates_key_to_preferences::Migration),
+            Box::new(m20240831_add_is_account_default_column_to_access_link::Migration),
             Box::new(m20240903_add_changes_for_user_to_collection_removal::Migration),
             Box::new(m20240904_create_monitored_entity::Migration),
+            Box::new(m20240918_add_default_rest_timer_to_workout_template::Migration),
             Box::new(m20240923_remove_extra_columns_for_daily_user_activities::Migration),
+            Box::new(m20240926_add_columns_for_open_sourcing_pro_version::Migration),
         ]
     }
 }
diff --git a/crates/migrations/src/m20230410_create_metadata.rs b/crates/migrations/src/m20230410_create_metadata.rs
index 90f55a9406..65f9d7d748 100644
--- a/crates/migrations/src/m20230410_create_metadata.rs
+++ b/crates/migrations/src/m20230410_create_metadata.rs
@@ -41,6 +41,8 @@ pub enum Metadata {
     IsNsfw,
     // those creators who can not be created as a `person` due to incomplete info
     FreeCreators,
+    // whether this is a recommendation
+    IsRecommendation,
     // specifics for each type of media
     AudioBookSpecifics,
     AnimeSpecifics,
@@ -102,6 +104,7 @@ impl MigrationTrait for Migration {
                     .col(ColumnDef::new(Metadata::VisualNovelSpecifics).json_binary())
                     .col(ColumnDef::new(Metadata::WatchProviders).json_binary())
                     .col(ColumnDef::new(Metadata::StateChanges).json_binary())
+                    .col(ColumnDef::new(Metadata::IsRecommendation).boolean())
                     .col(ColumnDef::new(Metadata::ExternalIdentifiers).json_binary())
                     .to_owned(),
             )
diff --git a/crates/migrations/src/m20230419_create_seen.rs b/crates/migrations/src/m20230419_create_seen.rs
index b38f29198a..6b9505f78a 100644
--- a/crates/migrations/src/m20230419_create_seen.rs
+++ b/crates/migrations/src/m20230419_create_seen.rs
@@ -25,6 +25,7 @@ pub enum Seen {
     AnimeExtraInformation,
     MangaExtraInformation,
     ProviderWatchedOn,
+    ManualTimeSpent,
 }
 
 #[async_trait::async_trait]
@@ -74,6 +75,7 @@ impl MigrationTrait for Migration {
                     )
                     .col(ColumnDef::new(Seen::MetadataId).text().not_null())
                     .col(ColumnDef::new(Seen::UserId).text().not_null())
+                    .col(ColumnDef::new(Seen::ManualTimeSpent).decimal())
                     .foreign_key(
                         ForeignKey::create()
                             .name("user_to_seen_foreign_key")
diff --git a/crates/migrations/src/m20230818_create_workout_template.rs b/crates/migrations/src/m20230818_create_workout_template.rs
new file mode 100644
index 0000000000..fefb06cdf3
--- /dev/null
+++ b/crates/migrations/src/m20230818_create_workout_template.rs
@@ -0,0 +1,78 @@
+// FIXME: Rename this to m20230506_create_workout_template in the next major release
+use enums::Visibility;
+use sea_orm_migration::prelude::*;
+
+use super::m20230417_create_user::User;
+
+#[derive(DeriveMigrationName)]
+pub struct Migration;
+
+#[derive(Iden)]
+pub enum WorkoutTemplate {
+    Table,
+    Id,
+    UserId,
+    CreatedOn,
+    Name,
+    Summary,
+    Visibility,
+    Information,
+    DefaultRestTimer,
+}
+
+#[async_trait::async_trait]
+impl MigrationTrait for Migration {
+    async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
+        manager
+            .create_table(
+                Table::create()
+                    .table(WorkoutTemplate::Table)
+                    .col(
+                        ColumnDef::new(WorkoutTemplate::Id)
+                            .primary_key()
+                            .text()
+                            .not_null(),
+                    )
+                    .col(
+                        ColumnDef::new(WorkoutTemplate::Visibility)
+                            .text()
+                            .not_null()
+                            .default(Visibility::Private),
+                    )
+                    .col(
+                        ColumnDef::new(WorkoutTemplate::Summary)
+                            .json_binary()
+                            .not_null(),
+                    )
+                    .col(
+                        ColumnDef::new(WorkoutTemplate::Information)
+                            .json_binary()
+                            .not_null(),
+                    )
+                    .col(ColumnDef::new(WorkoutTemplate::Name).text().not_null())
+                    .col(ColumnDef::new(WorkoutTemplate::UserId).text().not_null())
+                    .col(
+                        ColumnDef::new(WorkoutTemplate::CreatedOn)
+                            .timestamp_with_time_zone()
+                            .not_null()
+                            .default(Expr::current_timestamp()),
+                    )
+                    .col(ColumnDef::new(WorkoutTemplate::DefaultRestTimer).integer())
+                    .foreign_key(
+                        ForeignKey::create()
+                            .name("workout_template_to_user_foreign_key")
+                            .from(WorkoutTemplate::Table, WorkoutTemplate::UserId)
+                            .to(User::Table, User::Id)
+                            .on_delete(ForeignKeyAction::Cascade)
+                            .on_update(ForeignKeyAction::Cascade),
+                    )
+                    .to_owned(),
+            )
+            .await?;
+        Ok(())
+    }
+
+    async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> {
+        Ok(())
+    }
+}
diff --git a/crates/migrations/src/m20230819_create_workout.rs b/crates/migrations/src/m20230819_create_workout.rs
index 6bbe2a3f97..b770f849ba 100644
--- a/crates/migrations/src/m20230819_create_workout.rs
+++ b/crates/migrations/src/m20230819_create_workout.rs
@@ -1,7 +1,7 @@
 // FIXME: Rename this to m20230507_create_workout in the next major release
 use sea_orm_migration::prelude::*;
 
-use super::m20230417_create_user::User;
+use super::{m20230417_create_user::User, m20230818_create_workout_template::WorkoutTemplate};
 
 #[derive(DeriveMigrationName)]
 pub struct Migration;
@@ -22,6 +22,8 @@ pub enum Workout {
     Information,
     /// The workout this one was repeated from
     RepeatedFrom,
+    /// The template this workout was created from
+    TemplateId,
 }
 
 #[async_trait::async_trait]
@@ -57,6 +59,7 @@ impl MigrationTrait for Migration {
                     .col(ColumnDef::new(Workout::Name).text().not_null())
                     .col(ColumnDef::new(Workout::RepeatedFrom).text())
                     .col(ColumnDef::new(Workout::UserId).text().not_null())
+                    .col(ColumnDef::new(Workout::TemplateId).text())
                     .foreign_key(
                         ForeignKey::create()
                             .name("workout_to_user_foreign_key")
@@ -65,6 +68,14 @@ impl MigrationTrait for Migration {
                             .on_delete(ForeignKeyAction::Cascade)
                             .on_update(ForeignKeyAction::Cascade),
                     )
+                    .foreign_key(
+                        ForeignKey::create()
+                            .name("template_to_workout_foreign_key")
+                            .from(Workout::Table, Workout::TemplateId)
+                            .to(WorkoutTemplate::Table, WorkoutTemplate::Id)
+                            .on_delete(ForeignKeyAction::SetNull)
+                            .on_update(ForeignKeyAction::Cascade),
+                    )
                     .to_owned(),
             )
             .await?;
diff --git a/crates/migrations/src/m20231016_create_collection_to_entity.rs b/crates/migrations/src/m20231016_create_collection_to_entity.rs
index 495d3adf83..383283d182 100644
--- a/crates/migrations/src/m20231016_create_collection_to_entity.rs
+++ b/crates/migrations/src/m20231016_create_collection_to_entity.rs
@@ -4,7 +4,8 @@ use sea_orm_migration::prelude::*;
 use super::{
     m20230410_create_metadata::Metadata, m20230413_create_person::Person,
     m20230501_create_metadata_group::MetadataGroup, m20230504_create_collection::Collection,
-    m20230819_create_workout::Workout, m20230822_create_exercise::Exercise,
+    m20230818_create_workout_template::WorkoutTemplate, m20230819_create_workout::Workout,
+    m20230822_create_exercise::Exercise,
 };
 
 #[derive(DeriveMigrationName)]
@@ -15,6 +16,7 @@ pub static UNIQUE_INDEX_2: &str = "collection_to_entity_uqi2";
 pub static UNIQUE_INDEX_3: &str = "collection_to_entity_uqi3";
 pub static UNIQUE_INDEX_4: &str = "collection_to_entity_uqi4";
 pub static UNIQUE_INDEX_5: &str = "collection_to_entity_uqi5";
+pub static UNIQUE_INDEX_6: &str = "collection_to_entity_uqi6";
 pub static CONSTRAINT_SQL: &str = indoc! { r#"
     ALTER TABLE "collection_to_entity" DROP CONSTRAINT IF EXISTS "collection_to_entity__ensure_one_entity";
     ALTER TABLE "collection_to_entity"
@@ -24,7 +26,8 @@ pub static CONSTRAINT_SQL: &str = indoc! { r#"
         (CASE WHEN "person_id" IS NOT NULL THEN 1 ELSE 0 END) +
         (CASE WHEN "exercise_id" IS NOT NULL THEN 1 ELSE 0 END) +
         (CASE WHEN "metadata_group_id" IS NOT NULL THEN 1 ELSE 0 END) +
-        (CASE WHEN "workout_id" IS NOT NULL THEN 1 ELSE 0 END)
+        (CASE WHEN "workout_id" IS NOT NULL THEN 1 ELSE 0 END) +
+        (CASE WHEN "workout_template_id" IS NOT NULL THEN 1 ELSE 0 END)
         = 1
     );
 "# };
@@ -35,7 +38,8 @@ pub static ENTITY_ID_SQL: &str = indoc! { r#"
             "person_id",
             "metadata_group_id",
             "exercise_id",
-            "workout_id"
+            "workout_id",
+            "workout_template_id"
         )
     ) STORED
 "# };
@@ -47,6 +51,7 @@ pub static ENTITY_LOT_SQL: &str = indoc! { r#"
             WHEN "metadata_group_id" IS NOT NULL THEN 'metadata_group'
             WHEN "exercise_id" IS NOT NULL THEN 'exercise'
             WHEN "workout_id" IS NOT NULL THEN 'workout'
+            WHEN "workout_template_id" IS NOT NULL THEN 'workout_template'
         END
     ) STORED
 "# };
@@ -67,6 +72,7 @@ pub enum CollectionToEntity {
     PersonId,
     ExerciseId,
     WorkoutId,
+    WorkoutTemplateId,
 }
 
 #[async_trait::async_trait]
@@ -107,6 +113,7 @@ impl MigrationTrait for Migration {
                             .primary_key(),
                     )
                     .col(ColumnDef::new(CollectionToEntity::WorkoutId).text())
+                    .col(ColumnDef::new(CollectionToEntity::WorkoutTemplateId).text())
                     .col(
                         ColumnDef::new(CollectionToEntity::EntityId)
                             .text()
@@ -170,6 +177,17 @@ impl MigrationTrait for Migration {
                             .on_delete(ForeignKeyAction::Cascade)
                             .on_update(ForeignKeyAction::Cascade),
                     )
+                    .foreign_key(
+                        ForeignKey::create()
+                            .name("collection_to_entity-fk7")
+                            .from(
+                                CollectionToEntity::Table,
+                                CollectionToEntity::WorkoutTemplateId,
+                            )
+                            .to(WorkoutTemplate::Table, WorkoutTemplate::Id)
+                            .on_delete(ForeignKeyAction::Cascade)
+                            .on_update(ForeignKeyAction::Cascade),
+                    )
                     .to_owned(),
             )
             .await?;
@@ -179,6 +197,7 @@ impl MigrationTrait for Migration {
             (UNIQUE_INDEX_3, CollectionToEntity::MetadataGroupId),
             (UNIQUE_INDEX_4, CollectionToEntity::ExerciseId),
             (UNIQUE_INDEX_5, CollectionToEntity::WorkoutId),
+            (UNIQUE_INDEX_6, CollectionToEntity::WorkoutTemplateId),
         ] {
             manager
                 .create_index(
diff --git a/crates/migrations/src/m20240607_create_integration.rs b/crates/migrations/src/m20240607_create_integration.rs
index 688a94b0fd..da4be0d037 100644
--- a/crates/migrations/src/m20240607_create_integration.rs
+++ b/crates/migrations/src/m20240607_create_integration.rs
@@ -15,6 +15,7 @@ pub enum Integration {
     LastTriggeredOn,
     ProviderSpecifics,
     UserId,
+    SyncToOwnedCollection,
     MinimumProgress,
     MaximumProgress,
     IsDisabled,
@@ -46,6 +47,7 @@ impl MigrationTrait for Migration {
                     .col(ColumnDef::new(Integration::UserId).text().not_null())
                     .col(ColumnDef::new(Integration::MinimumProgress).decimal())
                     .col(ColumnDef::new(Integration::MaximumProgress).decimal())
+                    .col(ColumnDef::new(Integration::SyncToOwnedCollection).boolean())
                     .col(ColumnDef::new(Integration::IsDisabled).boolean())
                     .foreign_key(
                         ForeignKey::create()
diff --git a/crates/migrations/src/m20240714_create_access_link.rs b/crates/migrations/src/m20240714_create_access_link.rs
new file mode 100644
index 0000000000..32cc79857e
--- /dev/null
+++ b/crates/migrations/src/m20240714_create_access_link.rs
@@ -0,0 +1,87 @@
+use sea_orm_migration::prelude::*;
+
+use super::m20230417_create_user::User;
+
+#[derive(DeriveMigrationName)]
+pub struct Migration;
+
+#[derive(Iden)]
+pub enum AccessLink {
+    Table,
+    Id,
+    UserId,
+    CreatedOn,
+    Name,
+    IsMutationAllowed,
+    IsAccountDefault,
+    IsDemo,
+    IssuedTokens,
+    // The maximum number of times this link can be used to generate a token
+    MaximumUses,
+    ExpiresOn,
+    // If true, the link can no longer be used to generate a token
+    IsRevoked,
+    // The number of times this link has been used to generate a token
+    TimesUsed,
+    // Automatically redirect to this URL after generating a token
+    RedirectTo,
+}
+
+#[async_trait::async_trait]
+impl MigrationTrait for Migration {
+    async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
+        manager
+            .create_table(
+                Table::create()
+                    .table(AccessLink::Table)
+                    .col(
+                        ColumnDef::new(AccessLink::Id)
+                            .text()
+                            .not_null()
+                            .primary_key(),
+                    )
+                    .col(
+                        ColumnDef::new(AccessLink::CreatedOn)
+                            .timestamp_with_time_zone()
+                            .not_null()
+                            .default(Expr::current_timestamp()),
+                    )
+                    .col(
+                        ColumnDef::new(AccessLink::IssuedTokens)
+                            .array(ColumnType::Text)
+                            .not_null()
+                            .default(Expr::cust("'{}'")),
+                    )
+                    .col(ColumnDef::new(AccessLink::ExpiresOn).timestamp_with_time_zone())
+                    .col(
+                        ColumnDef::new(AccessLink::TimesUsed)
+                            .integer()
+                            .not_null()
+                            .extra("GENERATED ALWAYS AS (cardinality(issued_tokens)) STORED"),
+                    )
+                    .col(ColumnDef::new(AccessLink::MaximumUses).integer())
+                    .col(ColumnDef::new(AccessLink::IsRevoked).boolean())
+                    .col(ColumnDef::new(AccessLink::IsAccountDefault).boolean())
+                    .col(ColumnDef::new(AccessLink::UserId).text().not_null())
+                    .col(ColumnDef::new(AccessLink::Name).text().not_null())
+                    .col(ColumnDef::new(AccessLink::IsMutationAllowed).boolean())
+                    .col(ColumnDef::new(AccessLink::IsDemo).boolean())
+                    .col(ColumnDef::new(AccessLink::RedirectTo).text())
+                    .foreign_key(
+                        ForeignKey::create()
+                            .name("access_link_to_user_foreign_key")
+                            .from(AccessLink::Table, AccessLink::UserId)
+                            .to(User::Table, User::Id)
+                            .on_delete(ForeignKeyAction::Cascade)
+                            .on_update(ForeignKeyAction::Cascade),
+                    )
+                    .to_owned(),
+            )
+            .await?;
+        Ok(())
+    }
+
+    async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> {
+        Ok(())
+    }
+}
diff --git a/crates/migrations/src/m20240827_create_daily_user_activity.rs b/crates/migrations/src/m20240827_create_daily_user_activity.rs
index 0efd9aa089..e3610cfd5b 100644
--- a/crates/migrations/src/m20240827_create_daily_user_activity.rs
+++ b/crates/migrations/src/m20240827_create_daily_user_activity.rs
@@ -16,6 +16,7 @@ pub enum DailyUserActivity {
     Table,
     UserId,
     Date,
+    EntityIds,
     MetadataReviewCount,
     CollectionReviewCount,
     MetadataGroupReviewCount,
@@ -60,6 +61,12 @@ impl MigrationTrait for Migration {
                     .table(DailyUserActivity::Table)
                     .col(ColumnDef::new(DailyUserActivity::Date).date().not_null())
                     .col(ColumnDef::new(DailyUserActivity::UserId).text().not_null())
+                    .col(
+                        ColumnDef::new(DailyUserActivity::EntityIds)
+                            .array(ColumnType::Text)
+                            .not_null()
+                            .default(Expr::cust("'{}'")),
+                    )
                     .primary_key(
                         Index::create()
                             .name(DAILY_USER_ACTIVITY_PRIMARY_KEY)
diff --git a/crates/migrations/src/m20240831_add_is_account_default_column_to_access_link.rs b/crates/migrations/src/m20240831_add_is_account_default_column_to_access_link.rs
new file mode 100644
index 0000000000..6b9cdd4e3d
--- /dev/null
+++ b/crates/migrations/src/m20240831_add_is_account_default_column_to_access_link.rs
@@ -0,0 +1,22 @@
+use sea_orm_migration::prelude::*;
+
+#[derive(DeriveMigrationName)]
+pub struct Migration;
+
+#[async_trait::async_trait]
+impl MigrationTrait for Migration {
+    async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
+        let db = manager.get_connection();
+        db.execute_unprepared(
+            r#"
+ALTER TABLE "access_link" ADD COLUMN IF NOT EXISTS "is_account_default" boolean
+"#,
+        )
+        .await?;
+        Ok(())
+    }
+
+    async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> {
+        Ok(())
+    }
+}
diff --git a/crates/migrations/src/m20240831_add_templates_key_to_preferences.rs b/crates/migrations/src/m20240831_add_templates_key_to_preferences.rs
new file mode 100644
index 0000000000..d1ebf2ea10
--- /dev/null
+++ b/crates/migrations/src/m20240831_add_templates_key_to_preferences.rs
@@ -0,0 +1,22 @@
+use sea_orm_migration::prelude::*;
+
+#[derive(DeriveMigrationName)]
+pub struct Migration;
+
+#[async_trait::async_trait]
+impl MigrationTrait for Migration {
+    async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
+        let db = manager.get_connection();
+        db.execute_unprepared(
+            r#"
+UPDATE "user" SET "preferences" = jsonb_set("preferences", '{features_enabled,fitness,templates}', 'true');
+"#,
+        )
+        .await?;
+        Ok(())
+    }
+
+    async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> {
+        Ok(())
+    }
+}
diff --git a/crates/migrations/src/m20240918_add_default_rest_timer_to_workout_template.rs b/crates/migrations/src/m20240918_add_default_rest_timer_to_workout_template.rs
new file mode 100644
index 0000000000..da5c716148
--- /dev/null
+++ b/crates/migrations/src/m20240918_add_default_rest_timer_to_workout_template.rs
@@ -0,0 +1,27 @@
+use sea_orm_migration::prelude::*;
+
+#[derive(DeriveMigrationName)]
+pub struct Migration;
+
+#[async_trait::async_trait]
+impl MigrationTrait for Migration {
+    async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
+        let db = manager.get_connection();
+        if !manager
+            .has_column("workout_template", "default_rest_timer")
+            .await?
+        {
+            db.execute_unprepared(
+                r#"
+ALTER TABLE "workout_template" ADD COLUMN "default_rest_timer" INTEGER;
+"#,
+            )
+            .await?;
+        }
+        Ok(())
+    }
+
+    async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> {
+        Ok(())
+    }
+}
diff --git a/crates/migrations/src/m20240926_add_columns_for_open_sourcing_pro_version.rs b/crates/migrations/src/m20240926_add_columns_for_open_sourcing_pro_version.rs
new file mode 100644
index 0000000000..523dc81ef2
--- /dev/null
+++ b/crates/migrations/src/m20240926_add_columns_for_open_sourcing_pro_version.rs
@@ -0,0 +1,86 @@
+use sea_orm_migration::prelude::*;
+
+use crate::{
+    m20231016_create_collection_to_entity::{CONSTRAINT_SQL, ENTITY_ID_SQL, ENTITY_LOT_SQL},
+    m20240904_create_monitored_entity::MONITORED_ENTITY_VIEW_CREATION_SQL,
+};
+
+#[derive(DeriveMigrationName)]
+pub struct Migration;
+
+#[async_trait::async_trait]
+impl MigrationTrait for Migration {
+    async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
+        let db = manager.get_connection();
+        db.execute_unprepared(
+            r#"
+ALTER TABLE "metadata" ADD COLUMN IF NOT EXISTS "is_recommendation" BOOLEAN;
+ALTER TABLE "seen" ADD COLUMN IF NOT EXISTS "manual_time_spent" NUMERIC;
+ALTER TABLE "integration" ADD COLUMN IF NOT EXISTS "sync_to_owned_collection" BOOLEAN;
+
+UPDATE "user" SET "preferences" = jsonb_set("preferences", '{features_enabled,fitness,templates}', 'true');
+"#,
+        )
+        .await?;
+        if !manager.has_column("workout", "template_id").await? {
+            db.execute_unprepared(
+            r#"
+ALTER TABLE "workout" ADD COLUMN "template_id" TEXT;
+ALTER TABLE "workout" ADD CONSTRAINT "template_to_workout_foreign_key" FOREIGN KEY ("template_id") REFERENCES "workout_template"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+"#,
+        )
+        .await?;
+        }
+        if !manager
+            .has_column("collection_to_entity", "workout_template_id")
+            .await?
+        {
+            db.execute_unprepared(
+            r#"
+ALTER TABLE "collection_to_entity" ADD COLUMN "workout_template_id" TEXT;
+ALTER TABLE "collection_to_entity" ADD CONSTRAINT "collection_to_entity-fk7" FOREIGN KEY ("workout_template_id") REFERENCES "workout_template"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+CREATE UNIQUE INDEX "collection_to_entity_uqi6" ON "collection_to_entity" ("collection_id", "workout_template_id");
+"#,
+        )
+        .await?;
+        }
+        db.execute_unprepared(r#"DROP VIEW "monitored_entity""#)
+            .await?;
+        db.execute_unprepared(CONSTRAINT_SQL).await?;
+        db.execute_unprepared(&format!(
+            r#"
+        ALTER TABLE "collection_to_entity" DROP COLUMN "entity_lot";
+        ALTER TABLE "collection_to_entity" ADD COLUMN "entity_lot" TEXT {};
+        "#,
+            ENTITY_LOT_SQL
+        ))
+        .await?;
+        db.execute_unprepared(&format!(
+            r#"
+ALTER TABLE "collection_to_entity" DROP COLUMN "entity_id";
+ALTER TABLE "collection_to_entity" ADD COLUMN "entity_id" TEXT {};
+"#,
+            ENTITY_ID_SQL
+        ))
+        .await?;
+        db.execute_unprepared(MONITORED_ENTITY_VIEW_CREATION_SQL)
+            .await?;
+        if !manager
+            .has_column("daily_user_activity", "entity_ids")
+            .await?
+        {
+            db.execute_unprepared(
+                r#"
+DELETE FROM "daily_user_activity";
+ALTER TABLE "daily_user_activity" ADD COLUMN "entity_ids" TEXT[] NOT NULL DEFAULT '{}';
+        "#,
+            )
+            .await?;
+        }
+        Ok(())
+    }
+
+    async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> {
+        Ok(())
+    }
+}
diff --git a/crates/models/database/src/access_link.rs b/crates/models/database/src/access_link.rs
new file mode 100644
index 0000000000..04f009172c
--- /dev/null
+++ b/crates/models/database/src/access_link.rs
@@ -0,0 +1,59 @@
+//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15
+
+use async_graphql::SimpleObject;
+use async_trait::async_trait;
+use nanoid::nanoid;
+use sea_orm::{entity::prelude::*, ActiveValue};
+
+#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, SimpleObject)]
+#[sea_orm(table_name = "access_link")]
+#[graphql(name = "AccessLink")]
+pub struct Model {
+    #[sea_orm(primary_key)]
+    pub id: String,
+    pub name: String,
+    pub created_on: DateTimeUtc,
+    #[graphql(skip)]
+    pub issued_tokens: Vec<String>,
+    pub expires_on: Option<DateTimeUtc>,
+    pub times_used: i32,
+    pub maximum_uses: Option<i32>,
+    pub is_revoked: Option<bool>,
+    pub is_account_default: Option<bool>,
+    pub is_mutation_allowed: Option<bool>,
+    pub is_demo: Option<bool>,
+    #[graphql(skip)]
+    pub user_id: String,
+    pub redirect_to: Option<String>,
+}
+
+#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
+pub enum Relation {
+    #[sea_orm(
+        belongs_to = "super::user::Entity",
+        from = "Column::UserId",
+        to = "super::user::Column::Id",
+        on_update = "Cascade",
+        on_delete = "Cascade"
+    )]
+    User,
+}
+
+impl Related<super::user::Entity> for Entity {
+    fn to() -> RelationDef {
+        Relation::User.def()
+    }
+}
+
+#[async_trait]
+impl ActiveModelBehavior for ActiveModel {
+    async fn before_save<C>(mut self, _db: &C, insert: bool) -> Result<Self, DbErr>
+    where
+        C: ConnectionTrait,
+    {
+        if insert {
+            self.id = ActiveValue::Set(format!("acl_{}", nanoid!(12)));
+        }
+        Ok(self)
+    }
+}
diff --git a/crates/models/database/src/collection_to_entity.rs b/crates/models/database/src/collection_to_entity.rs
index d5093aec67..6389ed746a 100644
--- a/crates/models/database/src/collection_to_entity.rs
+++ b/crates/models/database/src/collection_to_entity.rs
@@ -21,6 +21,7 @@ pub struct Model {
     pub exercise_id: Option<String>,
     pub workout_id: Option<String>,
     pub information: Option<serde_json::Value>,
+    pub workout_template_id: Option<String>,
 }
 
 #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
@@ -73,6 +74,14 @@ pub enum Relation {
         on_delete = "Cascade"
     )]
     Workout,
+    #[sea_orm(
+        belongs_to = "super::workout_template::Entity",
+        from = "Column::WorkoutTemplateId",
+        to = "super::workout_template::Column::Id",
+        on_update = "Cascade",
+        on_delete = "Cascade"
+    )]
+    WorkoutTemplate,
 }
 
 impl Related<super::collection::Entity> for Entity {
@@ -111,4 +120,10 @@ impl Related<super::workout::Entity> for Entity {
     }
 }
 
+impl Related<super::workout_template::Entity> for Entity {
+    fn to() -> RelationDef {
+        Relation::WorkoutTemplate.def()
+    }
+}
+
 impl ActiveModelBehavior for ActiveModel {}
diff --git a/crates/models/database/src/daily_user_activity.rs b/crates/models/database/src/daily_user_activity.rs
index 05767dc7c9..dad603d890 100644
--- a/crates/models/database/src/daily_user_activity.rs
+++ b/crates/models/database/src/daily_user_activity.rs
@@ -10,6 +10,7 @@ pub struct Model {
     pub user_id: String,
     #[sea_orm(primary_key, auto_increment = false)]
     pub date: Date,
+    pub entity_ids: Vec<String>,
     pub metadata_review_count: i32,
     pub collection_review_count: i32,
     pub metadata_group_review_count: i32,
diff --git a/crates/models/database/src/functions.rs b/crates/models/database/src/functions.rs
index 2f299058e6..10acb8d110 100644
--- a/crates/models/database/src/functions.rs
+++ b/crates/models/database/src/functions.rs
@@ -21,7 +21,7 @@ where
         EntityLot::Person => user_to_entity::Column::PersonId,
         EntityLot::Exercise => user_to_entity::Column::ExerciseId,
         EntityLot::MetadataGroup => user_to_entity::Column::MetadataGroupId,
-        EntityLot::Collection | EntityLot::Workout => unreachable!(),
+        EntityLot::Collection | EntityLot::Workout | EntityLot::WorkoutTemplate => unreachable!(),
     };
     UserToEntity::find()
         .filter(user_to_entity::Column::UserId.eq(user_id.to_owned()))
@@ -58,7 +58,9 @@ where
                 EntityLot::MetadataGroup => {
                     user_to_meta.metadata_group_id = ActiveValue::Set(Some(entity_id))
                 }
-                EntityLot::Collection | EntityLot::Workout => unreachable!(),
+                EntityLot::Collection | EntityLot::Workout | EntityLot::WorkoutTemplate => {
+                    unreachable!()
+                }
             }
             user_to_meta.insert(db).await.unwrap()
         }
diff --git a/crates/models/database/src/integration.rs b/crates/models/database/src/integration.rs
index 1e67e432c6..b00a911dbe 100644
--- a/crates/models/database/src/integration.rs
+++ b/crates/models/database/src/integration.rs
@@ -20,6 +20,7 @@ pub struct Model {
     pub user_id: String,
     pub lot: IntegrationLot,
     pub provider: IntegrationProvider,
+    pub sync_to_owned_collection: Option<bool>,
     pub is_disabled: Option<bool>,
     #[graphql(skip_input)]
     pub created_on: DateTimeUtc,
diff --git a/crates/models/database/src/lib.rs b/crates/models/database/src/lib.rs
index 707158a804..729f63d3d2 100644
--- a/crates/models/database/src/lib.rs
+++ b/crates/models/database/src/lib.rs
@@ -2,6 +2,7 @@
 
 pub mod prelude;
 
+pub mod access_link;
 pub mod calendar_event;
 pub mod collection;
 pub mod collection_to_entity;
@@ -27,3 +28,4 @@ pub mod user;
 pub mod user_measurement;
 pub mod user_to_entity;
 pub mod workout;
+pub mod workout_template;
diff --git a/crates/models/database/src/metadata.rs b/crates/models/database/src/metadata.rs
index d77ddec683..417fafd98a 100644
--- a/crates/models/database/src/metadata.rs
+++ b/crates/models/database/src/metadata.rs
@@ -26,6 +26,7 @@ pub struct Model {
     pub source: MediaSource,
     pub is_nsfw: Option<bool>,
     pub is_partial: Option<bool>,
+    pub is_recommendation: Option<bool>,
     pub description: Option<String>,
     pub original_language: Option<String>,
     pub publish_year: Option<i32>,
diff --git a/crates/models/database/src/prelude.rs b/crates/models/database/src/prelude.rs
index e8f20aa054..4af84d5546 100644
--- a/crates/models/database/src/prelude.rs
+++ b/crates/models/database/src/prelude.rs
@@ -1,5 +1,6 @@
 //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15
 
+pub use super::access_link::Entity as AccessLink;
 pub use super::calendar_event::Entity as CalendarEvent;
 pub use super::collection::Entity as Collection;
 pub use super::collection_to_entity::Entity as CollectionToEntity;
@@ -24,3 +25,4 @@ pub use super::user::Entity as User;
 pub use super::user_measurement::Entity as UserMeasurement;
 pub use super::user_to_entity::Entity as UserToEntity;
 pub use super::workout::Entity as Workout;
+pub use super::workout_template::Entity as WorkoutTemplate;
diff --git a/crates/models/database/src/seen.rs b/crates/models/database/src/seen.rs
index a54b3d3f39..99c46886ee 100644
--- a/crates/models/database/src/seen.rs
+++ b/crates/models/database/src/seen.rs
@@ -42,6 +42,7 @@ pub struct Model {
     pub podcast_extra_information: Option<SeenPodcastExtraInformation>,
     pub anime_extra_information: Option<SeenAnimeExtraInformation>,
     pub manga_extra_information: Option<SeenMangaExtraInformation>,
+    pub manual_time_spent: Option<Decimal>,
     // Generated columns
     pub last_updated_on: DateTimeUtc,
     pub num_times_updated: i32,
diff --git a/crates/models/database/src/user.rs b/crates/models/database/src/user.rs
index 81f332f8d5..0f89fdfce7 100644
--- a/crates/models/database/src/user.rs
+++ b/crates/models/database/src/user.rs
@@ -34,6 +34,8 @@ pub struct Model {
 
 #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
 pub enum Relation {
+    #[sea_orm(has_many = "super::access_link::Entity")]
+    AccessLink,
     #[sea_orm(has_many = "super::collection::Entity")]
     Collection,
     #[sea_orm(has_many = "super::exercise::Entity")]
@@ -56,6 +58,14 @@ pub enum Relation {
     UserToEntity,
     #[sea_orm(has_many = "super::workout::Entity")]
     Workout,
+    #[sea_orm(has_many = "super::workout_template::Entity")]
+    WorkoutTemplate,
+}
+
+impl Related<super::access_link::Entity> for Entity {
+    fn to() -> RelationDef {
+        Relation::AccessLink.def()
+    }
 }
 
 impl Related<super::exercise::Entity> for Entity {
@@ -118,6 +128,12 @@ impl Related<super::workout::Entity> for Entity {
     }
 }
 
+impl Related<super::workout_template::Entity> for Entity {
+    fn to() -> RelationDef {
+        Relation::WorkoutTemplate.def()
+    }
+}
+
 #[async_trait]
 impl ActiveModelBehavior for ActiveModel {
     async fn before_save<C>(mut self, _db: &C, _insert: bool) -> Result<Self, DbErr>
diff --git a/crates/models/database/src/workout.rs b/crates/models/database/src/workout.rs
index 30b17587fd..7460842f69 100644
--- a/crates/models/database/src/workout.rs
+++ b/crates/models/database/src/workout.rs
@@ -32,6 +32,7 @@ pub struct Model {
     pub summary: WorkoutSummary,
     pub information: WorkoutInformation,
     pub name: String,
+    pub template_id: Option<String>,
 }
 
 #[async_trait]
@@ -83,6 +84,14 @@ pub enum Relation {
         on_delete = "SetNull"
     )]
     RepeatedFrom,
+    #[sea_orm(
+        belongs_to = "super::workout_template::Entity",
+        from = "Column::TemplateId",
+        to = "super::workout_template::Column::Id",
+        on_update = "Cascade",
+        on_delete = "SetNull"
+    )]
+    WorkoutTemplate,
 }
 
 impl Related<super::collection_to_entity::Entity> for Entity {
@@ -97,4 +106,10 @@ impl Related<super::user::Entity> for Entity {
     }
 }
 
+impl Related<super::workout_template::Entity> for Entity {
+    fn to() -> RelationDef {
+        Relation::WorkoutTemplate.def()
+    }
+}
+
 impl ActiveModelBehavior for ActiveModel {}
diff --git a/crates/models/database/src/workout_template.rs b/crates/models/database/src/workout_template.rs
new file mode 100644
index 0000000000..71eb430d9d
--- /dev/null
+++ b/crates/models/database/src/workout_template.rs
@@ -0,0 +1,64 @@
+//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15
+
+use async_graphql::SimpleObject;
+use enums::Visibility;
+use fitness_models::{WorkoutInformation, WorkoutSummary};
+use schematic::Schematic;
+use sea_orm::entity::prelude::*;
+use serde::{Deserialize, Serialize};
+
+#[derive(
+    Clone, Debug, PartialEq, DeriveEntityModel, Eq, SimpleObject, Serialize, Deserialize, Schematic,
+)]
+#[sea_orm(table_name = "workout_template")]
+#[graphql(name = "WorkoutTemplate")]
+#[schematic(rename = "WorkoutTemplate", rename_all = "snake_case")]
+pub struct Model {
+    #[sea_orm(primary_key)]
+    pub id: String,
+    pub name: String,
+    pub visibility: Visibility,
+    pub created_on: DateTimeUtc,
+    #[graphql(skip)]
+    #[serde(skip)]
+    pub user_id: String,
+    pub summary: WorkoutSummary,
+    pub information: WorkoutInformation,
+    pub default_rest_timer: Option<i32>,
+}
+
+#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
+pub enum Relation {
+    #[sea_orm(has_many = "super::collection_to_entity::Entity")]
+    CollectionToEntity,
+    #[sea_orm(
+        belongs_to = "super::user::Entity",
+        from = "Column::UserId",
+        to = "super::user::Column::Id",
+        on_update = "Cascade",
+        on_delete = "Cascade"
+    )]
+    User,
+    #[sea_orm(has_many = "super::workout::Entity")]
+    Workout,
+}
+
+impl Related<super::collection_to_entity::Entity> for Entity {
+    fn to() -> RelationDef {
+        Relation::CollectionToEntity.def()
+    }
+}
+
+impl Related<super::user::Entity> for Entity {
+    fn to() -> RelationDef {
+        Relation::User.def()
+    }
+}
+
+impl Related<super::workout::Entity> for Entity {
+    fn to() -> RelationDef {
+        Relation::Workout.def()
+    }
+}
+
+impl ActiveModelBehavior for ActiveModel {}
diff --git a/crates/models/dependent/src/lib.rs b/crates/models/dependent/src/lib.rs
index 4109b0878c..a12bb1cce1 100644
--- a/crates/models/dependent/src/lib.rs
+++ b/crates/models/dependent/src/lib.rs
@@ -2,7 +2,7 @@ use async_graphql::{InputObject, OutputType, SimpleObject, Union};
 use common_models::{BackendError, SearchDetails};
 use database_models::{
     collection, exercise, metadata, metadata_group, person, seen, user, user_measurement,
-    user_to_entity, workout,
+    user_to_entity, workout, workout_template,
 };
 use enums::UserToMediaReason;
 use fitness_models::{UserToExerciseHistoryExtraInformation, UserWorkoutInput};
@@ -36,6 +36,7 @@ use serde_with::skip_serializing_none;
 ))]
 #[graphql(concrete(name = "GenreListResults", params(media_models::GenreListItem)))]
 #[graphql(concrete(name = "WorkoutListResults", params(workout::Model)))]
+#[graphql(concrete(name = "WorkoutTemplateListResults", params(workout_template::Model)))]
 #[graphql(concrete(name = "IdResults", params(String)))]
 pub struct SearchResults<T: OutputType> {
     pub details: SearchDetails,
@@ -53,6 +54,12 @@ pub struct ImportOrExportWorkoutItem {
     pub collections: Vec<String>,
 }
 
+#[derive(Debug, SimpleObject, Clone, Serialize, Deserialize, Schematic)]
+pub struct ImportOrExportWorkoutTemplateItem {
+    pub details: workout_template::Model,
+    pub collections: Vec<String>,
+}
+
 /// Complete export of the user.
 #[skip_serializing_none]
 #[derive(Debug, Serialize, Deserialize, Clone, Schematic)]
@@ -70,6 +77,8 @@ pub struct CompleteExport {
     pub media_group: Option<Vec<media_models::ImportOrExportMediaGroupItem>>,
     /// Data about user's exercises.
     pub exercises: Option<Vec<ImportOrExportExerciseItem>>,
+    /// Data about user's workout templates.
+    pub workout_templates: Option<Vec<ImportOrExportWorkoutTemplateItem>>,
 }
 
 #[derive(Debug, Serialize, Deserialize, SimpleObject, Clone)]
@@ -213,3 +222,9 @@ pub struct DailyUserActivitiesResponse {
     pub items: Vec<DailyUserActivityItem>,
     pub grouped_by: DailyUserActivitiesResponseGroupedBy,
 }
+
+#[derive(Debug, SimpleObject, Clone, Serialize, Deserialize, Schematic)]
+pub struct UserWorkoutTemplateDetails {
+    pub details: workout_template::Model,
+    pub collections: Vec<collection::Model>,
+}
diff --git a/crates/models/fitness/Cargo.toml b/crates/models/fitness/Cargo.toml
index 8656f01584..0f88738ddf 100644
--- a/crates/models/fitness/Cargo.toml
+++ b/crates/models/fitness/Cargo.toml
@@ -8,11 +8,7 @@ application-utils = { path = "../../utils/application" }
 async-graphql = { workspace = true }
 async-trait = { workspace = true }
 common-models = { path = "../../models/common" }
-derive_more = { version = "=1.0.0", features = [
-  "add",
-  "sum",
-  "add_assign",
-], default-features = false }
+derive_more = { workspace = true }
 enums = { path = "../../enums" }
 file-storage-service = { path = "../../services/file-storage" }
 rust_decimal = { workspace = true }
diff --git a/crates/models/fitness/src/lib.rs b/crates/models/fitness/src/lib.rs
index 9743dd55f1..af24fa847c 100644
--- a/crates/models/fitness/src/lib.rs
+++ b/crates/models/fitness/src/lib.rs
@@ -519,9 +519,12 @@ pub struct UserWorkoutInput {
     pub end_time: DateTimeUtc,
     pub comment: Option<String>,
     pub start_time: DateTimeUtc,
+    pub template_id: Option<String>,
     pub assets: Option<EntityAssets>,
     pub repeated_from: Option<String>,
+    pub default_rest_timer: Option<i32>,
     pub exercises: Vec<UserExerciseInput>,
+    pub update_workout_template_id: Option<String>,
 }
 
 impl UserWorkoutSetRecord {
diff --git a/crates/models/media/src/lib.rs b/crates/models/media/src/lib.rs
index aa92781dfb..b8edaa5892 100644
--- a/crates/models/media/src/lib.rs
+++ b/crates/models/media/src/lib.rs
@@ -40,7 +40,7 @@ pub struct CreateOrUpdateCollectionInput {
     pub name: String,
     pub description: Option<String>,
     pub update_id: Option<String>,
-    #[graphql(skip_input)]
+    pub collaborators: Option<Vec<String>>,
     pub information_template: Option<Vec<CollectionExtraInformation>>,
 }
 
@@ -838,6 +838,7 @@ pub struct PartialMetadata {
     pub image: Option<String>,
     pub lot: MediaLot,
     pub source: MediaSource,
+    pub is_recommendation: Option<bool>,
 }
 
 #[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize, SimpleObject, FromQueryResult)]
@@ -885,7 +886,6 @@ pub struct CommitMediaInput {
     pub lot: MediaLot,
     pub source: MediaSource,
     pub identifier: String,
-    #[graphql(skip_input)]
     pub force_update: Option<bool>,
 }
 
@@ -1095,6 +1095,7 @@ pub struct CreateUserIntegrationInput {
     pub provider_specifics: Option<IntegrationProviderSpecifics>,
     pub minimum_progress: Option<Decimal>,
     pub maximum_progress: Option<Decimal>,
+    pub sync_to_owned_collection: Option<bool>,
 }
 
 #[derive(Debug, Serialize, Deserialize, InputObject, Clone)]
@@ -1103,6 +1104,7 @@ pub struct UpdateUserIntegrationInput {
     pub is_disabled: Option<bool>,
     pub minimum_progress: Option<Decimal>,
     pub maximum_progress: Option<Decimal>,
+    pub sync_to_owned_collection: Option<bool>,
 }
 
 #[derive(Debug, Serialize, Deserialize, InputObject, Clone)]
@@ -1472,6 +1474,7 @@ pub struct UpdateSeenItemInput {
     pub seen_id: String,
     pub started_on: Option<NaiveDate>,
     pub finished_on: Option<NaiveDate>,
+    pub manual_time_spent: Option<Decimal>,
     pub provider_watched_on: Option<String>,
 }
 
@@ -1559,6 +1562,48 @@ pub struct GroupedCalendarEvent {
     pub date: NaiveDate,
 }
 
+#[derive(Debug, Serialize, Deserialize, InputObject, Clone)]
+pub struct CreateAccessLinkInput {
+    pub name: String,
+    pub maximum_uses: Option<i32>,
+    pub expires_on: Option<DateTimeUtc>,
+    pub redirect_to: Option<String>,
+    pub is_mutation_allowed: Option<bool>,
+    pub is_account_default: Option<bool>,
+}
+
+#[derive(Debug, Serialize, Deserialize, OneofObject, Clone)]
+pub enum ProcessAccessLinkInput {
+    Id(String),
+    Username(String),
+}
+
+#[derive(Enum, Clone, Debug, Copy, PartialEq, Eq)]
+pub enum ProcessAccessLinkErrorVariant {
+    Expired,
+    Revoked,
+    NotFound,
+    MaximumUsesReached,
+}
+
+#[derive(Debug, SimpleObject)]
+pub struct ProcessAccessLinkError {
+    pub error: ProcessAccessLinkErrorVariant,
+}
+
+#[derive(Debug, SimpleObject)]
+pub struct ProcessAccessLinkResponse {
+    pub api_key: String,
+    pub token_valid_for_days: i32,
+    pub redirect_to: Option<String>,
+}
+
+#[derive(Union)]
+pub enum ProcessAccessLinkResult {
+    Ok(ProcessAccessLinkResponse),
+    Error(ProcessAccessLinkError),
+}
+
 #[derive(Debug, Serialize, Deserialize, Enum, Clone, Copy, Eq, PartialEq, Display)]
 #[strum(serialize_all = "snake_case")]
 pub enum DailyUserActivitiesResponseGroupedBy {
diff --git a/crates/models/user/src/lib.rs b/crates/models/user/src/lib.rs
index 2346c03a2a..978de17c36 100644
--- a/crates/models/user/src/lib.rs
+++ b/crates/models/user/src/lib.rs
@@ -97,6 +97,7 @@ pub struct UserFitnessFeaturesEnabledPreferences {
     pub enabled: bool,
     pub measurements: bool,
     pub workouts: bool,
+    pub templates: bool,
 }
 
 impl Default for UserFitnessFeaturesEnabledPreferences {
@@ -105,6 +106,7 @@ impl Default for UserFitnessFeaturesEnabledPreferences {
             enabled: true,
             measurements: true,
             workouts: true,
+            templates: true,
         }
     }
 }
@@ -277,7 +279,7 @@ pub enum DashboardElementLot {
 pub struct UserGeneralDashboardElement {
     pub section: DashboardElementLot,
     pub hidden: bool,
-    pub num_elements: Option<i32>,
+    pub num_elements: Option<u64>,
 }
 
 #[derive(
diff --git a/crates/providers/src/anilist/mod.rs b/crates/providers/src/anilist/mod.rs
index 1637b50bfb..e3ec8a7bc4 100644
--- a/crates/providers/src/anilist/mod.rs
+++ b/crates/providers/src/anilist/mod.rs
@@ -293,6 +293,7 @@ impl MediaProvider for NonMediaAnilistService {
                                 studio_query::MediaType::Other(_) => unreachable!(),
                             },
                             image: data.cover_image.unwrap().extra_large,
+                            is_recommendation: None,
                         },
                     )
                 })
@@ -381,6 +382,7 @@ impl MediaProvider for NonMediaAnilistService {
                                 staff_query::MediaType::Other(_) => unreachable!(),
                             },
                             image: data.cover_image.unwrap().extra_large,
+                            is_recommendation: None,
                         },
                     )
                 })
@@ -413,6 +415,7 @@ impl MediaProvider for NonMediaAnilistService {
                                     staff_query::MediaType::Other(_) => unreachable!(),
                                 },
                                 image: data.cover_image.unwrap().extra_large,
+                                is_recommendation: None,
                             },
                         )
                     }),
@@ -670,6 +673,7 @@ async fn media_details(
                         media_details_query::MediaType::Other(_) => unreachable!(),
                     },
                     image: data.cover_image.unwrap().extra_large,
+                    is_recommendation: None,
                 }
             })
         })
diff --git a/crates/providers/src/audible.rs b/crates/providers/src/audible.rs
index 6729b57f7f..77cd391c32 100644
--- a/crates/providers/src/audible.rs
+++ b/crates/providers/src/audible.rs
@@ -305,6 +305,7 @@ impl MediaProvider for AudibleService {
                 identifier: i,
                 source: MediaSource::Audible,
                 lot: MediaLot::AudioBook,
+                is_recommendation: None,
             })
         }
         Ok((
@@ -358,6 +359,7 @@ impl MediaProvider for AudibleService {
                     identifier: sim.asin,
                     source: MediaSource::Audible,
                     lot: MediaLot::AudioBook,
+                    is_recommendation: None,
                 });
             }
         }
diff --git a/crates/providers/src/igdb.rs b/crates/providers/src/igdb.rs
index 63f9ea99b7..86dd214e51 100644
--- a/crates/providers/src/igdb.rs
+++ b/crates/providers/src/igdb.rs
@@ -194,7 +194,7 @@ impl MediaProvider for IgdbService {
         page: Option<i32>,
         _display_nsfw: bool,
     ) -> Result<SearchResults<MetadataGroupSearchItem>> {
-        let client = get_client(&self.config).await;
+        let client = self.get_client().await;
         let req_body = format!(
             r#"
 {fields}
@@ -236,7 +236,7 @@ offset: {offset};
         &self,
         identifier: &str,
     ) -> Result<(MetadataGroupWithoutId, Vec<PartialMetadataWithoutId>)> {
-        let client = get_client(&self.config).await;
+        let client = self.get_client().await;
         let req_body = format!(
             r"
 {fields}
@@ -270,6 +270,7 @@ where id = {id};
                         image: g.cover.map(|c| self.get_cover_image_url(c.image_id)),
                         source: MediaSource::Igdb,
                         lot: MediaLot::VideoGame,
+                        is_recommendation: None,
                     })
                 }
             })
@@ -296,7 +297,7 @@ where id = {id};
         _source_specifics: &Option<PersonSourceSpecifics>,
         _display_nsfw: bool,
     ) -> Result<SearchResults<PeopleSearchItem>> {
-        let client = get_client(&self.config).await;
+        let client = self.get_client().await;
         let req_body = format!(
             r#"
 {fields}
@@ -342,7 +343,7 @@ offset: {offset};
         identity: &str,
         _source_specifics: &Option<PersonSourceSpecifics>,
     ) -> Result<MetadataPerson> {
-        let client = get_client(&self.config).await;
+        let client = self.get_client().await;
         let req_body = format!(
             r#"
 {fields}
@@ -376,6 +377,7 @@ where id = {id};
                         source: MediaSource::Igdb,
                         lot: MediaLot::VideoGame,
                         image,
+                        is_recommendation: None,
                     },
                 )
             })
@@ -390,6 +392,7 @@ where id = {id};
                     source: MediaSource::Igdb,
                     lot: MediaLot::VideoGame,
                     image,
+                    is_recommendation: None,
                 },
             )
         }));
@@ -419,7 +422,7 @@ where id = {id};
     }
 
     async fn metadata_details(&self, identifier: &str) -> Result<MediaDetails> {
-        let client = get_client(&self.config).await;
+        let client = self.get_client().await;
         let req_body = format!(
             r#"{field} where id = {id};"#,
             field = GAME_FIELDS,
@@ -451,7 +454,7 @@ where id = {id};
         _display_nsfw: bool,
     ) -> Result<SearchResults<MetadataSearchItem>> {
         let page = page.unwrap_or(1);
-        let client = get_client(&self.config).await;
+        let client = self.get_client().await;
         let count_req_body =
             format!(r#"fields id; where version_parent = null; search "{query}"; limit: 500;"#);
         let rsp = client
@@ -509,6 +512,55 @@ offset: {offset};
 }
 
 impl IgdbService {
+    async fn get_access_token(&self) -> String {
+        let client = Client::new();
+        #[derive(Deserialize, Serialize, Default, Debug)]
+        struct AccessResponse {
+            access_token: String,
+            token_type: String,
+            expires_in: u128,
+        }
+        let access_res = client
+            .post(AUTH_URL)
+            .query(&json!({
+                "client_id": self.config.twitch.client_id.to_owned(),
+                "client_secret": self.config.twitch.client_secret.to_owned(),
+                "grant_type": "client_credentials".to_owned(),
+            }))
+            .send()
+            .await
+            .unwrap();
+        let access = access_res
+            .json::<AccessResponse>()
+            .await
+            .unwrap_or_default();
+        format!("{} {}", access.token_type, access.access_token)
+    }
+
+    async fn get_client(&self) -> Client {
+        let path = PathBuf::new().join(TEMP_DIR).join(FILE);
+        let settings = if !path.exists() {
+            let tok = self.get_access_token().await;
+            let settings = Settings { access_token: tok };
+            let data_to_write = serde_json::to_string(&settings).unwrap();
+            fs::write(path, data_to_write).unwrap();
+            settings
+        } else {
+            let data = fs::read_to_string(path).unwrap();
+            serde_json::from_str(&data).unwrap()
+        };
+        get_base_http_client(Some(vec![
+            (
+                HeaderName::from_static("client-id"),
+                HeaderValue::from_str(&self.config.twitch.client_id).unwrap(),
+            ),
+            (
+                AUTHORIZATION,
+                HeaderValue::from_str(&settings.access_token).unwrap(),
+            ),
+        ]))
+    }
+
     fn igdb_response_to_search_response(&self, item: IgdbItemResponse) -> MediaDetails {
         let mut images = Vec::from_iter(item.cover.map(|a| MetadataImageForMediaDetails {
             image: self.get_cover_image_url(a.image_id),
@@ -593,6 +645,7 @@ impl IgdbService {
                     identifier: g.id.to_string(),
                     lot: MediaLot::VideoGame,
                     source: MediaSource::Igdb,
+                    is_recommendation: None,
                 })
                 .collect(),
             provider_rating: item.rating,
@@ -604,52 +657,3 @@ impl IgdbService {
         format!("{}/{}/{}.jpg", self.image_url, self.image_size, hash)
     }
 }
-
-async fn get_access_token(config: &config::VideoGameConfig) -> String {
-    let client = Client::new();
-    #[derive(Deserialize, Serialize, Default, Debug)]
-    struct AccessResponse {
-        access_token: String,
-        token_type: String,
-        expires_in: u128,
-    }
-    let access_res = client
-        .post(AUTH_URL)
-        .query(&json!({
-            "client_id": config.twitch.client_id.to_owned(),
-            "client_secret": config.twitch.client_secret.to_owned(),
-            "grant_type": "client_credentials".to_owned(),
-        }))
-        .send()
-        .await
-        .unwrap();
-    let access = access_res
-        .json::<AccessResponse>()
-        .await
-        .unwrap_or_default();
-    format!("{} {}", access.token_type, access.access_token)
-}
-
-async fn get_client(config: &config::VideoGameConfig) -> Client {
-    let path = PathBuf::new().join(TEMP_DIR).join(FILE);
-    let settings = if !path.exists() {
-        let tok = get_access_token(config).await;
-        let settings = Settings { access_token: tok };
-        let data_to_write = serde_json::to_string(&settings).unwrap();
-        fs::write(path, data_to_write).unwrap();
-        settings
-    } else {
-        let data = fs::read_to_string(path).unwrap();
-        serde_json::from_str(&data).unwrap()
-    };
-    get_base_http_client(Some(vec![
-        (
-            HeaderName::from_static("client-id"),
-            HeaderValue::from_str(&config.twitch.client_id).unwrap(),
-        ),
-        (
-            AUTHORIZATION,
-            HeaderValue::from_str(&settings.access_token).unwrap(),
-        ),
-    ]))
-}
diff --git a/crates/providers/src/listennotes.rs b/crates/providers/src/listennotes.rs
index f75c58e0cb..1f37020806 100644
--- a/crates/providers/src/listennotes.rs
+++ b/crates/providers/src/listennotes.rs
@@ -103,6 +103,7 @@ impl MediaProvider for ListennotesService {
                 identifier: r.id,
                 lot: MediaLot::Podcast,
                 source: MediaSource::Listennotes,
+                is_recommendation: None,
             })
             .collect();
 
diff --git a/crates/providers/src/mal.rs b/crates/providers/src/mal.rs
index 7fe2114627..89f6a860a7 100644
--- a/crates/providers/src/mal.rs
+++ b/crates/providers/src/mal.rs
@@ -238,6 +238,7 @@ async fn details(client: &Client, media_type: &str, id: &str) -> Result<MediaDet
             image: Some(rel.node.main_picture.large),
             source: MediaSource::Mal,
             lot: MediaLot::Anime,
+            is_recommendation: None,
         });
     }
     for rel in details.related_manga.unwrap_or_default().into_iter() {
@@ -247,6 +248,7 @@ async fn details(client: &Client, media_type: &str, id: &str) -> Result<MediaDet
             image: Some(rel.node.main_picture.large),
             source: MediaSource::Mal,
             lot: MediaLot::Manga,
+            is_recommendation: None,
         });
     }
     for rel in details.recommendations.unwrap_or_default().into_iter() {
@@ -256,6 +258,7 @@ async fn details(client: &Client, media_type: &str, id: &str) -> Result<MediaDet
             image: Some(rel.node.main_picture.large),
             source: MediaSource::Mal,
             lot,
+            is_recommendation: None,
         });
     }
     suggestions.shuffle(&mut thread_rng());
diff --git a/crates/providers/src/manga_updates.rs b/crates/providers/src/manga_updates.rs
index ce50d8f063..e89bf34f17 100644
--- a/crates/providers/src/manga_updates.rs
+++ b/crates/providers/src/manga_updates.rs
@@ -246,6 +246,7 @@ impl MediaProvider for MangaUpdatesService {
                         source: MediaSource::MangaUpdates,
                         lot: MediaLot::Manga,
                         image: None,
+                        is_recommendation: None,
                     },
                 )
             })
@@ -326,6 +327,7 @@ impl MediaProvider for MangaUpdatesService {
                     identifier: data.series_id.unwrap().to_string(),
                     source: MediaSource::MangaUpdates,
                     lot: MediaLot::Manga,
+                    is_recommendation: None,
                 });
             }
         }
diff --git a/crates/providers/src/openlibrary.rs b/crates/providers/src/openlibrary.rs
index 132d26b8c6..a0049d92e3 100644
--- a/crates/providers/src/openlibrary.rs
+++ b/crates/providers/src/openlibrary.rs
@@ -268,6 +268,7 @@ impl MediaProvider for OpenlibraryService {
                         title,
                         lot: MediaLot::Book,
                         source: MediaSource::Openlibrary,
+                        is_recommendation: None,
                         image,
                     },
                 ))
@@ -442,6 +443,7 @@ impl MediaProvider for OpenlibraryService {
                     identifier,
                     lot: MediaLot::Book,
                     source: MediaSource::Openlibrary,
+                    is_recommendation: None,
                 });
             }
         }
diff --git a/crates/providers/src/tmdb.rs b/crates/providers/src/tmdb.rs
index 02151a1064..d2b15e5168 100644
--- a/crates/providers/src/tmdb.rs
+++ b/crates/providers/src/tmdb.rs
@@ -257,6 +257,7 @@ impl TmdbService {
                     identifier: entry.id.to_string(),
                     source: MediaSource::Tmdb,
                     lot,
+                    is_recommendation: None,
                 });
             }
             if new_recs.page >= new_recs.total_pages {
@@ -515,6 +516,7 @@ impl MediaProvider for NonMediaTmdbService {
                                 _ => continue,
                             },
                             source: MediaSource::Tmdb,
+                            is_recommendation: None,
                         },
                     ));
                 }
@@ -547,6 +549,7 @@ impl MediaProvider for NonMediaTmdbService {
                                     _ => unreachable!(),
                                 },
                                 source: MediaSource::Tmdb,
+                                is_recommendation: None,
                             },
                         )
                     }));
@@ -932,6 +935,7 @@ impl MediaProvider for TmdbMovieService {
                 source: MediaSource::Tmdb,
                 lot: MediaLot::Movie,
                 image: p.poster_path.map(|p| self.base.get_image_url(p)),
+                is_recommendation: None,
             })
             .collect_vec();
         Ok((
@@ -954,6 +958,13 @@ impl MediaProvider for TmdbMovieService {
             parts,
         ))
     }
+
+    async fn get_recommendations_for_metadata(
+        &self,
+        identifier: &str,
+    ) -> Result<Vec<PartialMetadataWithoutId>> {
+        self.base.get_all_suggestions("movie", identifier).await
+    }
 }
 
 #[derive(Debug, Clone)]
@@ -1294,6 +1305,13 @@ impl MediaProvider for TmdbShowService {
             items: resp.to_vec(),
         })
     }
+
+    async fn get_recommendations_for_metadata(
+        &self,
+        identifier: &str,
+    ) -> Result<Vec<PartialMetadataWithoutId>> {
+        self.base.get_all_suggestions("tv", identifier).await
+    }
 }
 
 async fn get_client_config(access_token: &str) -> (Client, Settings) {
diff --git a/crates/resolvers/fitness/src/lib.rs b/crates/resolvers/fitness/src/lib.rs
index f21d652c79..1af176b8fa 100644
--- a/crates/resolvers/fitness/src/lib.rs
+++ b/crates/resolvers/fitness/src/lib.rs
@@ -2,9 +2,10 @@ use std::sync::Arc;
 
 use async_graphql::{Context, Object, Result};
 use common_models::SearchInput;
-use database_models::{exercise, user_measurement, workout};
+use database_models::{exercise, user_measurement, workout, workout_template};
 use dependent_models::{
     SearchResults, UpdateCustomExerciseInput, UserExerciseDetails, UserWorkoutDetails,
+    UserWorkoutTemplateDetails,
 };
 use fitness_models::{
     ExerciseListItem, ExerciseParameters, ExercisesListInput, UpdateUserWorkoutInput,
@@ -21,6 +22,30 @@ impl AuthProvider for ExerciseQuery {}
 
 #[Object]
 impl ExerciseQuery {
+    /// Get a paginated list of templates created by the user.
+    async fn user_workout_templates_list(
+        &self,
+        gql_ctx: &Context<'_>,
+        input: SearchInput,
+    ) -> Result<SearchResults<workout_template::Model>> {
+        let service = gql_ctx.data_unchecked::<Arc<ExerciseService>>();
+        let user_id = self.user_id_from_ctx(gql_ctx).await?;
+        service.user_workout_templates_list(user_id, input).await
+    }
+
+    /// Get information about a workout template.
+    async fn workout_template_details(
+        &self,
+        gql_ctx: &Context<'_>,
+        workout_template_id: String,
+    ) -> Result<UserWorkoutTemplateDetails> {
+        let service = gql_ctx.data_unchecked::<Arc<ExerciseService>>();
+        let user_id = self.user_id_from_ctx(gql_ctx).await?;
+        service
+            .workout_template_details(user_id, workout_template_id)
+            .await
+    }
+
     /// Get all the parameters related to exercises.
     async fn exercise_parameters(&self, gql_ctx: &Context<'_>) -> Result<ExerciseParameters> {
         let service = gql_ctx.data_unchecked::<Arc<ExerciseService>>();
@@ -104,6 +129,32 @@ impl AuthProvider for ExerciseMutation {
 
 #[Object]
 impl ExerciseMutation {
+    /// Create or update a workout template.
+    async fn create_or_update_workout_template(
+        &self,
+        gql_ctx: &Context<'_>,
+        input: UserWorkoutInput,
+    ) -> Result<String> {
+        let service = gql_ctx.data_unchecked::<Arc<ExerciseService>>();
+        let user_id = self.user_id_from_ctx(gql_ctx).await?;
+        service
+            .create_or_update_workout_template(user_id, input)
+            .await
+    }
+
+    /// Delete a workout template.
+    async fn delete_workout_template(
+        &self,
+        gql_ctx: &Context<'_>,
+        workout_template_id: String,
+    ) -> Result<bool> {
+        let service = gql_ctx.data_unchecked::<Arc<ExerciseService>>();
+        let user_id = self.user_id_from_ctx(gql_ctx).await?;
+        service
+            .delete_workout_template(user_id, workout_template_id)
+            .await
+    }
+
     /// Create a user measurement.
     async fn create_user_measurement(
         &self,
diff --git a/crates/resolvers/user/src/lib.rs b/crates/resolvers/user/src/lib.rs
index 8c067e20dd..c9bf68ab1c 100644
--- a/crates/resolvers/user/src/lib.rs
+++ b/crates/resolvers/user/src/lib.rs
@@ -2,11 +2,12 @@ use std::sync::Arc;
 
 use async_graphql::{Context, Object, Result};
 use common_models::StringIdObject;
-use database_models::{integration, notification_platform, user};
+use database_models::{access_link, integration, notification_platform, user};
 use dependent_models::UserDetailsResult;
 use media_models::{
-    AuthUserInput, CreateUserIntegrationInput, CreateUserNotificationPlatformInput, LoginResult,
-    OidcTokenOutput, RegisterResult, RegisterUserInput, UpdateUserInput,
+    AuthUserInput, CreateAccessLinkInput, CreateUserIntegrationInput,
+    CreateUserNotificationPlatformInput, LoginResult, OidcTokenOutput, ProcessAccessLinkInput,
+    ProcessAccessLinkResult, RegisterResult, RegisterUserInput, UpdateUserInput,
     UpdateUserIntegrationInput, UpdateUserNotificationPlatformInput, UpdateUserPreferenceInput,
 };
 use traits::AuthProvider;
@@ -20,6 +21,20 @@ impl AuthProvider for UserQuery {}
 
 #[Object]
 impl UserQuery {
+    /// Get media recommendations for the currently logged in user.
+    async fn user_recommendations(&self, gql_ctx: &Context<'_>) -> Result<Vec<String>> {
+        let service = gql_ctx.data_unchecked::<Arc<UserService>>();
+        let user_id = self.user_id_from_ctx(gql_ctx).await?;
+        service.user_recommendations(&user_id).await
+    }
+
+    /// Get all access links generated by the currently logged in user.
+    async fn user_access_links(&self, gql_ctx: &Context<'_>) -> Result<Vec<access_link::Model>> {
+        let service = gql_ctx.data_unchecked::<Arc<UserService>>();
+        let user_id = self.user_id_from_ctx(gql_ctx).await?;
+        service.user_access_links(&user_id).await
+    }
+
     /// Get details about all the users in the service.
     async fn users_list(
         &self,
@@ -95,6 +110,38 @@ impl AuthProvider for UserMutation {
 
 #[Object]
 impl UserMutation {
+    /// Create or edit an access link.
+    async fn create_access_link(
+        &self,
+        gql_ctx: &Context<'_>,
+        input: CreateAccessLinkInput,
+    ) -> Result<StringIdObject> {
+        let service = gql_ctx.data_unchecked::<Arc<UserService>>();
+        let user_id = self.user_id_from_ctx(gql_ctx).await?;
+        service.create_access_link(input, user_id).await
+    }
+
+    /// Get an access token using an access link.
+    async fn process_access_link(
+        &self,
+        gql_ctx: &Context<'_>,
+        input: ProcessAccessLinkInput,
+    ) -> Result<ProcessAccessLinkResult> {
+        let service = gql_ctx.data_unchecked::<Arc<UserService>>();
+        service.process_access_link(input).await
+    }
+
+    /// Revoke an access link.
+    async fn revoke_access_link(
+        &self,
+        gql_ctx: &Context<'_>,
+        access_link_id: String,
+    ) -> Result<bool> {
+        let service = gql_ctx.data_unchecked::<Arc<UserService>>();
+        self.user_id_from_ctx(gql_ctx).await?;
+        service.revoke_access_link(access_link_id).await
+    }
+
     /// Delete a user. The account deleting the user must be an `Admin`.
     async fn delete_user(&self, gql_ctx: &Context<'_>, to_delete_user_id: String) -> Result<bool> {
         let service = gql_ctx.data_unchecked::<Arc<UserService>>();
diff --git a/crates/services/collection/src/lib.rs b/crates/services/collection/src/lib.rs
index 2cba2e9a41..69a9580e18 100644
--- a/crates/services/collection/src/lib.rs
+++ b/crates/services/collection/src/lib.rs
@@ -239,6 +239,9 @@ impl CollectionService {
                             collection_to_entity::Column::ExerciseId.is_not_null()
                         }
                         EntityLot::Workout => collection_to_entity::Column::WorkoutId.is_not_null(),
+                        EntityLot::WorkoutTemplate => {
+                            collection_to_entity::Column::WorkoutTemplateId.is_not_null()
+                        }
                         EntityLot::Collection => unreachable!(),
                     };
                     query.filter(f)
diff --git a/crates/services/exporter/src/lib.rs b/crates/services/exporter/src/lib.rs
index 8c7211c17e..56158b6285 100644
--- a/crates/services/exporter/src/lib.rs
+++ b/crates/services/exporter/src/lib.rs
@@ -7,13 +7,16 @@ use chrono::{DateTime, Utc};
 use common_models::ExportJob;
 use common_utils::{IsFeatureEnabled, TEMP_DIR};
 use database_models::{
-    prelude::{Exercise, Metadata, MetadataGroup, Person, Seen, UserToEntity, Workout},
-    seen, user_to_entity, workout,
+    prelude::{
+        Exercise, Metadata, MetadataGroup, Person, Seen, UserToEntity, Workout, WorkoutTemplate,
+    },
+    seen, user_to_entity, workout, workout_template,
 };
 use database_utils::{
     entity_in_collections, item_reviews, user_measurements_list, workout_details,
+    workout_template_details,
 };
-use dependent_models::ImportOrExportWorkoutItem;
+use dependent_models::{ImportOrExportWorkoutItem, ImportOrExportWorkoutTemplateItem};
 use enums::EntityLot;
 use file_storage_service::FileStorageService;
 use fitness_models::UserMeasurementsListInput;
@@ -44,6 +47,7 @@ enum ExportItem {
     Exercises,
     MediaGroups,
     Measurements,
+    WorkoutTemplates,
 }
 
 pub struct ExporterService {
@@ -134,6 +138,9 @@ impl ExporterService {
                 ExportItem::Exercises => self.export_exercises(&user_id, &mut writer).await?,
                 ExportItem::MediaGroups => self.export_media_group(&user_id, &mut writer).await?,
                 ExportItem::Measurements => self.export_measurements(&user_id, &mut writer).await?,
+                ExportItem::WorkoutTemplates => {
+                    self.export_workout_templates(&user_id, &mut writer).await?
+                }
             };
             writer.end_array().unwrap();
         }
@@ -416,6 +423,30 @@ impl ExporterService {
         Ok(())
     }
 
+    async fn export_workout_templates(
+        &self,
+        user_id: &String,
+        writer: &mut JsonStreamWriter<StdFile>,
+    ) -> Result<()> {
+        let workout_template_ids = WorkoutTemplate::find()
+            .select_only()
+            .column(workout_template::Column::Id)
+            .filter(workout_template::Column::UserId.eq(user_id))
+            .order_by_desc(workout_template::Column::CreatedOn)
+            .into_tuple::<String>()
+            .all(&self.db)
+            .await?;
+        for workout_template_id in workout_template_ids {
+            let details = workout_template_details(&self.db, user_id, workout_template_id).await?;
+            let exp = ImportOrExportWorkoutTemplateItem {
+                details: details.details,
+                collections: details.collections.into_iter().map(|c| c.name).collect(),
+            };
+            writer.serialize_value(&exp).unwrap();
+        }
+        Ok(())
+    }
+
     fn get_review_export_item(&self, rev: ReviewItem) -> ImportOrExportItemRating {
         let (show_season_number, show_episode_number) = match rev.show_extra_information {
             Some(d) => (Some(d.season), Some(d.episode)),
diff --git a/crates/services/fitness/src/lib.rs b/crates/services/fitness/src/lib.rs
index 8bce6a9477..59d165cbc3 100644
--- a/crates/services/fitness/src/lib.rs
+++ b/crates/services/fitness/src/lib.rs
@@ -10,35 +10,40 @@ use common_models::{
 use common_utils::ryot_log;
 use database_models::{
     collection_to_entity, exercise,
-    prelude::{CollectionToEntity, Exercise, UserMeasurement, UserToEntity, Workout},
-    user_measurement, user_to_entity, workout,
+    prelude::{
+        CollectionToEntity, Exercise, UserMeasurement, UserToEntity, Workout, WorkoutTemplate,
+    },
+    user_measurement, user_to_entity, workout, workout_template,
 };
 use database_utils::{
-    add_entity_to_collection, entity_in_collections, ilike_sql, item_reviews,
-    user_measurements_list, workout_details,
+    add_entity_to_collection, entity_in_collections, ilike_sql, item_reviews, pro_instance_guard,
+    user_measurements_list, workout_details, workout_template_details,
 };
 use dependent_models::{
     SearchResults, UpdateCustomExerciseInput, UserExerciseDetails, UserWorkoutDetails,
+    UserWorkoutTemplateDetails,
 };
 use enums::{
     EntityLot, ExerciseEquipment, ExerciseForce, ExerciseLevel, ExerciseLot, ExerciseMechanic,
-    ExerciseMuscle, ExerciseSource,
+    ExerciseMuscle, ExerciseSource, Visibility,
 };
 use file_storage_service::FileStorageService;
 use fitness_models::{
     ExerciseAttributes, ExerciseCategory, ExerciseFilters, ExerciseListItem, ExerciseParameters,
     ExerciseParametersLotMapping, ExerciseSortBy, ExercisesListInput, GithubExercise,
-    GithubExerciseAttributes, UpdateUserWorkoutInput, UserExerciseInput, UserMeasurementsListInput,
-    UserWorkoutInput, UserWorkoutSetRecord, WorkoutSetPersonalBest,
+    GithubExerciseAttributes, ProcessedExercise, UpdateUserWorkoutInput, UserExerciseInput,
+    UserMeasurementsListInput, UserWorkoutInput, UserWorkoutSetRecord, WorkoutInformation,
+    WorkoutSetPersonalBest, WorkoutSetRecord, WorkoutSummary, WorkoutSummaryExercise,
 };
 use itertools::Itertools;
 use migrations::AliasedExercise;
+use nanoid::nanoid;
 use sea_orm::{
     prelude::DateTimeUtc, ActiveModelTrait, ActiveValue, ColumnTrait, DatabaseConnection,
     EntityTrait, Iterable, ModelTrait, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect,
     QueryTrait, RelationTrait,
 };
-use sea_query::{extension::postgres::PgExpr, Alias, Condition, Expr, Func, JoinType};
+use sea_query::{extension::postgres::PgExpr, Alias, Condition, Expr, Func, JoinType, OnConflict};
 use slug::slugify;
 
 use logic::{calculate_and_commit, delete_existing_workout};
@@ -67,6 +72,7 @@ const LOT_MAPPINGS: &[(ExerciseLot, &[WorkoutSetPersonalBest])] = &[
 ];
 
 pub struct ExerciseService {
+    is_pro: bool,
     db: DatabaseConnection,
     config: Arc<config::AppConfig>,
     file_storage_service: Arc<FileStorageService>,
@@ -76,6 +82,7 @@ pub struct ExerciseService {
 
 impl ExerciseService {
     pub fn new(
+        is_pro: bool,
         db: &DatabaseConnection,
         config: Arc<config::AppConfig>,
         file_storage_service: Arc<FileStorageService>,
@@ -84,6 +91,7 @@ impl ExerciseService {
     ) -> Self {
         Self {
             config,
+            is_pro,
             db: db.clone(),
             file_storage_service,
             perform_application_job: perform_application_job.clone(),
@@ -93,6 +101,135 @@ impl ExerciseService {
 }
 
 impl ExerciseService {
+    pub async fn user_workout_templates_list(
+        &self,
+        user_id: String,
+        input: SearchInput,
+    ) -> Result<SearchResults<workout_template::Model>> {
+        let page = input.page.unwrap_or(1);
+        let query = WorkoutTemplate::find()
+            .filter(workout_template::Column::UserId.eq(user_id))
+            .apply_if(input.query, |query, v| {
+                query.filter(Expr::col(workout_template::Column::Name).ilike(ilike_sql(&v)))
+            })
+            .order_by_desc(workout_template::Column::CreatedOn);
+        let total = query.clone().count(&self.db).await?;
+        let total: i32 = total.try_into().unwrap();
+        let data = query.paginate(&self.db, self.config.frontend.page_size.try_into().unwrap());
+        let items = data.fetch_page((page - 1).try_into().unwrap()).await?;
+        let next_page = if total - (page * self.config.frontend.page_size) > 0 {
+            Some(page + 1)
+        } else {
+            None
+        };
+        Ok(SearchResults {
+            details: SearchDetails { total, next_page },
+            items,
+        })
+    }
+
+    pub async fn workout_template_details(
+        &self,
+        user_id: String,
+        workout_template_id: String,
+    ) -> Result<UserWorkoutTemplateDetails> {
+        workout_template_details(&self.db, &user_id, workout_template_id).await
+    }
+
+    pub async fn create_or_update_workout_template(
+        &self,
+        user_id: String,
+        input: UserWorkoutInput,
+    ) -> Result<String> {
+        pro_instance_guard(self.is_pro).await?;
+        let mut summary = WorkoutSummary {
+            total: None,
+            exercises: vec![],
+        };
+        let mut information = WorkoutInformation {
+            assets: None,
+            comment: input.comment,
+            exercises: vec![],
+        };
+        for exercise in input.exercises {
+            let db_ex = self.exercise_details(exercise.exercise_id.clone()).await?;
+            summary.exercises.push(WorkoutSummaryExercise {
+                id: exercise.exercise_id.clone(),
+                best_set: None,
+                lot: None,
+                num_sets: exercise.sets.len(),
+            });
+            information.exercises.push(ProcessedExercise {
+                total: None,
+                assets: None,
+                lot: db_ex.lot,
+                sets: exercise
+                    .sets
+                    .into_iter()
+                    .map(|s| WorkoutSetRecord {
+                        lot: s.lot,
+                        note: s.note,
+                        totals: None,
+                        confirmed_at: None,
+                        personal_bests: None,
+                        actual_rest_time: None,
+                        statistic: s.statistic,
+                    })
+                    .collect(),
+                notes: exercise.notes,
+                name: exercise.exercise_id,
+                rest_time: exercise.rest_time,
+                superset_with: exercise.superset_with,
+            });
+        }
+        let template = workout_template::ActiveModel {
+            id: match input.update_workout_template_id {
+                Some(id) => ActiveValue::Set(id),
+                None => ActiveValue::Set(format!("wktpl_{}", nanoid!(12))),
+            },
+            name: ActiveValue::Set(input.name),
+            user_id: ActiveValue::Set(user_id),
+            summary: ActiveValue::Set(summary),
+            information: ActiveValue::Set(information),
+            visibility: ActiveValue::Set(Visibility::Private),
+            default_rest_timer: ActiveValue::Set(input.default_rest_timer),
+            ..Default::default()
+        };
+        let template = WorkoutTemplate::insert(template)
+            .on_conflict(
+                OnConflict::column(workout_template::Column::Id)
+                    .update_columns([
+                        workout_template::Column::Name,
+                        workout_template::Column::Summary,
+                        workout_template::Column::Visibility,
+                        workout_template::Column::Information,
+                        workout_template::Column::DefaultRestTimer,
+                    ])
+                    .to_owned(),
+            )
+            .exec_with_returning(&self.db)
+            .await?;
+        Ok(template.id)
+    }
+
+    pub async fn delete_workout_template(
+        &self,
+        user_id: String,
+        workout_template_id: String,
+    ) -> Result<bool> {
+        pro_instance_guard(self.is_pro).await?;
+        if let Some(wkt) = WorkoutTemplate::find_by_id(workout_template_id)
+            .filter(workout_template::Column::UserId.eq(&user_id))
+            .one(&self.db)
+            .await?
+        {
+            wkt.delete(&self.db).await?;
+            Ok(true)
+        } else {
+            Err(Error::new("Workout template does not exist for user"))
+        }
+    }
+
     pub async fn exercise_parameters(&self) -> Result<ExerciseParameters> {
         let download_required = Exercise::find().count(&self.db).await? == 0;
         Ok(ExerciseParameters {
@@ -560,8 +697,11 @@ impl ExerciseService {
         UserWorkoutInput {
             name: user_workout.name,
             id: Some(user_workout.id),
+            default_rest_timer: None,
             end_time: user_workout.end_time,
+            update_workout_template_id: None,
             start_time: user_workout.start_time,
+            template_id: user_workout.template_id,
             assets: user_workout.information.assets,
             repeated_from: user_workout.repeated_from,
             comment: user_workout.information.comment,
diff --git a/crates/services/fitness/src/logic.rs b/crates/services/fitness/src/logic.rs
index 5bfba3b7bc..492fd9a9cd 100644
--- a/crates/services/fitness/src/logic.rs
+++ b/crates/services/fitness/src/logic.rs
@@ -293,6 +293,7 @@ pub async fn calculate_and_commit(
             assets: input.assets,
             exercises: exercises.into_iter().map(|(_, _, ex)| ex).collect(),
         },
+        template_id: input.template_id,
         duration: 0,
     };
     let mut insert: workout::ActiveModel = model.into();
diff --git a/crates/services/importer/Cargo.toml b/crates/services/importer/Cargo.toml
index 9c5b7db605..68a85e62a3 100644
--- a/crates/services/importer/Cargo.toml
+++ b/crates/services/importer/Cargo.toml
@@ -15,6 +15,7 @@ convert_case = { workspace = true }
 csv = "=1.3.0"
 data-encoding = "=2.6.0"
 enum_meta = { workspace = true }
+env-utils = { path = "../../utils/env" }
 common-models = { path = "../../models/common" }
 common-utils = { path = "../../utils/common" }
 database-models = { path = "../../models/database" }
diff --git a/crates/services/importer/src/audiobookshelf.rs b/crates/services/importer/src/audiobookshelf.rs
index d9622037af..476231f991 100644
--- a/crates/services/importer/src/audiobookshelf.rs
+++ b/crates/services/importer/src/audiobookshelf.rs
@@ -32,7 +32,7 @@ where
 {
     let mut media = vec![];
     let mut failed_items = vec![];
-    let url = format!("{}/api/", input.api_url);
+    let url = format!("{}/api", input.api_url);
     let client = get_base_http_client(Some(vec![(
         AUTHORIZATION,
         HeaderValue::from_str(&format!("Bearer {}", input.api_key)).unwrap(),
diff --git a/crates/services/importer/src/media_tracker.rs b/crates/services/importer/src/media_tracker.rs
index 2c3ff37062..85fd2c9c78 100644
--- a/crates/services/importer/src/media_tracker.rs
+++ b/crates/services/importer/src/media_tracker.rs
@@ -127,7 +127,7 @@ pub async fn import(input: DeployUrlAndKeyImportInput) -> Result<ImportResult> {
     let mut headers = HeaderMap::new();
     headers.insert(USER_AGENT, HeaderValue::from_static(USER_AGENT_STR));
     headers.insert("Access-Token", input.api_key.parse().unwrap());
-    let url = format!("{}/api/", api_url);
+    let url = format!("{}/api", api_url);
     let client = ClientBuilder::new()
         .default_headers(headers)
         .build()
diff --git a/crates/services/importer/src/strong_app.rs b/crates/services/importer/src/strong_app.rs
index af82b44784..96668b78d5 100644
--- a/crates/services/importer/src/strong_app.rs
+++ b/crates/services/importer/src/strong_app.rs
@@ -123,13 +123,16 @@ pub async fn import(
             };
             workouts.push(UserWorkoutInput {
                 id: None,
+                template_id: None,
                 repeated_from: None,
+                default_rest_timer: None,
                 name: entry.workout_name,
                 comment: entry.workout_notes,
                 start_time: ndt,
                 end_time: ndt + workout_duration,
                 exercises,
                 assets: None,
+                update_workout_template_id: None,
             });
             exercises = vec![];
         }
diff --git a/crates/services/importer/src/trakt.rs b/crates/services/importer/src/trakt.rs
index 7974c76e5c..2926d0625b 100644
--- a/crates/services/importer/src/trakt.rs
+++ b/crates/services/importer/src/trakt.rs
@@ -4,6 +4,7 @@ use common_utils::{ryot_log, APPLICATION_JSON_HEADER};
 use convert_case::{Case, Casing};
 use dependent_models::ImportResult;
 use enums::{ImportSource, MediaLot, MediaSource};
+use env_utils::TRAKT_CLIENT_ID;
 use itertools::Itertools;
 use media_models::{
     CreateOrUpdateCollectionInput, DeployTraktImportInput, ImportOrExportItemRating,
@@ -18,7 +19,6 @@ use serde::{Deserialize, Serialize};
 use super::{ImportFailStep, ImportFailedItem, ImportOrExportMediaItem};
 
 const API_URL: &str = "https://api.trakt.tv";
-const CLIENT_ID: &str = "b3d93fd4c53d78d61b18e0f0bf7ad5153de323788dbc0be1a3627205a36e89f5";
 const API_VERSION: &str = "2";
 
 #[derive(Debug, Serialize, Deserialize)]
@@ -58,12 +58,12 @@ pub async fn import(input: DeployTraktImportInput) -> Result<ImportResult> {
     let mut media = vec![];
     let mut failed_items = vec![];
 
-    let url = format!("{}/users/{}/", API_URL, input.username);
+    let url = format!("{}/users/{}", API_URL, input.username);
     let client = get_base_http_client(Some(vec![
         (CONTENT_TYPE, APPLICATION_JSON_HEADER.clone()),
         (
             HeaderName::from_static("trakt-api-key"),
-            HeaderValue::from_static(CLIENT_ID),
+            HeaderValue::from_static(TRAKT_CLIENT_ID),
         ),
         (
             HeaderName::from_static("trakt-api-version"),
diff --git a/crates/services/integration/Cargo.toml b/crates/services/integration/Cargo.toml
index 4edbae2e53..e15188658b 100644
--- a/crates/services/integration/Cargo.toml
+++ b/crates/services/integration/Cargo.toml
@@ -8,6 +8,7 @@ anyhow = { workspace = true }
 application-utils = { path = "../../utils/application" }
 async-graphql = { workspace = true }
 common-utils = { path = "../../utils/common" }
+common-models = { path = "../../models/common" }
 database-models = { path = "../../models/database" }
 database-utils = { path = "../../utils/database" }
 enums = { path = "../../enums" }
diff --git a/crates/services/integration/src/audiobookshelf.rs b/crates/services/integration/src/audiobookshelf.rs
index e151b028b8..ef13f4f6a6 100644
--- a/crates/services/integration/src/audiobookshelf.rs
+++ b/crates/services/integration/src/audiobookshelf.rs
@@ -3,6 +3,7 @@ use std::future::Future;
 use anyhow::anyhow;
 use application_utils::get_base_http_client;
 use async_graphql::Result as GqlResult;
+use common_models::DefaultCollection;
 use common_utils::ryot_log;
 use database_models::metadata;
 use enums::{MediaLot, MediaSource};
@@ -10,21 +11,28 @@ use media_models::{CommitMediaInput, IntegrationMediaCollection, IntegrationMedi
 use providers::google_books::GoogleBooksService;
 use reqwest::header::{HeaderValue, AUTHORIZATION};
 use rust_decimal_macros::dec;
-use specific_models::audiobookshelf;
+use specific_models::audiobookshelf::{self, LibrariesListResponse, ListResponse};
 
 use super::integration_trait::YankIntegrationWithCommit;
 
 pub(crate) struct AudiobookshelfIntegration {
     base_url: String,
     access_token: String,
+    sync_to_owned_collection: Option<bool>,
     isbn_service: GoogleBooksService,
 }
 
 impl AudiobookshelfIntegration {
-    pub fn new(base_url: String, access_token: String, isbn_service: GoogleBooksService) -> Self {
+    pub fn new(
+        base_url: String,
+        access_token: String,
+        sync_to_owned_collection: Option<bool>,
+        isbn_service: GoogleBooksService,
+    ) -> Self {
         Self {
             base_url,
             access_token,
+            sync_to_owned_collection,
             isbn_service,
         }
     }
@@ -38,7 +46,7 @@ impl YankIntegrationWithCommit for AudiobookshelfIntegration {
     where
         F: Future<Output = GqlResult<metadata::Model>>,
     {
-        let url = format!("{}/api/", self.base_url);
+        let url = format!("{}/api", self.base_url);
         let client = get_base_http_client(Some(vec![(
             AUTHORIZATION,
             HeaderValue::from_str(&format!("Bearer {}", self.access_token)).unwrap(),
@@ -172,7 +180,52 @@ impl YankIntegrationWithCommit for AudiobookshelfIntegration {
                 }
             };
         }
-
-        Ok((media_items, vec![]))
+        let mut collection_updates = vec![];
+        if let Some(true) = self.sync_to_owned_collection {
+            let libraries_resp = client
+                .get("libraries")
+                .send()
+                .await
+                .map_err(|e| anyhow!(e))?
+                .json::<LibrariesListResponse>()
+                .await
+                .unwrap();
+            for library in libraries_resp.libraries {
+                let items = client
+                    .get(&format!("libraries/{}/items", library.id))
+                    .send()
+                    .await
+                    .map_err(|e| anyhow!(e))?
+                    .json::<ListResponse>()
+                    .await
+                    .unwrap();
+                for item in items.results.into_iter() {
+                    let metadata = item.media.clone().unwrap().metadata;
+                    let (identifier, lot, source) =
+                        if Some("epub".to_string()) == item.media.as_ref().unwrap().ebook_format {
+                            match &metadata.isbn {
+                                Some(isbn) => match self.isbn_service.id_from_isbn(isbn).await {
+                                    Some(id) => (id, MediaLot::Book, MediaSource::GoogleBooks),
+                                    _ => continue,
+                                },
+                                _ => continue,
+                            }
+                        } else if let Some(asin) = metadata.asin.clone() {
+                            (asin, MediaLot::AudioBook, MediaSource::Audible)
+                        } else if let Some(itunes_id) = metadata.itunes_id.clone() {
+                            (itunes_id, MediaLot::Podcast, MediaSource::Itunes)
+                        } else {
+                            continue;
+                        };
+                    collection_updates.push(IntegrationMediaCollection {
+                        identifier,
+                        lot,
+                        source,
+                        collection: DefaultCollection::Owned.to_string(),
+                    });
+                }
+            }
+        }
+        Ok((media_items, collection_updates))
     }
 }
diff --git a/crates/services/integration/src/integration_type.rs b/crates/services/integration/src/integration_type.rs
index dfc1f6495a..8709f580a9 100644
--- a/crates/services/integration/src/integration_type.rs
+++ b/crates/services/integration/src/integration_type.rs
@@ -2,11 +2,11 @@ use enums::MediaSource;
 use providers::google_books::GoogleBooksService;
 
 pub enum IntegrationType {
-    Komga(String, String, String, MediaSource),
+    Komga(String, String, String, MediaSource, Option<bool>),
     Jellyfin(String),
     Emby(String),
     Plex(String, Option<String>),
-    Audiobookshelf(String, String, GoogleBooksService),
+    Audiobookshelf(String, String, Option<bool>, GoogleBooksService),
     Kodi(String),
     Sonarr(String, String, i32, String, String),
     Radarr(String, String, i32, String, String),
diff --git a/crates/services/integration/src/komga.rs b/crates/services/integration/src/komga.rs
index 0c9acb64f6..f830876828 100644
--- a/crates/services/integration/src/komga.rs
+++ b/crates/services/integration/src/komga.rs
@@ -5,7 +5,8 @@ use std::{
 
 use anyhow::{anyhow, Context, Result};
 use application_utils::get_base_http_client;
-use async_graphql::futures_util::StreamExt;
+use async_graphql::futures_util::{stream, StreamExt};
+use common_models::DefaultCollection;
 use common_utils::ryot_log;
 use database_models::{metadata, prelude::Metadata};
 use enums::{MediaLot, MediaSource};
@@ -188,6 +189,7 @@ pub(crate) struct KomgaIntegration {
     password: String,
     provider: MediaSource,
     db: DatabaseConnection,
+    sync_to_owned_collection: Option<bool>,
 }
 
 impl KomgaIntegration {
@@ -197,6 +199,7 @@ impl KomgaIntegration {
         password: String,
         provider: MediaSource,
         db: DatabaseConnection,
+        sync_to_owned_collection: Option<bool>,
     ) -> Self {
         Self {
             base_url,
@@ -204,6 +207,7 @@ impl KomgaIntegration {
             password,
             provider,
             db,
+            sync_to_owned_collection,
         }
     }
 
@@ -258,11 +262,11 @@ impl KomgaIntegration {
                         }
                     }
                 } else {
-                    ryot_log!(debug, event_type = ?event.event, "Received unhandled event type");
+                    ryot_log!(trace, event_type = ?event.event, "Received unhandled event type");
                 }
             }
 
-            ryot_log!(debug, "SSE listener finished");
+            ryot_log!(trace, "SSE listener finished");
             tokio::time::sleep(tokio::time::Duration::from_secs(30)).await;
         }
     }
@@ -377,6 +381,53 @@ impl KomgaIntegration {
         })
     }
 
+    async fn sync_manga_collection(&self) -> Result<Vec<IntegrationMediaCollection>> {
+        if self.sync_to_owned_collection != Some(true) {
+            return Ok(Vec::new());
+        }
+
+        let url = &format!("{}/api/v1", self.base_url);
+        let client = get_base_http_client(None);
+
+        let series: komga_series::Response = client
+            .get(format!("{}/{}", url, "series?unpaged=true"))
+            .basic_auth(&self.username, Some(&self.password))
+            .send()
+            .await?
+            .error_for_status()?
+            .json()
+            .await?;
+
+        // Hashmap for If you have the same manga in multiple languages it will appear
+        // multiple times this prevents us from double commiting an identifier
+        let unique_collection_updates: HashMap<String, IntegrationMediaCollection> =
+            stream::iter(series.content)
+                .filter_map(|book| async move {
+                    match self.find_provider_and_id(&book).await {
+                        Ok((source, Some(id))) => Some((
+                            id.clone(),
+                            IntegrationMediaCollection {
+                                identifier: id,
+                                lot: MediaLot::Manga,
+                                source,
+                                collection: DefaultCollection::Owned.to_string(),
+                            },
+                        )),
+                        _ => {
+                            tracing::debug!(
+                                "No URL or database entry found for manga: {}",
+                                book.name
+                            );
+                            None
+                        }
+                    }
+                })
+                .collect()
+                .await;
+
+        Ok(unique_collection_updates.into_values().collect())
+    }
+
     async fn komga_progress(
         &self,
     ) -> Result<(Vec<IntegrationMediaSeen>, Vec<IntegrationMediaCollection>)> {
@@ -450,7 +501,9 @@ impl KomgaIntegration {
         media_items.sort_by(|a, b| a.manga_chapter_number.cmp(&b.manga_chapter_number));
         ryot_log!(debug, "Media Items: {:?}", media_items);
 
-        Ok((media_items, vec![]))
+        let collection_updates = self.sync_manga_collection().await?;
+
+        Ok((media_items, collection_updates))
     }
 }
 
diff --git a/crates/services/integration/src/lib.rs b/crates/services/integration/src/lib.rs
index e75dae20d1..d14877ba02 100644
--- a/crates/services/integration/src/lib.rs
+++ b/crates/services/integration/src/lib.rs
@@ -105,14 +105,35 @@ impl IntegrationService {
         F: Future<Output = GqlResult<metadata::Model>>,
     {
         match integration_type {
-            IntegrationType::Audiobookshelf(base_url, access_token, isbn_service) => {
-                let audiobookshelf =
-                    AudiobookshelfIntegration::new(base_url, access_token, isbn_service);
+            IntegrationType::Audiobookshelf(
+                base_url,
+                access_token,
+                sync_to_owned_collection,
+                isbn_service,
+            ) => {
+                let audiobookshelf = AudiobookshelfIntegration::new(
+                    base_url,
+                    access_token,
+                    sync_to_owned_collection,
+                    isbn_service,
+                );
                 audiobookshelf.yank_progress(commit_metadata).await
             }
-            IntegrationType::Komga(base_url, username, password, provider) => {
-                let komga =
-                    KomgaIntegration::new(base_url, username, password, provider, self.db.clone());
+            IntegrationType::Komga(
+                base_url,
+                username,
+                password,
+                provider,
+                sync_to_owned_collection,
+            ) => {
+                let komga = KomgaIntegration::new(
+                    base_url,
+                    username,
+                    password,
+                    provider,
+                    self.db.clone(),
+                    sync_to_owned_collection,
+                );
                 komga.yank_progress().await
             }
             _ => Err(anyhow::anyhow!("Unsupported integration type")),
diff --git a/crates/services/jwt/src/lib.rs b/crates/services/jwt/src/lib.rs
index 62169f2ddf..bfd57cfa37 100644
--- a/crates/services/jwt/src/lib.rs
+++ b/crates/services/jwt/src/lib.rs
@@ -5,6 +5,13 @@ use serde::{Deserialize, Serialize};
 use serde_with::skip_serializing_none;
 use uuid::Uuid;
 
+#[skip_serializing_none]
+#[derive(Debug, Deserialize, Serialize)]
+pub struct AccessLinkClaims {
+    pub id: String,
+    pub is_demo: Option<bool>,
+}
+
 #[skip_serializing_none]
 #[derive(Debug, Deserialize, Serialize)]
 pub struct Claims {
@@ -12,10 +19,15 @@ pub struct Claims {
     pub exp: usize,
     pub iat: usize,
     pub jti: Uuid,
+    pub access_link: Option<AccessLinkClaims>,
 }
 
 impl Claims {
-    pub fn new(sub: String, token_valid_for_days: i32) -> Self {
+    pub fn new(
+        sub: String,
+        token_valid_for_days: i32,
+        access_link: Option<AccessLinkClaims>,
+    ) -> Self {
         let iat = Utc::now();
         let exp = iat + Duration::try_days(token_valid_for_days.into()).unwrap();
 
@@ -24,14 +36,20 @@ impl Claims {
             iat: iat.timestamp().try_into().unwrap(),
             exp: exp.timestamp().try_into().unwrap(),
             jti: Uuid::new_v4(),
+            access_link,
         }
     }
 }
 
-pub fn sign(user_id: String, jwt_secret: &str, token_valid_for_days: i32) -> Result<String> {
+pub fn sign(
+    user_id: String,
+    jwt_secret: &str,
+    token_valid_for_days: i32,
+    access_link: Option<AccessLinkClaims>,
+) -> Result<String> {
     let tokens = encode(
         &Header::default(),
-        &Claims::new(user_id, token_valid_for_days),
+        &Claims::new(user_id, token_valid_for_days, access_link),
         &EncodingKey::from_secret(jwt_secret.as_bytes()),
     )?;
     Ok(tokens)
diff --git a/crates/services/miscellaneous/Cargo.toml b/crates/services/miscellaneous/Cargo.toml
index 20615b250e..030c436639 100644
--- a/crates/services/miscellaneous/Cargo.toml
+++ b/crates/services/miscellaneous/Cargo.toml
@@ -8,7 +8,6 @@ apalis = { workspace = true }
 application-utils = { path = "../../utils/application" }
 async-graphql = { workspace = true }
 background = { path = "../../background" }
-cached = { version = "=0.53.1", features = ["disk_store"] }
 chrono = { workspace = true }
 chrono-tz = { workspace = true }
 common-models = { path = "../../models/common" }
@@ -17,7 +16,9 @@ config = { path = "../../config" }
 database-models = { path = "../../models/database" }
 database-utils = { path = "../../utils/database" }
 dependent-models = { path = "../../models/dependent" }
+derive_more = { workspace = true }
 enums = { path = "../../enums" }
+env-utils = { path = "../../utils/env" }
 file-storage-service = { path = "../../services/file-storage" }
 futures = { workspace = true }
 integration-service = { path = "../../services/integration" }
@@ -25,6 +26,7 @@ itertools = { workspace = true }
 markdown = { workspace = true }
 media-models = { path = "../../models/media" }
 migrations = { path = "../../migrations" }
+moka = { version = "=0.12.8", features = ["future"] }
 nanoid = { workspace = true }
 notification-service = { path = "../../services/notification" }
 providers = { path = "../../providers" }
diff --git a/crates/services/miscellaneous/src/lib.rs b/crates/services/miscellaneous/src/lib.rs
index 9db82a11e8..e4dde1986d 100644
--- a/crates/services/miscellaneous/src/lib.rs
+++ b/crates/services/miscellaneous/src/lib.rs
@@ -1,8 +1,6 @@
 use std::{
     collections::{HashMap, HashSet},
-    fmt,
     iter::zip,
-    path::PathBuf,
     sync::Arc,
 };
 
@@ -10,7 +8,6 @@ use apalis::prelude::{MemoryStorage, MessageQueue};
 use application_utils::get_current_date;
 use async_graphql::{Enum, Error, Result};
 use background::{ApplicationJob, CoreApplicationJob};
-use cached::{DiskCache, IOCached};
 use chrono::{Days, Duration as ChronoDuration, NaiveDate, Utc};
 use common_models::{
     BackendError, BackgroundJob, ChangeCollectionToEntityInput, DefaultCollection,
@@ -18,19 +15,18 @@ use common_models::{
 };
 use common_utils::{
     get_first_and_last_day_of_month, ryot_log, IsFeatureEnabled, SHOW_SPECIAL_SEASON_NAMES,
-    TEMP_DIR, VERSION,
 };
 use database_models::{
-    calendar_event, collection, collection_to_entity,
+    access_link, calendar_event, collection, collection_to_entity,
     functions::{associate_user_with_entity, get_user_to_entity_association},
     genre, import_report, integration, metadata, metadata_group, metadata_to_genre,
     metadata_to_metadata, metadata_to_metadata_group, metadata_to_person, monitored_entity,
     notification_platform, person,
     prelude::{
-        CalendarEvent, Collection, CollectionToEntity, Genre, ImportReport, Integration, Metadata,
-        MetadataGroup, MetadataToGenre, MetadataToMetadata, MetadataToMetadataGroup,
-        MetadataToPerson, MonitoredEntity, NotificationPlatform, Person, QueuedNotification,
-        Review, Seen, User, UserToEntity,
+        AccessLink, CalendarEvent, Collection, CollectionToEntity, Genre, ImportReport,
+        Integration, Metadata, MetadataGroup, MetadataToGenre, MetadataToMetadata,
+        MetadataToMetadataGroup, MetadataToPerson, MonitoredEntity, NotificationPlatform, Person,
+        QueuedNotification, Review, Seen, User, UserToEntity,
     },
     queued_notification, review, seen, user, user_to_entity,
 };
@@ -38,7 +34,7 @@ use database_utils::{
     add_entity_to_collection, admin_account_guard, apply_collection_filter,
     calculate_user_activities_and_summary, entity_in_collections,
     entity_in_collections_with_collection_to_entity_ids, ilike_sql, item_reviews,
-    remove_entity_from_collection, user_by_id, user_preferences_by_id,
+    remove_entity_from_collection, revoke_access_link, user_by_id, user_preferences_by_id,
 };
 use dependent_models::{
     CoreDetails, GenreDetails, MetadataBaseData, MetadataGroupDetails, PersonDetails,
@@ -48,6 +44,7 @@ use enums::{
     EntityLot, IntegrationLot, IntegrationProvider, MediaLot, MediaSource,
     MetadataToMetadataRelation, SeenState, UserToMediaReason, Visibility,
 };
+use env_utils::APP_VERSION;
 use file_storage_service::FileStorageService;
 use futures::TryStreamExt;
 use integration_service::{integration_type::IntegrationType, IntegrationService};
@@ -75,6 +72,7 @@ use media_models::{
 use migrations::{
     AliasedMetadata, AliasedMetadataToGenre, AliasedReview, AliasedSeen, AliasedUserToEntity,
 };
+use moka::future::Cache;
 use nanoid::nanoid;
 use notification_service::send_notification;
 use providers::{
@@ -135,25 +133,35 @@ struct ProgressUpdateCache {
     manga_volume_number: Option<i32>,
 }
 
-impl fmt::Display for ProgressUpdateCache {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(f, "{:#?}", self)
-    }
+#[derive(Debug, Ord, PartialEq, Eq, PartialOrd, Clone, Hash)]
+struct CommitCache {
+    id: String,
+    lot: EntityLot,
 }
 
 pub struct MiscellaneousService {
+    is_pro: bool,
     oidc_enabled: bool,
     db: DatabaseConnection,
     timezone: Arc<chrono_tz::Tz>,
     config: Arc<config::AppConfig>,
     file_storage_service: Arc<FileStorageService>,
     perform_application_job: MemoryStorage<ApplicationJob>,
-    seen_progress_cache: DiskCache<ProgressUpdateCache, ()>,
+    seen_progress_cache: Cache<ProgressUpdateCache, ()>,
     perform_core_application_job: MemoryStorage<CoreApplicationJob>,
+    commit_cache: Cache<CommitCache, ()>,
+}
+
+fn create_disk_cache<T: Eq + std::hash::Hash + Sync + Send + 'static>(hours: i64) -> Cache<T, ()> {
+    Cache::builder()
+        .time_to_live(ChronoDuration::try_hours(hours).unwrap().to_std().unwrap())
+        .build()
 }
 
 impl MiscellaneousService {
+    #[allow(clippy::too_many_arguments)]
     pub async fn new(
+        is_pro: bool,
         oidc_enabled: bool,
         db: &DatabaseConnection,
         timezone: Arc<chrono_tz::Tz>,
@@ -162,24 +170,14 @@ impl MiscellaneousService {
         perform_application_job: &MemoryStorage<ApplicationJob>,
         perform_core_application_job: &MemoryStorage<CoreApplicationJob>,
     ) -> Self {
-        let cache_name = "seen_progress_cache";
-        let path = PathBuf::new().join(TEMP_DIR);
-        let seen_progress_cache = DiskCache::new(cache_name)
-            .set_lifespan(
-                ChronoDuration::try_hours(config.server.progress_update_threshold)
-                    .unwrap()
-                    .num_seconds()
-                    .try_into()
-                    .unwrap(),
-            )
-            .set_disk_directory(path)
-            .build()
-            .unwrap();
-
+        let seen_progress_cache = create_disk_cache(config.server.progress_update_threshold);
+        let commit_cache = create_disk_cache(1);
         Self {
             config,
+            is_pro,
             timezone,
             oidc_enabled,
+            commit_cache,
             db: db.clone(),
             seen_progress_cache,
             file_storage_service,
@@ -190,14 +188,109 @@ impl MiscellaneousService {
 }
 
 impl MiscellaneousService {
+    pub async fn update_claimed_recommendations_and_download_new_ones(&self) -> Result<()> {
+        ryot_log!(
+            debug,
+            "Updating old recommendations to not be recommendations anymore"
+        );
+        let mut metadata_stream = Metadata::find()
+            .select_only()
+            .column(metadata::Column::Id)
+            .filter(metadata::Column::IsRecommendation.eq(true))
+            .into_tuple::<String>()
+            .stream(&self.db)
+            .await?;
+        let mut recommendations_to_update = vec![];
+        while let Some(meta) = metadata_stream.try_next().await? {
+            let num_ute = UserToEntity::find()
+                .filter(user_to_entity::Column::MetadataId.eq(&meta))
+                .count(&self.db)
+                .await?;
+            if num_ute > 0 {
+                recommendations_to_update.push(meta);
+            }
+        }
+        Metadata::update_many()
+            .filter(metadata::Column::Id.is_in(recommendations_to_update))
+            .set(metadata::ActiveModel {
+                is_recommendation: ActiveValue::Set(None),
+                ..Default::default()
+            })
+            .exec(&self.db)
+            .await?;
+        ryot_log!(debug, "Downloading new recommendations for users");
+        #[derive(Debug, FromQueryResult)]
+        struct CustomQueryResponse {
+            lot: MediaLot,
+            source: MediaSource,
+            identifier: String,
+        }
+        let media_items = CustomQueryResponse::find_by_statement(Statement::from_sql_and_values(
+            DatabaseBackend::Postgres,
+            r#"
+SELECT "m"."lot", "m"."identifier", "m"."source"
+FROM (
+    SELECT "user_id", "metadata_id" FROM "user_to_entity"
+    WHERE "user_id" IN (SELECT "id" from "user") AND "metadata_id" IS NOT NULL
+) "sub"
+JOIN "metadata" "m" ON "sub"."metadata_id" = "m"."id" AND "m"."source" NOT IN ($1, $2, $3)
+ORDER BY RANDOM() LIMIT 10;
+        "#,
+            [
+                MediaSource::GoogleBooks.into(),
+                MediaSource::Itunes.into(),
+                MediaSource::Vndb.into(),
+            ],
+        ))
+        .all(&self.db)
+        .await?;
+        let mut media_item_ids = vec![];
+        for media in media_items.into_iter() {
+            let provider = self.get_metadata_provider(media.lot, media.source).await?;
+            if let Ok(recommendations) = provider
+                .get_recommendations_for_metadata(&media.identifier)
+                .await
+            {
+                for mut rec in recommendations {
+                    rec.is_recommendation = Some(true);
+                    if let Ok(meta) = self.create_partial_metadata(rec).await {
+                        media_item_ids.push(meta.id);
+                    }
+                }
+            }
+        }
+        Ok(())
+    }
+
+    pub async fn revoke_invalid_access_tokens(&self) -> Result<()> {
+        let access_links = AccessLink::find()
+            .select_only()
+            .column(access_link::Column::Id)
+            .filter(
+                Condition::any()
+                    .add(
+                        Expr::col(access_link::Column::TimesUsed)
+                            .gte(Expr::col(access_link::Column::MaximumUses)),
+                    )
+                    .add(access_link::Column::ExpiresOn.lte(Utc::now())),
+            )
+            .into_tuple::<String>()
+            .all(&self.db)
+            .await?;
+        for access_link in access_links {
+            revoke_access_link(&self.db, access_link).await?;
+        }
+        Ok(())
+    }
+
     pub async fn core_details(&self) -> CoreDetails {
         let mut files_enabled = self.config.file_storage.is_enabled();
         if files_enabled && !self.file_storage_service.is_enabled().await {
             files_enabled = false;
         }
         CoreDetails {
-            is_pro: false,
-            version: VERSION.to_owned(),
+            is_pro: self.is_pro,
+            version: APP_VERSION.to_owned(),
             file_storage_enabled: files_enabled,
             oidc_enabled: self.oidc_enabled,
             page_limit: self.config.frontend.page_size,
@@ -1045,7 +1138,7 @@ impl MiscellaneousService {
             manga_chapter_number: input.manga_chapter_number,
             manga_volume_number: input.manga_volume_number,
         };
-        let in_cache = self.seen_progress_cache.cache_get(&cache).unwrap();
+        let in_cache = self.seen_progress_cache.get(&cache).await;
         if respect_cache && in_cache.is_some() {
             return Ok(ProgressUpdateResultUnion::Error(ProgressUpdateError {
                 error: ProgressUpdateErrorVariant::AlreadySeen,
@@ -1253,7 +1346,7 @@ impl MiscellaneousService {
         ryot_log!(debug, "Progress update = {:?}", seen);
         let id = seen.id.clone();
         if seen.state == SeenState::Completed && respect_cache {
-            self.seen_progress_cache.cache_set(cache, ()).unwrap();
+            self.seen_progress_cache.insert(cache, ()).await;
         }
         self.after_media_seen_tasks(seen).await?;
         Ok(ProgressUpdateResultUnion::Ok(StringIdObject { id }))
@@ -1821,6 +1914,7 @@ impl MiscellaneousService {
                 source: ActiveValue::Set(data.source),
                 images: ActiveValue::Set(image),
                 is_partial: ActiveValue::Set(Some(true)),
+                is_recommendation: ActiveValue::Set(data.is_recommendation),
                 ..Default::default()
             };
             c.insert(&self.db).await?
@@ -1832,6 +1926,7 @@ impl MiscellaneousService {
             lot: mode.lot,
             source: mode.source,
             image: data.image,
+            is_recommendation: mode.is_recommendation,
         };
         Ok(model)
     }
@@ -1881,6 +1976,9 @@ impl MiscellaneousService {
         if let Some(provider_watched_on) = input.provider_watched_on {
             seen.provider_watched_on = ActiveValue::Set(Some(provider_watched_on));
         }
+        if let Some(manual_time_spent) = input.manual_time_spent {
+            seen.manual_time_spent = ActiveValue::Set(Some(manual_time_spent));
+        }
         let seen = seen.update(&self.db).await.unwrap();
         self.after_media_seen_tasks(seen).await?;
         Ok(true)
@@ -2498,10 +2596,19 @@ impl MiscellaneousService {
             .one(&self.db)
             .await?
         {
+            let cached_metadata = CommitCache {
+                id: m.id.clone(),
+                lot: EntityLot::Metadata,
+            };
+            if self.commit_cache.get(&cached_metadata).await.is_some() {
+                return Ok(m);
+            }
+            self.commit_cache.insert(cached_metadata.clone(), ()).await;
             if input.force_update.unwrap_or_default() {
-                ryot_log!(debug, "Forcing update of metadata with id {}", m.id);
+                ryot_log!(debug, "Forcing update of metadata with id {}", &m.id);
                 self.update_metadata_and_notify_users(&m.id, true).await?;
             }
+            self.commit_cache.remove(&cached_metadata).await;
             Ok(m)
         } else {
             let details = self
@@ -2626,7 +2733,7 @@ impl MiscellaneousService {
             }
             EntityLot::Collection => review_obj.collection_id = ActiveValue::Set(Some(entity_id)),
             EntityLot::Exercise => review_obj.exercise_id = ActiveValue::Set(Some(entity_id)),
-            EntityLot::Workout => unreachable!(),
+            EntityLot::Workout | EntityLot::WorkoutTemplate => unreachable!(),
         };
         if let Some(s) = input.is_spoiler {
             review_obj.is_spoiler = ActiveValue::Set(s);
@@ -2665,7 +2772,7 @@ impl MiscellaneousService {
                         .name
                 }
                 EntityLot::Exercise => id.clone(),
-                EntityLot::Workout => unreachable!(),
+                EntityLot::Workout | EntityLot::WorkoutTemplate => unreachable!(),
             };
             let user = user_by_id(&self.db, &insert.user_id.unwrap()).await?;
             // DEV: Do not send notification if updating a review
@@ -2740,7 +2847,7 @@ impl MiscellaneousService {
                 manga_chapter_number: mcn,
                 manga_volume_number: mvn,
             };
-            self.seen_progress_cache.cache_remove(&cache).unwrap();
+            self.seen_progress_cache.remove(&cache).await;
             let seen_id = si.id.clone();
             let metadata_id = si.metadata_id.clone();
             if &si.user_id != user_id {
@@ -3021,6 +3128,7 @@ impl MiscellaneousService {
                 IntegrationProvider::Audiobookshelf => IntegrationType::Audiobookshelf(
                     specifics.audiobookshelf_base_url.unwrap(),
                     specifics.audiobookshelf_token.unwrap(),
+                    integration.sync_to_owned_collection,
                     self.get_isbn_service().await.unwrap(),
                 ),
                 IntegrationProvider::Komga => IntegrationType::Komga(
@@ -3028,6 +3136,7 @@ impl MiscellaneousService {
                     specifics.komga_username.unwrap(),
                     specifics.komga_password.unwrap(),
                     specifics.komga_provider.unwrap(),
+                    integration.sync_to_owned_collection,
                 ),
                 _ => continue,
             };
@@ -4338,6 +4447,7 @@ impl MiscellaneousService {
             EntityLot::Workout => format!("fitness/workouts/{}", id),
             EntityLot::Exercise => format!("fitness/exercises/{}", id),
             EntityLot::MetadataGroup => format!("media/groups/item/{}", id),
+            EntityLot::WorkoutTemplate => format!("fitness/templates/{}", id),
         };
         url = format!("{}/{}", self.config.frontend.url, url);
         if let Some(tab) = default_tab {
@@ -4448,6 +4558,11 @@ impl MiscellaneousService {
         }
         ryot_log!(debug, "Deleting all queued notifications");
         QueuedNotification::delete_many().exec(&self.db).await?;
+        ryot_log!(debug, "Deleting revoked access tokens");
+        AccessLink::delete_many()
+            .filter(access_link::Column::IsRevoked.eq(true))
+            .exec(&self.db)
+            .await?;
         Ok(())
     }
 
@@ -4586,6 +4701,16 @@ impl MiscellaneousService {
         self.remove_useless_data().await.trace_ok();
         ryot_log!(trace, "Putting entities in partial state");
         self.put_entities_in_partial_state().await.trace_ok();
+        // DEV: This is called after removing useless data so that recommendations are not
+        // delete right after they are downloaded.
+        ryot_log!(trace, "Downloading recommendations for users");
+        self.update_claimed_recommendations_and_download_new_ones()
+            .await
+            .trace_ok();
+        // DEV: Invalid access tokens are revoked before being deleted, so we call this
+        // function after removing useless data.
+        ryot_log!(trace, "Revoking invalid access tokens");
+        self.revoke_invalid_access_tokens().await.trace_ok();
 
         ryot_log!(debug, "Completed background jobs...");
         Ok(())
diff --git a/crates/services/user/src/lib.rs b/crates/services/user/src/lib.rs
index 28e2d80ec9..3e89e375bc 100644
--- a/crates/services/user/src/lib.rs
+++ b/crates/services/user/src/lib.rs
@@ -1,33 +1,35 @@
-use std::{str::FromStr, sync::Arc};
+use std::{fmt::Write, str::FromStr, sync::Arc};
 
 use apalis::prelude::MemoryStorage;
 use application_utils::user_id_from_token;
 use argon2::{Argon2, PasswordHash, PasswordVerifier};
 use async_graphql::{Error, Result};
 use background::ApplicationJob;
-use chrono::Utc;
+use chrono::{Timelike, Utc};
 use common_models::{DefaultCollection, StringIdObject};
 use common_utils::ryot_log;
 use database_models::{
-    integration, notification_platform,
-    prelude::{Integration, NotificationPlatform, User},
+    access_link, integration, metadata, notification_platform,
+    prelude::{AccessLink, Integration, Metadata, NotificationPlatform, User},
     user,
 };
 use database_utils::{
     admin_account_guard, create_or_update_collection,
-    deploy_job_to_calculate_user_activities_and_summary, ilike_sql, user_by_id,
-    user_preferences_by_id,
+    deploy_job_to_calculate_user_activities_and_summary, ilike_sql, pro_instance_guard,
+    revoke_access_link, user_by_id, user_preferences_by_id,
 };
 use dependent_models::UserDetailsResult;
 use enum_meta::Meta;
 use enums::{IntegrationLot, IntegrationProvider, NotificationPlatformLot, UserLot};
 use fitness_models::UserUnitSystem;
 use itertools::Itertools;
-use jwt_service::sign;
+use jwt_service::{sign, AccessLinkClaims};
 use media_models::{
-    AuthUserInput, CreateOrUpdateCollectionInput, CreateUserIntegrationInput,
-    CreateUserNotificationPlatformInput, LoginError, LoginErrorVariant, LoginResponse, LoginResult,
-    OidcTokenOutput, PasswordUserInput, RegisterError, RegisterErrorVariant, RegisterResult,
+    AuthUserInput, CreateAccessLinkInput, CreateOrUpdateCollectionInput,
+    CreateUserIntegrationInput, CreateUserNotificationPlatformInput, LoginError, LoginErrorVariant,
+    LoginResponse, LoginResult, OidcTokenOutput, PasswordUserInput, ProcessAccessLinkError,
+    ProcessAccessLinkErrorVariant, ProcessAccessLinkInput, ProcessAccessLinkResponse,
+    ProcessAccessLinkResult, RegisterError, RegisterErrorVariant, RegisterResult,
     RegisterUserInput, UpdateUserInput, UpdateUserIntegrationInput,
     UpdateUserNotificationPlatformInput, UpdateUserPreferenceInput, UserDetailsError,
     UserDetailsErrorVariant,
@@ -40,13 +42,14 @@ use openidconnect::{
     AuthenticationFlow, AuthorizationCode, CsrfToken, Nonce, Scope, TokenResponse,
 };
 use sea_orm::{
-    prelude::Expr, sea_query::extension::postgres::PgExpr, ActiveModelTrait, ActiveValue,
-    ColumnTrait, DatabaseConnection, EntityTrait, Iterable, ModelTrait, PaginatorTrait,
-    QueryFilter, QueryOrder, QueryTrait,
+    prelude::Expr,
+    sea_query::{extension::postgres::PgExpr, Func},
+    ActiveModelTrait, ActiveValue, ColumnTrait, DatabaseConnection, EntityTrait, Iden, Iterable,
+    ModelTrait, Order, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect, QueryTrait,
 };
 use user_models::{
-    NotificationPlatformSpecifics, UserGeneralDashboardElement, UserGeneralPreferences,
-    UserPreferences, UserReviewScale,
+    DashboardElementLot, NotificationPlatformSpecifics, UserGeneralDashboardElement,
+    UserGeneralPreferences, UserPreferences, UserReviewScale,
 };
 
 fn empty_nonce_verifier(_nonce: Option<&Nonce>) -> Result<(), String> {
@@ -55,6 +58,7 @@ fn empty_nonce_verifier(_nonce: Option<&Nonce>) -> Result<(), String> {
 
 #[derive(Debug)]
 pub struct UserService {
+    is_pro: bool,
     db: DatabaseConnection,
     config: Arc<config::AppConfig>,
     oidc_client: Arc<Option<CoreClient>>,
@@ -63,6 +67,7 @@ pub struct UserService {
 
 impl UserService {
     pub fn new(
+        is_pro: bool,
         db: &DatabaseConnection,
         config: Arc<config::AppConfig>,
         perform_application_job: &MemoryStorage<ApplicationJob>,
@@ -70,16 +75,169 @@ impl UserService {
     ) -> Self {
         Self {
             config,
+            is_pro,
             oidc_client,
             db: db.clone(),
             perform_application_job: perform_application_job.clone(),
         }
     }
 
+    pub async fn user_recommendations(&self, user_id: &String) -> Result<Vec<String>> {
+        // TODO: Replace when https://github.com/SeaQL/sea-query/pull/786 is merged
+        struct Md5;
+        impl Iden for Md5 {
+            fn unquoted(&self, s: &mut dyn Write) {
+                write!(s, "MD5").unwrap();
+            }
+        }
+        let preferences = user_preferences_by_id(&self.db, user_id, &self.config).await?;
+        let limit = preferences
+            .general
+            .dashboard
+            .into_iter()
+            .find(|d| d.section == DashboardElementLot::Recommendations)
+            .unwrap()
+            .num_elements;
+        let current_hour = Utc::now().hour();
+        let recs = Metadata::find()
+            .filter(metadata::Column::IsRecommendation.eq(true))
+            .order_by(
+                Expr::expr(
+                    Func::cust(Md5).arg(
+                        Expr::col(metadata::Column::Title)
+                            .concat(Expr::val(user_id))
+                            .concat(Expr::val(current_hour)),
+                    ),
+                ),
+                Order::Desc,
+            )
+            .limit(limit)
+            .all(&self.db)
+            .await?
+            .into_iter()
+            .map(|r| r.id)
+            .collect_vec();
+        Ok(recs)
+    }
+
+    pub async fn user_access_links(&self, user_id: &String) -> Result<Vec<access_link::Model>> {
+        let links = AccessLink::find()
+            .filter(access_link::Column::UserId.eq(user_id))
+            .order_by_desc(access_link::Column::CreatedOn)
+            .all(&self.db)
+            .await?;
+        Ok(links)
+    }
+
+    pub async fn create_access_link(
+        &self,
+        input: CreateAccessLinkInput,
+        user_id: String,
+    ) -> Result<StringIdObject> {
+        pro_instance_guard(self.is_pro).await?;
+        let new_link = access_link::ActiveModel {
+            user_id: ActiveValue::Set(user_id),
+            name: ActiveValue::Set(input.name),
+            expires_on: ActiveValue::Set(input.expires_on),
+            redirect_to: ActiveValue::Set(input.redirect_to),
+            maximum_uses: ActiveValue::Set(input.maximum_uses),
+            is_account_default: ActiveValue::Set(input.is_account_default),
+            is_mutation_allowed: ActiveValue::Set(input.is_mutation_allowed),
+            ..Default::default()
+        };
+        let link = new_link.insert(&self.db).await?;
+        Ok(StringIdObject { id: link.id })
+    }
+
+    pub async fn process_access_link(
+        &self,
+        input: ProcessAccessLinkInput,
+    ) -> Result<ProcessAccessLinkResult> {
+        let maybe_link = match input {
+            ProcessAccessLinkInput::Id(id) => AccessLink::find_by_id(id).one(&self.db).await?,
+            ProcessAccessLinkInput::Username(username) => {
+                let user = User::find()
+                    .filter(user::Column::Name.eq(username))
+                    .one(&self.db)
+                    .await?;
+                match user {
+                    None => None,
+                    Some(u) => {
+                        u.find_related(AccessLink)
+                            .filter(access_link::Column::IsAccountDefault.eq(true))
+                            .filter(access_link::Column::IsRevoked.is_null())
+                            .one(&self.db)
+                            .await?
+                    }
+                }
+            }
+        };
+        let link = match maybe_link {
+            None => {
+                return Ok(ProcessAccessLinkResult::Error(ProcessAccessLinkError {
+                    error: ProcessAccessLinkErrorVariant::NotFound,
+                }))
+            }
+            Some(l) => l,
+        };
+        if let Some(expiration_time) = link.expires_on {
+            if expiration_time < Utc::now() {
+                return Ok(ProcessAccessLinkResult::Error(ProcessAccessLinkError {
+                    error: ProcessAccessLinkErrorVariant::Expired,
+                }));
+            }
+        }
+        if let Some(max_uses) = link.maximum_uses {
+            if link.times_used >= max_uses {
+                return Ok(ProcessAccessLinkResult::Error(ProcessAccessLinkError {
+                    error: ProcessAccessLinkErrorVariant::MaximumUsesReached,
+                }));
+            }
+        }
+        if let Some(true) = link.is_revoked {
+            return Ok(ProcessAccessLinkResult::Error(ProcessAccessLinkError {
+                error: ProcessAccessLinkErrorVariant::Revoked,
+            }));
+        }
+        let validity = if let Some(expires) = link.expires_on {
+            (expires - Utc::now()).num_days().try_into().unwrap()
+        } else {
+            self.config.users.token_valid_for_days
+        };
+        let api_key = sign(
+            link.user_id.clone(),
+            &self.config.users.jwt_secret,
+            validity,
+            Some(AccessLinkClaims {
+                id: link.id.clone(),
+                is_demo: link.is_demo,
+            }),
+        )?;
+        let mut issued_tokens = link.issued_tokens.clone();
+        issued_tokens.push(api_key.clone());
+        let mut link: access_link::ActiveModel = link.into();
+        link.issued_tokens = ActiveValue::Set(issued_tokens);
+        let link = link.update(&self.db).await?;
+        Ok(ProcessAccessLinkResult::Ok(ProcessAccessLinkResponse {
+            api_key,
+            token_valid_for_days: validity,
+            redirect_to: link.redirect_to,
+        }))
+    }
+
+    pub async fn revoke_access_link(&self, access_link_id: String) -> Result<bool> {
+        pro_instance_guard(self.is_pro).await?;
+        revoke_access_link(&self.db, access_link_id).await
+    }
+
     pub async fn users_list(&self, query: Option<String>) -> Result<Vec<user::Model>> {
         let users = User::find()
             .apply_if(query, |query, value| {
-                query.filter(Expr::col(user::Column::Name).ilike(ilike_sql(&value)))
+                query.filter(
+                    Expr::col(user::Column::Name)
+                        .ilike(ilike_sql(&value))
+                        .or(Expr::col(user::Column::Id).ilike(ilike_sql(&value))),
+                )
             })
             .order_by_asc(user::Column::Name)
             .all(&self.db)
@@ -193,6 +351,7 @@ impl UserService {
             user_id,
             &self.config.users.jwt_secret,
             self.config.users.token_valid_for_days,
+            None,
         )?;
         Ok(auth_token)
     }
@@ -488,6 +647,10 @@ impl UserService {
                                     preferences.features_enabled.fitness.workouts =
                                         value_bool.unwrap()
                                 }
+                                "templates" => {
+                                    preferences.features_enabled.fitness.templates =
+                                        value_bool.unwrap()
+                                }
                                 _ => return Err(err()),
                             },
                             "media" => {
diff --git a/crates/traits/Cargo.toml b/crates/traits/Cargo.toml
index 160691ed27..e79548a9bf 100644
--- a/crates/traits/Cargo.toml
+++ b/crates/traits/Cargo.toml
@@ -11,7 +11,9 @@ async-graphql = { workspace = true }
 async-trait = { workspace = true }
 common-models = { path = "../models/common" }
 common-utils = { path = "../utils/common" }
+config = { path = "../config" }
 database-models = { path = "../models/database" }
+database-utils = { path = "../utils/database" }
 media-models = { path = "../models/media" }
 sea-orm = { workspace = true }
 tracing = { workspace = true }
diff --git a/crates/traits/src/lib.rs b/crates/traits/src/lib.rs
index c8e3c7abad..e41d4c3071 100644
--- a/crates/traits/src/lib.rs
+++ b/crates/traits/src/lib.rs
@@ -1,4 +1,4 @@
-use std::fmt::Debug;
+use std::{fmt::Debug, sync::Arc};
 
 use anyhow::{bail, Result};
 use application_utils::AuthContext;
@@ -7,12 +7,13 @@ use async_trait::async_trait;
 use common_models::BackendError;
 use common_utils::ryot_log;
 use database_models::metadata_group::MetadataGroupWithoutId;
+use database_utils::check_token;
 use dependent_models::SearchResults;
 use media_models::{
     MediaDetails, MetadataGroupSearchItem, MetadataPerson, MetadataSearchItem,
     PartialMetadataWithoutId, PeopleSearchItem, PersonSourceSpecifics,
 };
-use sea_orm::prelude::DateTimeUtc;
+use sea_orm::{prelude::DateTimeUtc, DatabaseConnection};
 
 #[async_trait]
 pub trait MediaProvider {
@@ -80,6 +81,15 @@ pub trait MediaProvider {
     ) -> Result<(MetadataGroupWithoutId, Vec<PartialMetadataWithoutId>)> {
         bail!("This provider does not support getting group details")
     }
+
+    /// Get recommendations for a media item.
+    async fn get_recommendations_for_metadata(
+        &self,
+        identifier: &str,
+    ) -> Result<Vec<PartialMetadataWithoutId>> {
+        let details = self.metadata_details(identifier).await?;
+        Ok(details.suggestions)
+    }
 }
 
 pub trait MediaProviderLanguages {
@@ -107,6 +117,11 @@ pub trait AuthProvider {
 
     async fn user_id_from_ctx(&self, ctx: &Context<'_>) -> GraphqlResult<String> {
         let auth_ctx = ctx.data_unchecked::<AuthContext>();
+        if let Some(auth_token) = &auth_ctx.auth_token {
+            let config = ctx.data_unchecked::<Arc<config::AppConfig>>();
+            let db = ctx.data_unchecked::<DatabaseConnection>();
+            check_token(auth_token, self.is_mutation(), &config.users.jwt_secret, db).await?;
+        }
         auth_ctx
             .user_id
             .clone()
diff --git a/crates/utils/common/Cargo.toml b/crates/utils/common/Cargo.toml
index fa104ceee2..6785fba610 100644
--- a/crates/utils/common/Cargo.toml
+++ b/crates/utils/common/Cargo.toml
@@ -7,5 +7,5 @@ edition = "2021"
 chrono = { workspace = true }
 compile-time = "=0.2.0"
 const-str = { workspace = true }
-dotenvy_macro = { workspace = true }
+env-utils = { path = "../env" }
 reqwest = { workspace = true }
diff --git a/crates/utils/common/src/lib.rs b/crates/utils/common/src/lib.rs
index e849e35bd5..a1dde168e7 100644
--- a/crates/utils/common/src/lib.rs
+++ b/crates/utils/common/src/lib.rs
@@ -1,19 +1,16 @@
 use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, Utc};
+use env_utils::APP_VERSION;
 use reqwest::header::HeaderValue;
 
 pub const PROJECT_NAME: &str = "ryot";
 pub const AUTHOR: &str = "ignisda";
 pub const AUTHOR_EMAIL: &str = "ignisda2001@gmail.com";
-#[cfg(debug_assertions)]
-pub const VERSION: &str = dotenvy_macro::dotenv!("APP_VERSION");
-#[cfg(not(debug_assertions))]
-pub const VERSION: &str = env!("APP_VERSION");
 pub const USER_AGENT_STR: &str = const_str::concat!(
     AUTHOR,
     "/",
     PROJECT_NAME,
     "-v",
-    VERSION,
+    APP_VERSION,
     " (",
     AUTHOR_EMAIL,
     ")"
diff --git a/crates/utils/database/Cargo.toml b/crates/utils/database/Cargo.toml
index 816affb0e4..5520266f6c 100644
--- a/crates/utils/database/Cargo.toml
+++ b/crates/utils/database/Cargo.toml
@@ -19,6 +19,7 @@ file-storage-service = { path = "../../services/file-storage" }
 fitness-models = { path = "../../models/fitness" }
 futures = { workspace = true }
 itertools = { workspace = true }
+jwt-service = { path = "../../services/jwt" }
 media-models = { path = "../../models/media" }
 migrations = { path = "../../migrations" }
 markdown = { workspace = true }
diff --git a/crates/utils/database/src/lib.rs b/crates/utils/database/src/lib.rs
index 627e0078ac..836985e5b2 100644
--- a/crates/utils/database/src/lib.rs
+++ b/crates/utils/database/src/lib.rs
@@ -11,21 +11,22 @@ use common_models::{
 };
 use common_utils::{ryot_log, IsFeatureEnabled};
 use database_models::{
-    collection, collection_to_entity, daily_user_activity,
+    access_link, collection, collection_to_entity, daily_user_activity,
     functions::associate_user_with_entity,
     metadata,
     prelude::{
-        Collection, CollectionToEntity, DailyUserActivity, Metadata, Review, Seen, User,
-        UserMeasurement, UserToEntity, Workout,
+        AccessLink, Collection, CollectionToEntity, DailyUserActivity, Metadata, Review, Seen,
+        User, UserMeasurement, UserToEntity, Workout, WorkoutTemplate,
     },
     review, seen, user, user_measurement, user_to_entity, workout,
 };
-use dependent_models::UserWorkoutDetails;
+use dependent_models::{UserWorkoutDetails, UserWorkoutTemplateDetails};
 use enums::{EntityLot, MediaLot, SeenState, UserLot, Visibility};
 use file_storage_service::FileStorageService;
 use fitness_models::UserMeasurementsListInput;
 use futures::TryStreamExt;
 use itertools::Itertools;
+use jwt_service::{verify, Claims};
 use markdown::to_html as markdown_to_html;
 use media_models::{
     AnimeSpecifics, AudioBookSpecifics, BookSpecifics, CreateOrUpdateCollectionInput,
@@ -47,6 +48,17 @@ use serde::{Deserialize, Serialize};
 use user_models::{UserPreferences, UserReviewScale};
 use uuid::Uuid;
 
+pub async fn revoke_access_link(db: &DatabaseConnection, access_link_id: String) -> Result<bool> {
+    AccessLink::update(access_link::ActiveModel {
+        id: ActiveValue::Set(access_link_id),
+        is_revoked: ActiveValue::Set(Some(true)),
+        ..Default::default()
+    })
+    .exec(db)
+    .await?;
+    Ok(true)
+}
+
 pub fn ilike_sql(value: &str) -> String {
     format!("%{value}%")
 }
@@ -92,6 +104,15 @@ pub async fn admin_account_guard(db: &DatabaseConnection, user_id: &String) -> R
     Ok(())
 }
 
+pub async fn pro_instance_guard(is_pro: bool) -> Result<()> {
+    if !is_pro {
+        return Err(Error::new(
+            "This feature is only available on the Pro version",
+        ));
+    }
+    Ok(())
+}
+
 pub async fn user_measurements_list(
     db: &DatabaseConnection,
     user_id: &String,
@@ -120,6 +141,7 @@ fn get_cte_column_from_lot(entity_lot: EntityLot) -> collection_to_entity::Colum
         EntityLot::MetadataGroup => CteColAlias::MetadataGroupId,
         EntityLot::Exercise => CteColAlias::ExerciseId,
         EntityLot::Workout => CteColAlias::WorkoutId,
+        EntityLot::WorkoutTemplate => CteColAlias::WorkoutTemplateId,
         EntityLot::Collection => unreachable!(),
     }
 }
@@ -192,6 +214,34 @@ pub async fn workout_details(
     }
 }
 
+pub async fn workout_template_details(
+    db: &DatabaseConnection,
+    user_id: &String,
+    workout_template_id: String,
+) -> Result<UserWorkoutTemplateDetails> {
+    let maybe_template = WorkoutTemplate::find_by_id(workout_template_id.clone())
+        .one(db)
+        .await?;
+    match maybe_template {
+        None => Err(Error::new(
+            "Workout template with the given ID could not be found.",
+        )),
+        Some(details) => {
+            let collections = entity_in_collections(
+                db,
+                user_id,
+                &workout_template_id,
+                EntityLot::WorkoutTemplate,
+            )
+            .await?;
+            Ok(UserWorkoutTemplateDetails {
+                details,
+                collections,
+            })
+        }
+    }
+}
+
 pub fn apply_collection_filter<E, C, D>(
     query: Select<E>,
     collection_id: Option<Vec<String>>,
@@ -275,11 +325,15 @@ pub async fn add_entity_to_collection(
             }
             EntityLot::Exercise => created_collection.exercise_id = ActiveValue::Set(Some(id)),
             EntityLot::Workout => created_collection.workout_id = ActiveValue::Set(Some(id)),
+            EntityLot::WorkoutTemplate => {
+                created_collection.workout_template_id = ActiveValue::Set(Some(id))
+            }
             EntityLot::Collection => unreachable!(),
         }
         let created = created_collection.insert(db).await?;
         ryot_log!(debug, "Created collection to entity: {:?}", created);
-        if input.entity_lot != EntityLot::Workout {
+        if input.entity_lot != EntityLot::Workout && input.entity_lot != EntityLot::WorkoutTemplate
+        {
             associate_user_with_entity(db, user_id, input.entity_id, input.entity_lot)
                 .await
                 .ok();
@@ -296,6 +350,43 @@ pub async fn add_entity_to_collection(
     Ok(true)
 }
 
+pub fn user_claims_from_token(token: &str, jwt_secret: &str) -> Result<Claims> {
+    verify(token, jwt_secret).map_err(|e| Error::new(format!("Encountered error: {:?}", e)))
+}
+
+/// If the token has an access link, then checks that:
+/// - the access link is not revoked
+/// - if the operation is a mutation, then the access link allows mutations
+///
+/// If any of the above conditions are not met, then an error is returned.
+#[inline]
+pub async fn check_token(
+    token: &str,
+    is_mutation: bool,
+    jwt_secret: &str,
+    db: &DatabaseConnection,
+) -> Result<bool> {
+    let claims = user_claims_from_token(token, jwt_secret)?;
+    if let Some(access_link) = claims.access_link {
+        let access_link = AccessLink::find_by_id(access_link.id)
+            .one(db)
+            .await?
+            .ok_or_else(|| Error::new(BackendError::SessionExpired.to_string()))?;
+        if access_link.is_revoked.unwrap_or_default() {
+            return Err(Error::new(BackendError::SessionExpired.to_string()));
+        }
+        if is_mutation {
+            if !access_link.is_mutation_allowed.unwrap_or_default() {
+                return Err(Error::new(BackendError::MutationNotAllowed.to_string()));
+            }
+            return Ok(true);
+        }
+        Ok(true)
+    } else {
+        Ok(true)
+    }
+}
+
 pub async fn remove_entity_from_collection(
     db: &DatabaseConnection,
     user_id: &String,
@@ -315,7 +406,7 @@ pub async fn remove_entity_from_collection(
         .filter(column.eq(input.entity_id.clone()))
         .exec(db)
         .await?;
-    if input.entity_lot != EntityLot::Workout {
+    if input.entity_lot != EntityLot::Workout && input.entity_lot != EntityLot::WorkoutTemplate {
         associate_user_with_entity(db, user_id, input.entity_id, input.entity_lot).await?;
     }
     Ok(StringIdObject { id: collect.id })
@@ -333,7 +424,7 @@ pub async fn item_reviews(
         EntityLot::Person => review::Column::PersonId,
         EntityLot::Exercise => review::Column::ExerciseId,
         EntityLot::Collection => review::Column::CollectionId,
-        EntityLot::Workout => unreachable!(),
+        EntityLot::Workout | EntityLot::WorkoutTemplate => unreachable!(),
     };
     let all_reviews = Review::find()
         .filter(review::Column::UserId.eq(user_id))
@@ -438,7 +529,10 @@ pub async fn create_or_update_collection(
                 .await
                 .map_err(|_| Error::new("There was an error creating the collection".to_owned()))?;
             let id = inserted.id.unwrap();
-            let collaborators = vec![user_id.to_owned()];
+            let mut collaborators = vec![user_id.to_owned()];
+            if let Some(input_collaborators) = input.collaborators {
+                collaborators.extend(input_collaborators);
+            }
             let inserts = collaborators
                 .into_iter()
                 .map(|c| user_to_entity::ActiveModel {
@@ -516,6 +610,7 @@ pub async fn calculate_user_activities_and_summary(
                 user_id: user_id.to_owned(),
                 ..Default::default()
             });
+        existing.entity_ids.push(entity_id);
         existing
     }
     let mut seen_stream = Seen::find()
@@ -561,7 +656,6 @@ pub async fn calculate_user_activities_and_summary(
                 .get_episode(show_extra.season, show_extra.episode)
                 .and_then(|(_, e)| e.runtime)
             {
-                activity.show_count += 1;
                 activity.show_duration += runtime;
             }
         } else if let (Some(podcast_seen), Some(podcast_extra)) =
diff --git a/crates/utils/env/Cargo.toml b/crates/utils/env/Cargo.toml
new file mode 100644
index 0000000000..63cac6e8d6
--- /dev/null
+++ b/crates/utils/env/Cargo.toml
@@ -0,0 +1,7 @@
+[package]
+name = "env-utils"
+version = "0.1.0"
+edition = "2021"
+
+[build-dependencies]
+dotenv-build = "0.1.1"
diff --git a/crates/utils/env/build.rs b/crates/utils/env/build.rs
new file mode 100644
index 0000000000..5b14a16e11
--- /dev/null
+++ b/crates/utils/env/build.rs
@@ -0,0 +1,3 @@
+fn main() {
+    dotenv_build::output(dotenv_build::Config::default()).unwrap();
+}
diff --git a/crates/utils/env/src/lib.rs b/crates/utils/env/src/lib.rs
new file mode 100644
index 0000000000..972f1a57e0
--- /dev/null
+++ b/crates/utils/env/src/lib.rs
@@ -0,0 +1,5 @@
+pub const APP_VERSION: &str = env!("APP_VERSION");
+pub const UNKEY_API_ID: &str = env!("UNKEY_API_ID");
+pub const DEFAULT_TMDB_ACCESS_TOKEN: &str = env!("DEFAULT_TMDB_ACCESS_TOKEN");
+pub const DEFAULT_MAL_CLIENT_ID: &str = env!("DEFAULT_MAL_CLIENT_ID");
+pub const TRAKT_CLIENT_ID: &str = env!("TRAKT_CLIENT_ID");
diff --git a/docs/content/.pages b/docs/content/.pages
index ae98e49e02..e5bfa1ce9e 100644
--- a/docs/content/.pages
+++ b/docs/content/.pages
@@ -2,4 +2,4 @@ nav:
     - index.md
     - migration.md
     - ...
-    - architecture.md
+    - contributing.md
diff --git a/docs/content/architecture.md b/docs/content/architecture.md
deleted file mode 100644
index 053613c8b5..0000000000
--- a/docs/content/architecture.md
+++ /dev/null
@@ -1,46 +0,0 @@
-# Architecture
-
-The frontend is a Remix app, the backend is an Axum server. All these run behind a Caddy
-reverse proxy and are managed by `concurrently`.
-
-## Releases
-
-Each version of Ryot is released as a docker image. Then it is associated with the latest
-Github release of the major version. For example, if the latest tag is `v5.2.1`, then
-the Github release will be called `Version 5` and the docker image will be tagged as
-`:v5.2.1`, `v5.2`, `v5` and `:latest`.
-
-## Development
-
-There is a [devcontainer](https://code.visualstudio.com/docs/devcontainers/containers)
-configuration in the repository. You can use it to launch a development environment
-with all tools installed.
-
-### Environment
-
-In development, both servers are started independently running on `:3000` and `:5000`
-respectively and reverse proxied at `:8000`. To get them running, install
-[mprocs](https://github.com/pvolok/mprocs), and run `mprocs` in the project root.
-
-I also recommend the following environment file:
-
-```bash title=".env"
-DATABASE_URL=postgres://postgres:postgres@postgres:5432/postgres
-RUST_LOG=ryot=trace,sea_orm=debug
-```
-
-Your website would be available at `http://localhost:8000`.
-
-### Miscellaneous
-
-You can use [Serveo](https://serveo.net/) to test webhooks.
-
-```bash
-ssh -R ryot:80:0.0.0.0:8000 serveo.net
-```
-
-This will expose your local server on `https://ryot.serveo.net`. You can use this URL
-in Jellyfin etc. to test events sent to your local Ryot instance.
-
-Another helpful tool is [Webhook.site](https://webhook.site/). It can be used to inspect
-the requests sent to your server.
diff --git a/docs/content/contributing.md b/docs/content/contributing.md
new file mode 100644
index 0000000000..aedf0e7869
--- /dev/null
+++ b/docs/content/contributing.md
@@ -0,0 +1,31 @@
+# Contributing
+
+Each version of Ryot is released as docker images. For example, if the latest tag is
+`v5.2.1`, then the docker image will be tagged as `v5.2.1`, `v5.2`, `v5` and `latest`. The
+images will be made available on [Docker Hub](https://hub.docker.com/r/ignisda/ryot) and
+[GitHub Container Registry](https://ghcr.io/ignisda/ryot).
+
+## Development
+
+There is a [devcontainer](https://code.visualstudio.com/docs/devcontainers/containers)
+configuration in the repository. You can use it to launch a development environment
+with all tools installed.
+
+### Environment
+
+Create the following environment file in the root of the repository:
+
+```bash title=".env"
+DATABASE_URL=postgres://postgres:postgres@postgres:5432/postgres
+UNKEY_API_ID=api_4GvvJVbWobkNjcnnvFHmBP5pXb4K
+APP_VERSION=v5.2.1
+DEFAULT_TMDB_ACCESS_TOKEN=your-tmdb-access-token
+DEFAULT_MAL_CLIENT_ID=your-mal-client-id
+TRAKT_CLIENT_ID=your-trakt-client-id
+```
+
+In development, both servers are started independently running on `:3000` and `:5000`
+respectively and reverse proxied at `:8000`. To get everything started, run `mprocs` in the
+project root.
+
+Your website would be available at `http://localhost:8000`.
diff --git a/docs/content/index.md b/docs/content/index.md
index d4d0aac4c0..bddc15b6d2 100644
--- a/docs/content/index.md
+++ b/docs/content/index.md
@@ -34,11 +34,6 @@ the features of the pro version, check the [features page]({{extra.main_website_
 
 ## Upgrading to Pro
 
-!!! info
-
-    The pro version is not backwards compatible with the community version, so be sure to
-    [backup](./guides/exporting.md#exporting-the-entire-database) your data before upgrading.
-
 To upgrade to the pro version, you need to provide a `SERVER_PRO_KEY` environment variable.
 You can get a key by purchasing it from the [website]({{ extra.main_website_url }}).
 
@@ -46,8 +41,10 @@ Once you have the key, you can set it in the `docker-compose.yml` file:
 
 ```diff
   ryot:
--   image: ignisda/ryot:latest # or ghcr.io/ignisda/ryot:latest
-+   image: ignisda/ryot-pro:latest # or ghcr.io/ignisda/ryot-pro:latest
     environment:
 +      - SERVER_PRO_KEY=<pro_key_issued_to_you>
 ```
+
+If the key is invalid or your subscription has expired, the server will automatically start
+with the community version. Since the two versions are compatible, you can switch between
+them by simply fixing the key and restarting the server.
diff --git a/docs/includes/backend-config-schema.yaml b/docs/includes/backend-config-schema.yaml
index 234dd727f2..ee03f4d346 100644
--- a/docs/includes/backend-config-schema.yaml
+++ b/docs/includes/backend-config-schema.yaml
@@ -187,6 +187,10 @@ server:
     # @envvar SERVER_OIDC_ISSUER_URL
     issuer_url: ""
 
+  # The pro key assigned to the user.
+  # @envvar SERVER_PRO_KEY
+  pro_key: ""
+
   # The hours in which a media can be marked as seen again for a user. This
   # is used so that the same media can not be used marked as started when
   # it has been already marked as seen in the last `n` hours.
diff --git a/docs/includes/export-schema.ts b/docs/includes/export-schema.ts
index c104cc4f40..0f3f1f2127 100644
--- a/docs/includes/export-schema.ts
+++ b/docs/includes/export-schema.ts
@@ -305,6 +305,25 @@ export interface WorkoutSummary {
 	total: WorkoutOrExerciseTotals | null;
 }
 
+export interface WorkoutTemplate {
+	created_on: string;
+	default_rest_timer: number | null;
+	id: string;
+	information: WorkoutInformation;
+	name: string;
+	summary: WorkoutSummary;
+	/**
+	 * @default 'public'
+	 * @type {'public' | 'private'}
+	 */
+	visibility: Visibility;
+}
+
+export interface ImportOrExportWorkoutTemplateItem {
+	collections: string[];
+	details: WorkoutTemplate;
+}
+
 /** A workout that was completed by the user. */
 export interface Workout {
 	duration: number;
@@ -314,6 +333,7 @@ export interface Workout {
 	name: string;
 	start_time: string;
 	summary: WorkoutSummary;
+	template_id: string | null;
 }
 
 /** Details about a specific exercise item that needs to be exported. */
@@ -336,6 +356,8 @@ export interface CompleteExport {
 	media_group: ImportOrExportMediaGroupItem[] | null;
 	/** Data about user's people. */
 	people: ImportOrExportPersonItem[] | null;
+	/** Data about user's workout templates. */
+	workout_templates: ImportOrExportWorkoutTemplateItem[] | null;
 	/** Data about user's workouts. */
 	workouts: ImportOrExportWorkoutItem[] | null;
 }
diff --git a/docs/logo.png b/docs/logo.png
deleted file mode 100644
index a1d06a2d16..0000000000
Binary files a/docs/logo.png and /dev/null differ
diff --git a/libs/generated/src/graphql/backend/gql.ts b/libs/generated/src/graphql/backend/gql.ts
index 1e044f3770..70c15aa902 100644
--- a/libs/generated/src/graphql/backend/gql.ts
+++ b/libs/generated/src/graphql/backend/gql.ts
@@ -13,7 +13,7 @@ import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/
  * Therefore it is highly recommended to use the babel or swc plugin for production.
  */
 const documents = {
-    "mutation RegisterUser($input: RegisterUserInput!) {\n  registerUser(input: $input) {\n    __typename\n    ... on RegisterError {\n      error\n    }\n    ... on StringIdObject {\n      id\n    }\n  }\n}\n\nmutation LoginUser($input: AuthUserInput!) {\n  loginUser(input: $input) {\n    __typename\n    ... on LoginError {\n      error\n    }\n    ... on LoginResponse {\n      apiKey\n    }\n  }\n}\n\nmutation AddEntityToCollection($input: ChangeCollectionToEntityInput!) {\n  addEntityToCollection(input: $input)\n}\n\nmutation CommitMetadata($input: CommitMediaInput!) {\n  commitMetadata(input: $input) {\n    id\n  }\n}\n\nmutation CommitMetadataGroup($input: CommitMediaInput!) {\n  commitMetadataGroup(input: $input) {\n    id\n  }\n}\n\nmutation CommitPerson($input: CommitPersonInput!) {\n  commitPerson(input: $input) {\n    id\n  }\n}\n\nmutation CreateCustomExercise($input: ExerciseInput!) {\n  createCustomExercise(input: $input)\n}\n\nmutation UpdateCustomExercise($input: UpdateCustomExerciseInput!) {\n  updateCustomExercise(input: $input)\n}\n\nmutation UpdateUserIntegration($input: UpdateUserIntegrationInput!) {\n  updateUserIntegration(input: $input)\n}\n\nmutation CreateCustomMetadata($input: CreateCustomMetadataInput!) {\n  createCustomMetadata(input: $input) {\n    id\n  }\n}\n\nmutation CreateOrUpdateCollection($input: CreateOrUpdateCollectionInput!) {\n  createOrUpdateCollection(input: $input) {\n    id\n  }\n}\n\nmutation CreateReviewComment($input: CreateReviewCommentInput!) {\n  createReviewComment(input: $input)\n}\n\nmutation CreateUserMeasurement($input: UserMeasurementInput!) {\n  createUserMeasurement(input: $input)\n}\n\nmutation CreateUserNotificationPlatform($input: CreateUserNotificationPlatformInput!) {\n  createUserNotificationPlatform(input: $input)\n}\n\nmutation CreateUserIntegration($input: CreateUserIntegrationInput!) {\n  createUserIntegration(input: $input) {\n    id\n  }\n}\n\nmutation CreateUserWorkout($input: UserWorkoutInput!) {\n  createUserWorkout(input: $input)\n}\n\nmutation DeleteCollection($collectionName: String!) {\n  deleteCollection(collectionName: $collectionName)\n}\n\nmutation DeleteReview($reviewId: String!) {\n  deleteReview(reviewId: $reviewId)\n}\n\nmutation DeleteS3Object($key: String!) {\n  deleteS3Object(key: $key)\n}\n\nmutation DeleteSeenItem($seenId: String!) {\n  deleteSeenItem(seenId: $seenId) {\n    id\n  }\n}\n\nmutation DeleteUser($toDeleteUserId: String!) {\n  deleteUser(toDeleteUserId: $toDeleteUserId)\n}\n\nmutation DeleteUserIntegration($integrationId: String!) {\n  deleteUserIntegration(integrationId: $integrationId)\n}\n\nmutation DeleteUserMeasurement($timestamp: DateTime!) {\n  deleteUserMeasurement(timestamp: $timestamp)\n}\n\nmutation DeleteUserNotificationPlatform($notificationId: String!) {\n  deleteUserNotificationPlatform(notificationId: $notificationId)\n}\n\nmutation DeleteUserWorkout($workoutId: String!) {\n  deleteUserWorkout(workoutId: $workoutId)\n}\n\nmutation DeployBackgroundJob($jobName: BackgroundJob!) {\n  deployBackgroundJob(jobName: $jobName)\n}\n\nmutation DeployBulkProgressUpdate($input: [ProgressUpdateInput!]!) {\n  deployBulkProgressUpdate(input: $input)\n}\n\nmutation DeployExportJob {\n  deployExportJob\n}\n\nmutation DeployImportJob($input: DeployImportJobInput!) {\n  deployImportJob(input: $input)\n}\n\nmutation DeployUpdateMetadataJob($metadataId: String!) {\n  deployUpdateMetadataJob(metadataId: $metadataId)\n}\n\nmutation DeployUpdatePersonJob($personId: String!) {\n  deployUpdatePersonJob(personId: $personId)\n}\n\nmutation DeployUpdateMetadataGroupJob($metadataGroupId: String!) {\n  deployUpdateMetadataGroupJob(metadataGroupId: $metadataGroupId)\n}\n\nmutation UpdateSeenItem($input: UpdateSeenItemInput!) {\n  updateSeenItem(input: $input)\n}\n\nmutation UpdateUserNotificationPlatform($input: UpdateUserNotificationPlatformInput!) {\n  updateUserNotificationPlatform(input: $input)\n}\n\nmutation UpdateUserWorkout($input: UpdateUserWorkoutInput!) {\n  updateUserWorkout(input: $input)\n}\n\nmutation GenerateAuthToken {\n  generateAuthToken\n}\n\nmutation MergeMetadata($mergeFrom: String!, $mergeInto: String!) {\n  mergeMetadata(mergeFrom: $mergeFrom, mergeInto: $mergeInto)\n}\n\nmutation DisassociateMetadata($metadataId: String!) {\n  disassociateMetadata(metadataId: $metadataId)\n}\n\nmutation PostReview($input: PostReviewInput!) {\n  postReview(input: $input) {\n    id\n  }\n}\n\nmutation PresignedPutS3Url($input: PresignedPutUrlInput!) {\n  presignedPutS3Url(input: $input) {\n    key\n    uploadUrl\n  }\n}\n\nmutation RemoveEntityFromCollection($input: ChangeCollectionToEntityInput!) {\n  removeEntityFromCollection(input: $input) {\n    id\n  }\n}\n\nmutation TestUserNotificationPlatforms {\n  testUserNotificationPlatforms\n}\n\nmutation UpdateUser($input: UpdateUserInput!) {\n  updateUser(input: $input) {\n    id\n  }\n}\n\nmutation UpdateUserPreference($input: UpdateUserPreferenceInput!) {\n  updateUserPreference(input: $input)\n}": types.RegisterUserDocument,
+    "mutation RegisterUser($input: RegisterUserInput!) {\n  registerUser(input: $input) {\n    __typename\n    ... on RegisterError {\n      error\n    }\n    ... on StringIdObject {\n      id\n    }\n  }\n}\n\nmutation LoginUser($input: AuthUserInput!) {\n  loginUser(input: $input) {\n    __typename\n    ... on LoginError {\n      error\n    }\n    ... on LoginResponse {\n      apiKey\n    }\n  }\n}\n\nmutation AddEntityToCollection($input: ChangeCollectionToEntityInput!) {\n  addEntityToCollection(input: $input)\n}\n\nmutation CommitMetadata($input: CommitMediaInput!) {\n  commitMetadata(input: $input) {\n    id\n  }\n}\n\nmutation CommitMetadataGroup($input: CommitMediaInput!) {\n  commitMetadataGroup(input: $input) {\n    id\n  }\n}\n\nmutation CommitPerson($input: CommitPersonInput!) {\n  commitPerson(input: $input) {\n    id\n  }\n}\n\nmutation CreateCustomExercise($input: ExerciseInput!) {\n  createCustomExercise(input: $input)\n}\n\nmutation UpdateCustomExercise($input: UpdateCustomExerciseInput!) {\n  updateCustomExercise(input: $input)\n}\n\nmutation UpdateUserIntegration($input: UpdateUserIntegrationInput!) {\n  updateUserIntegration(input: $input)\n}\n\nmutation CreateCustomMetadata($input: CreateCustomMetadataInput!) {\n  createCustomMetadata(input: $input) {\n    id\n  }\n}\n\nmutation CreateOrUpdateCollection($input: CreateOrUpdateCollectionInput!) {\n  createOrUpdateCollection(input: $input) {\n    id\n  }\n}\n\nmutation CreateReviewComment($input: CreateReviewCommentInput!) {\n  createReviewComment(input: $input)\n}\n\nmutation CreateUserMeasurement($input: UserMeasurementInput!) {\n  createUserMeasurement(input: $input)\n}\n\nmutation CreateUserNotificationPlatform($input: CreateUserNotificationPlatformInput!) {\n  createUserNotificationPlatform(input: $input)\n}\n\nmutation CreateUserIntegration($input: CreateUserIntegrationInput!) {\n  createUserIntegration(input: $input) {\n    id\n  }\n}\n\nmutation CreateUserWorkout($input: UserWorkoutInput!) {\n  createUserWorkout(input: $input)\n}\n\nmutation CreateOrUpdateWorkoutTemplate($input: UserWorkoutInput!) {\n  createOrUpdateWorkoutTemplate(input: $input)\n}\n\nmutation DeleteCollection($collectionName: String!) {\n  deleteCollection(collectionName: $collectionName)\n}\n\nmutation DeleteReview($reviewId: String!) {\n  deleteReview(reviewId: $reviewId)\n}\n\nmutation DeleteS3Object($key: String!) {\n  deleteS3Object(key: $key)\n}\n\nmutation DeleteSeenItem($seenId: String!) {\n  deleteSeenItem(seenId: $seenId) {\n    id\n  }\n}\n\nmutation DeleteUser($toDeleteUserId: String!) {\n  deleteUser(toDeleteUserId: $toDeleteUserId)\n}\n\nmutation DeleteUserIntegration($integrationId: String!) {\n  deleteUserIntegration(integrationId: $integrationId)\n}\n\nmutation DeleteUserMeasurement($timestamp: DateTime!) {\n  deleteUserMeasurement(timestamp: $timestamp)\n}\n\nmutation DeleteUserNotificationPlatform($notificationId: String!) {\n  deleteUserNotificationPlatform(notificationId: $notificationId)\n}\n\nmutation DeleteUserWorkout($workoutId: String!) {\n  deleteUserWorkout(workoutId: $workoutId)\n}\n\nmutation DeleteWorkoutTemplate($workoutTemplateId: String!) {\n  deleteWorkoutTemplate(workoutTemplateId: $workoutTemplateId)\n}\n\nmutation DeployBackgroundJob($jobName: BackgroundJob!) {\n  deployBackgroundJob(jobName: $jobName)\n}\n\nmutation DeployBulkProgressUpdate($input: [ProgressUpdateInput!]!) {\n  deployBulkProgressUpdate(input: $input)\n}\n\nmutation DeployExportJob {\n  deployExportJob\n}\n\nmutation DeployImportJob($input: DeployImportJobInput!) {\n  deployImportJob(input: $input)\n}\n\nmutation DeployUpdateMetadataJob($metadataId: String!) {\n  deployUpdateMetadataJob(metadataId: $metadataId)\n}\n\nmutation DeployUpdatePersonJob($personId: String!) {\n  deployUpdatePersonJob(personId: $personId)\n}\n\nmutation DeployUpdateMetadataGroupJob($metadataGroupId: String!) {\n  deployUpdateMetadataGroupJob(metadataGroupId: $metadataGroupId)\n}\n\nmutation UpdateSeenItem($input: UpdateSeenItemInput!) {\n  updateSeenItem(input: $input)\n}\n\nmutation UpdateUserNotificationPlatform($input: UpdateUserNotificationPlatformInput!) {\n  updateUserNotificationPlatform(input: $input)\n}\n\nmutation UpdateUserWorkout($input: UpdateUserWorkoutInput!) {\n  updateUserWorkout(input: $input)\n}\n\nmutation GenerateAuthToken {\n  generateAuthToken\n}\n\nmutation MergeMetadata($mergeFrom: String!, $mergeInto: String!) {\n  mergeMetadata(mergeFrom: $mergeFrom, mergeInto: $mergeInto)\n}\n\nmutation DisassociateMetadata($metadataId: String!) {\n  disassociateMetadata(metadataId: $metadataId)\n}\n\nmutation PostReview($input: PostReviewInput!) {\n  postReview(input: $input) {\n    id\n  }\n}\n\nmutation PresignedPutS3Url($input: PresignedPutUrlInput!) {\n  presignedPutS3Url(input: $input) {\n    key\n    uploadUrl\n  }\n}\n\nmutation RemoveEntityFromCollection($input: ChangeCollectionToEntityInput!) {\n  removeEntityFromCollection(input: $input) {\n    id\n  }\n}\n\nmutation TestUserNotificationPlatforms {\n  testUserNotificationPlatforms\n}\n\nmutation UpdateUser($input: UpdateUserInput!) {\n  updateUser(input: $input) {\n    id\n  }\n}\n\nmutation UpdateUserPreference($input: UpdateUserPreferenceInput!) {\n  updateUserPreference(input: $input)\n}\n\nmutation CreateAccessLink($input: CreateAccessLinkInput!) {\n  createAccessLink(input: $input) {\n    id\n  }\n}\n\nmutation ProcessAccessLink($input: ProcessAccessLinkInput!) {\n  processAccessLink(input: $input) {\n    __typename\n    ... on ProcessAccessLinkError {\n      error\n    }\n    ... on ProcessAccessLinkResponse {\n      apiKey\n      redirectTo\n      tokenValidForDays\n    }\n  }\n}\n\nmutation revokeAccessLink($accessLinkId: String!) {\n  revokeAccessLink(accessLinkId: $accessLinkId)\n}": types.RegisterUserDocument,
     "query CollectionContents($input: CollectionContentsInput!) {\n  collectionContents(input: $input) {\n    user {\n      id\n      name\n    }\n    reviews {\n      ...ReviewItemPart\n    }\n    results {\n      details {\n        total\n        nextPage\n      }\n      items {\n        entityId\n        entityLot\n      }\n    }\n    details {\n      name\n      description\n      createdOn\n    }\n  }\n}": types.CollectionContentsDocument,
     "query CoreDetails {\n  coreDetails {\n    isPro\n    version\n    docsLink\n    pageLimit\n    websiteUrl\n    smtpEnabled\n    oidcEnabled\n    signupAllowed\n    repositoryLink\n    localAuthDisabled\n    fileStorageEnabled\n    tokenValidForDays\n  }\n}": types.CoreDetailsDocument,
     "query ExerciseDetails($exerciseId: String!) {\n  exerciseDetails(exerciseId: $exerciseId) {\n    id\n    lot\n    source\n    level\n    force\n    mechanic\n    equipment\n    muscles\n    createdByUserId\n    attributes {\n      instructions\n      images\n    }\n  }\n}": types.ExerciseDetailsDocument,
@@ -36,11 +36,13 @@ const documents = {
     "query UserMetadataDetails($metadataId: String!) {\n  userMetadataDetails(metadataId: $metadataId) {\n    mediaReason\n    hasInteracted\n    collections {\n      ...CollectionPart\n    }\n    inProgress {\n      ...SeenPart\n    }\n    history {\n      ...SeenPart\n    }\n    averageRating\n    reviews {\n      ...ReviewItemPart\n    }\n    seenByAllCount\n    seenByUserCount\n    nextEntry {\n      season\n      volume\n      episode\n      chapter\n    }\n    showProgress {\n      timesSeen\n      seasonNumber\n      episodes {\n        episodeNumber\n        timesSeen\n      }\n    }\n    podcastProgress {\n      episodeNumber\n      timesSeen\n    }\n  }\n}": types.UserMetadataDetailsDocument,
     "query UserMetadataGroupDetails($metadataGroupId: String!) {\n  userMetadataGroupDetails(metadataGroupId: $metadataGroupId) {\n    reviews {\n      ...ReviewItemPart\n    }\n    collections {\n      ...CollectionPart\n    }\n  }\n}": types.UserMetadataGroupDetailsDocument,
     "query UserPersonDetails($personId: String!) {\n  userPersonDetails(personId: $personId) {\n    collections {\n      ...CollectionPart\n    }\n    reviews {\n      ...ReviewItemPart\n    }\n  }\n}": types.UserPersonDetailsDocument,
-    "query UserPreferences {\n  userPreferences {\n    general {\n      reviewScale\n      displayNsfw\n      disableVideos\n      persistQueries\n      watchProviders\n      disableReviews\n      disableIntegrations\n      disableWatchProviders\n      disableNavigationAnimation\n      dashboard {\n        section\n        hidden\n        numElements\n      }\n    }\n    fitness {\n      measurements {\n        custom {\n          name\n          dataType\n        }\n        inbuilt {\n          weight\n          bodyMassIndex\n          totalBodyWater\n          muscle\n          leanBodyMass\n          bodyFat\n          boneMass\n          visceralFat\n          waistCircumference\n          waistToHeightRatio\n          hipCircumference\n          waistToHipRatio\n          chestCircumference\n          thighCircumference\n          bicepsCircumference\n          neckCircumference\n          bodyFatCaliper\n          chestSkinfold\n          abdominalSkinfold\n          thighSkinfold\n          basalMetabolicRate\n          totalDailyEnergyExpenditure\n          calories\n        }\n      }\n      exercises {\n        unitSystem\n      }\n    }\n    notifications {\n      toSend\n      enabled\n    }\n    featuresEnabled {\n      others {\n        calendar\n        collections\n      }\n      fitness {\n        enabled\n        workouts\n        measurements\n      }\n      media {\n        enabled\n        anime\n        audioBook\n        book\n        manga\n        movie\n        podcast\n        show\n        videoGame\n        visualNovel\n        people\n        groups\n        genres\n      }\n    }\n  }\n}": types.UserPreferencesDocument,
+    "query UserPreferences {\n  userPreferences {\n    general {\n      reviewScale\n      displayNsfw\n      disableVideos\n      persistQueries\n      watchProviders\n      disableReviews\n      disableIntegrations\n      disableWatchProviders\n      disableNavigationAnimation\n      dashboard {\n        section\n        hidden\n        numElements\n      }\n    }\n    fitness {\n      measurements {\n        custom {\n          name\n          dataType\n        }\n        inbuilt {\n          weight\n          bodyMassIndex\n          totalBodyWater\n          muscle\n          leanBodyMass\n          bodyFat\n          boneMass\n          visceralFat\n          waistCircumference\n          waistToHeightRatio\n          hipCircumference\n          waistToHipRatio\n          chestCircumference\n          thighCircumference\n          bicepsCircumference\n          neckCircumference\n          bodyFatCaliper\n          chestSkinfold\n          abdominalSkinfold\n          thighSkinfold\n          basalMetabolicRate\n          totalDailyEnergyExpenditure\n          calories\n        }\n      }\n      exercises {\n        unitSystem\n      }\n    }\n    notifications {\n      toSend\n      enabled\n    }\n    featuresEnabled {\n      others {\n        calendar\n        collections\n      }\n      fitness {\n        enabled\n        workouts\n        templates\n        measurements\n      }\n      media {\n        enabled\n        anime\n        audioBook\n        book\n        manga\n        movie\n        podcast\n        show\n        videoGame\n        visualNovel\n        people\n        groups\n        genres\n      }\n    }\n  }\n}": types.UserPreferencesDocument,
+    "query UserWorkoutTemplatesList($input: SearchInput!) {\n  userWorkoutTemplatesList(input: $input) {\n    details {\n      total\n      nextPage\n    }\n    items {\n      id\n      name\n      createdOn\n      visibility\n      summary {\n        ...WorkoutSummaryPart\n      }\n    }\n  }\n}": types.UserWorkoutTemplatesListDocument,
     "query UserWorkoutsList($input: SearchInput!) {\n  userWorkoutsList(input: $input) {\n    details {\n      total\n      nextPage\n    }\n    items {\n      id\n      name\n      endTime\n      duration\n      startTime\n      summary {\n        ...WorkoutSummaryPart\n      }\n    }\n  }\n}": types.UserWorkoutsListDocument,
-    "query WorkoutDetails($workoutId: String!) {\n  workoutDetails(workoutId: $workoutId) {\n    collections {\n      ...CollectionPart\n    }\n    details {\n      id\n      name\n      endTime\n      duration\n      startTime\n      repeatedFrom\n      summary {\n        ...WorkoutSummaryPart\n      }\n      information {\n        ...WorkoutInformationPart\n      }\n    }\n  }\n}": types.WorkoutDetailsDocument,
-    "query GetOidcRedirectUrl {\n  getOidcRedirectUrl\n}\n\nquery UserByOidcIssuerId($oidcIssuerId: String!) {\n  userByOidcIssuerId(oidcIssuerId: $oidcIssuerId)\n}\n\nquery GetOidcToken($code: String!) {\n  getOidcToken(code: $code) {\n    subject\n    email\n  }\n}\n\nquery GetPresignedS3Url($key: String!) {\n  getPresignedS3Url(key: $key)\n}\n\nquery ProvidersLanguageInformation {\n  providersLanguageInformation {\n    supported\n    default\n    source\n  }\n}\n\nquery UserExports {\n  userExports {\n    url\n    size\n    endedAt\n    startedAt\n  }\n}\n\nquery UserCollectionsList($name: String) {\n  userCollectionsList(name: $name) {\n    id\n    name\n    count\n    isDefault\n    description\n    creator {\n      id\n      name\n    }\n    collaborators {\n      id\n      name\n    }\n    informationTemplate {\n      lot\n      name\n      required\n      description\n      defaultValue\n    }\n  }\n}\n\nquery UserIntegrations {\n  userIntegrations {\n    id\n    lot\n    provider\n    createdOn\n    isDisabled\n    maximumProgress\n    minimumProgress\n    lastTriggeredOn\n  }\n}\n\nquery UserNotificationPlatforms {\n  userNotificationPlatforms {\n    id\n    lot\n    createdOn\n    isDisabled\n    description\n  }\n}\n\nquery UsersList($query: String) {\n  usersList(query: $query) {\n    id\n    lot\n    name\n    isDisabled\n  }\n}\n\nquery UserUpcomingCalendarEvents($input: UserUpcomingCalendarEventInput!) {\n  userUpcomingCalendarEvents(input: $input) {\n    ...CalendarEventPart\n  }\n}\n\nquery UserCalendarEvents($input: UserCalendarEventInput!) {\n  userCalendarEvents(input: $input) {\n    date\n    events {\n      ...CalendarEventPart\n    }\n  }\n}\n\nquery MetadataPartialDetails($metadataId: String!) {\n  metadataPartialDetails(metadataId: $metadataId) {\n    id\n    lot\n    title\n    image\n    publishYear\n  }\n}\n\nquery MetadataGroupsList($input: MetadataGroupsListInput!) {\n  metadataGroupsList(input: $input) {\n    details {\n      total\n      nextPage\n    }\n    items\n  }\n}\n\nquery PeopleList($input: PeopleListInput!) {\n  peopleList(input: $input) {\n    details {\n      total\n      nextPage\n    }\n    items\n  }\n}\n\nquery DailyUserActivities($input: DailyUserActivitiesInput!) {\n  dailyUserActivities(input: $input) {\n    groupedBy\n    totalCount\n    totalDuration\n    items {\n      day\n      totalReviewCount\n      workoutCount\n      measurementCount\n      audioBookCount\n      animeCount\n      bookCount\n      podcastCount\n      mangaCount\n      showCount\n      movieCount\n      videoGameCount\n      visualNovelCount\n    }\n  }\n}": types.GetOidcRedirectUrlDocument,
-    "fragment SeenPodcastExtraInformationPart on SeenPodcastExtraInformation {\n  episode\n}\n\nfragment SeenShowExtraInformationPart on SeenShowExtraInformation {\n  episode\n  season\n}\n\nfragment SeenAnimeExtraInformationPart on SeenAnimeExtraInformation {\n  episode\n}\n\nfragment SeenMangaExtraInformationPart on SeenMangaExtraInformation {\n  chapter\n  volume\n}\n\nfragment CalendarEventPart on GraphqlCalendarEvent {\n  date\n  metadataId\n  metadataLot\n  episodeName\n  metadataTitle\n  metadataImage\n  calendarEventId\n  showExtraInformation {\n    ...SeenShowExtraInformationPart\n  }\n  podcastExtraInformation {\n    ...SeenPodcastExtraInformationPart\n  }\n  animeExtraInformation {\n    ...SeenAnimeExtraInformationPart\n  }\n}\n\nfragment SeenPart on Seen {\n  id\n  progress\n  providerWatchedOn\n  state\n  startedOn\n  finishedOn\n  lastUpdatedOn\n  numTimesUpdated\n  showExtraInformation {\n    ...SeenShowExtraInformationPart\n  }\n  podcastExtraInformation {\n    ...SeenPodcastExtraInformationPart\n  }\n  animeExtraInformation {\n    ...SeenAnimeExtraInformationPart\n  }\n  mangaExtraInformation {\n    ...SeenMangaExtraInformationPart\n  }\n}\n\nfragment MetadataSearchItemPart on MetadataSearchItem {\n  identifier\n  title\n  image\n  publishYear\n}\n\nfragment WorkoutOrExerciseTotalsPart on WorkoutOrExerciseTotals {\n  personalBestsAchieved\n  weight\n  reps\n  distance\n  duration\n  restTime\n}\n\nfragment EntityAssetsPart on EntityAssets {\n  images\n  videos\n}\n\nfragment WorkoutSetStatisticPart on WorkoutSetStatistic {\n  duration\n  distance\n  reps\n  weight\n  oneRm\n  pace\n  volume\n}\n\nfragment WorkoutSetRecordPart on WorkoutSetRecord {\n  statistic {\n    ...WorkoutSetStatisticPart\n  }\n  lot\n  personalBests\n}\n\nfragment WorkoutSummaryPart on WorkoutSummary {\n  total {\n    ...WorkoutOrExerciseTotalsPart\n  }\n  exercises {\n    numSets\n    id\n    lot\n    bestSet {\n      ...WorkoutSetRecordPart\n    }\n  }\n}\n\nfragment CollectionPart on Collection {\n  id\n  name\n  userId\n}\n\nfragment ReviewItemPart on ReviewItem {\n  id\n  rating\n  textOriginal\n  textRendered\n  isSpoiler\n  visibility\n  postedOn\n  postedBy {\n    id\n    name\n  }\n  comments {\n    id\n    text\n    createdOn\n    user {\n      id\n      name\n    }\n    likedBy\n  }\n  showExtraInformation {\n    ...SeenShowExtraInformationPart\n  }\n  podcastExtraInformation {\n    ...SeenPodcastExtraInformationPart\n  }\n  animeExtraInformation {\n    ...SeenAnimeExtraInformationPart\n  }\n  mangaExtraInformation {\n    ...SeenMangaExtraInformationPart\n  }\n}\n\nfragment WorkoutInformationPart on WorkoutInformation {\n  comment\n  assets {\n    ...EntityAssetsPart\n  }\n  exercises {\n    name\n    lot\n    notes\n    restTime\n    total {\n      ...WorkoutOrExerciseTotalsPart\n    }\n    supersetWith\n    assets {\n      ...EntityAssetsPart\n    }\n    sets {\n      statistic {\n        ...WorkoutSetStatisticPart\n      }\n      note\n      lot\n      personalBests\n      confirmedAt\n    }\n  }\n}": types.SeenPodcastExtraInformationPartFragmentDoc,
+    "query WorkoutDetails($workoutId: String!) {\n  workoutDetails(workoutId: $workoutId) {\n    collections {\n      ...CollectionPart\n    }\n    details {\n      id\n      name\n      endTime\n      duration\n      startTime\n      templateId\n      repeatedFrom\n      summary {\n        ...WorkoutSummaryPart\n      }\n      information {\n        ...WorkoutInformationPart\n      }\n    }\n  }\n}": types.WorkoutDetailsDocument,
+    "query WorkoutTemplateDetails($workoutTemplateId: String!) {\n  workoutTemplateDetails(workoutTemplateId: $workoutTemplateId) {\n    collections {\n      ...CollectionPart\n    }\n    details {\n      id\n      name\n      createdOn\n      visibility\n      defaultRestTimer\n      summary {\n        ...WorkoutSummaryPart\n      }\n      information {\n        ...WorkoutInformationPart\n      }\n    }\n  }\n}": types.WorkoutTemplateDetailsDocument,
+    "query GetOidcRedirectUrl {\n  getOidcRedirectUrl\n}\n\nquery UserByOidcIssuerId($oidcIssuerId: String!) {\n  userByOidcIssuerId(oidcIssuerId: $oidcIssuerId)\n}\n\nquery GetOidcToken($code: String!) {\n  getOidcToken(code: $code) {\n    subject\n    email\n  }\n}\n\nquery GetPresignedS3Url($key: String!) {\n  getPresignedS3Url(key: $key)\n}\n\nquery ProvidersLanguageInformation {\n  providersLanguageInformation {\n    supported\n    default\n    source\n  }\n}\n\nquery UserExports {\n  userExports {\n    url\n    size\n    endedAt\n    startedAt\n  }\n}\n\nquery UserCollectionsList($name: String) {\n  userCollectionsList(name: $name) {\n    id\n    name\n    count\n    isDefault\n    description\n    creator {\n      id\n      name\n    }\n    collaborators {\n      id\n      name\n    }\n    informationTemplate {\n      lot\n      name\n      required\n      description\n      defaultValue\n    }\n  }\n}\n\nquery UserIntegrations {\n  userIntegrations {\n    id\n    lot\n    provider\n    createdOn\n    isDisabled\n    maximumProgress\n    minimumProgress\n    lastTriggeredOn\n    syncToOwnedCollection\n  }\n}\n\nquery UserNotificationPlatforms {\n  userNotificationPlatforms {\n    id\n    lot\n    createdOn\n    isDisabled\n    description\n  }\n}\n\nquery UsersList($query: String) {\n  usersList(query: $query) {\n    id\n    lot\n    name\n    isDisabled\n  }\n}\n\nquery UserRecommendations {\n  userRecommendations\n}\n\nquery UserUpcomingCalendarEvents($input: UserUpcomingCalendarEventInput!) {\n  userUpcomingCalendarEvents(input: $input) {\n    ...CalendarEventPart\n  }\n}\n\nquery UserCalendarEvents($input: UserCalendarEventInput!) {\n  userCalendarEvents(input: $input) {\n    date\n    events {\n      ...CalendarEventPart\n    }\n  }\n}\n\nquery MetadataPartialDetails($metadataId: String!) {\n  metadataPartialDetails(metadataId: $metadataId) {\n    id\n    lot\n    title\n    image\n    publishYear\n  }\n}\n\nquery MetadataGroupsList($input: MetadataGroupsListInput!) {\n  metadataGroupsList(input: $input) {\n    details {\n      total\n      nextPage\n    }\n    items\n  }\n}\n\nquery PeopleList($input: PeopleListInput!) {\n  peopleList(input: $input) {\n    details {\n      total\n      nextPage\n    }\n    items\n  }\n}\n\nquery UserAccessLinks {\n  userAccessLinks {\n    id\n    name\n    isDemo\n    createdOn\n    expiresOn\n    timesUsed\n    isRevoked\n    maximumUses\n    isAccountDefault\n    isMutationAllowed\n  }\n}\n\nquery DailyUserActivities($input: DailyUserActivitiesInput!) {\n  dailyUserActivities(input: $input) {\n    groupedBy\n    totalCount\n    totalDuration\n    items {\n      day\n      totalReviewCount\n      workoutCount\n      measurementCount\n      audioBookCount\n      animeCount\n      bookCount\n      podcastCount\n      mangaCount\n      showCount\n      movieCount\n      videoGameCount\n      visualNovelCount\n    }\n  }\n}": types.GetOidcRedirectUrlDocument,
+    "fragment SeenPodcastExtraInformationPart on SeenPodcastExtraInformation {\n  episode\n}\n\nfragment SeenShowExtraInformationPart on SeenShowExtraInformation {\n  episode\n  season\n}\n\nfragment SeenAnimeExtraInformationPart on SeenAnimeExtraInformation {\n  episode\n}\n\nfragment SeenMangaExtraInformationPart on SeenMangaExtraInformation {\n  chapter\n  volume\n}\n\nfragment CalendarEventPart on GraphqlCalendarEvent {\n  date\n  metadataId\n  metadataLot\n  episodeName\n  metadataTitle\n  metadataImage\n  calendarEventId\n  showExtraInformation {\n    ...SeenShowExtraInformationPart\n  }\n  podcastExtraInformation {\n    ...SeenPodcastExtraInformationPart\n  }\n  animeExtraInformation {\n    ...SeenAnimeExtraInformationPart\n  }\n}\n\nfragment SeenPart on Seen {\n  id\n  state\n  progress\n  startedOn\n  finishedOn\n  lastUpdatedOn\n  manualTimeSpent\n  numTimesUpdated\n  providerWatchedOn\n  showExtraInformation {\n    ...SeenShowExtraInformationPart\n  }\n  podcastExtraInformation {\n    ...SeenPodcastExtraInformationPart\n  }\n  animeExtraInformation {\n    ...SeenAnimeExtraInformationPart\n  }\n  mangaExtraInformation {\n    ...SeenMangaExtraInformationPart\n  }\n}\n\nfragment MetadataSearchItemPart on MetadataSearchItem {\n  identifier\n  title\n  image\n  publishYear\n}\n\nfragment WorkoutOrExerciseTotalsPart on WorkoutOrExerciseTotals {\n  personalBestsAchieved\n  weight\n  reps\n  distance\n  duration\n  restTime\n}\n\nfragment EntityAssetsPart on EntityAssets {\n  images\n  videos\n}\n\nfragment WorkoutSetStatisticPart on WorkoutSetStatistic {\n  duration\n  distance\n  reps\n  weight\n  oneRm\n  pace\n  volume\n}\n\nfragment WorkoutSetRecordPart on WorkoutSetRecord {\n  statistic {\n    ...WorkoutSetStatisticPart\n  }\n  lot\n  personalBests\n}\n\nfragment WorkoutSummaryPart on WorkoutSummary {\n  total {\n    ...WorkoutOrExerciseTotalsPart\n  }\n  exercises {\n    numSets\n    id\n    lot\n    bestSet {\n      ...WorkoutSetRecordPart\n    }\n  }\n}\n\nfragment CollectionPart on Collection {\n  id\n  name\n  userId\n}\n\nfragment ReviewItemPart on ReviewItem {\n  id\n  rating\n  textOriginal\n  textRendered\n  isSpoiler\n  visibility\n  postedOn\n  postedBy {\n    id\n    name\n  }\n  comments {\n    id\n    text\n    createdOn\n    user {\n      id\n      name\n    }\n    likedBy\n  }\n  showExtraInformation {\n    ...SeenShowExtraInformationPart\n  }\n  podcastExtraInformation {\n    ...SeenPodcastExtraInformationPart\n  }\n  animeExtraInformation {\n    ...SeenAnimeExtraInformationPart\n  }\n  mangaExtraInformation {\n    ...SeenMangaExtraInformationPart\n  }\n}\n\nfragment WorkoutInformationPart on WorkoutInformation {\n  comment\n  assets {\n    ...EntityAssetsPart\n  }\n  exercises {\n    name\n    lot\n    notes\n    restTime\n    total {\n      ...WorkoutOrExerciseTotalsPart\n    }\n    supersetWith\n    assets {\n      ...EntityAssetsPart\n    }\n    sets {\n      statistic {\n        ...WorkoutSetStatisticPart\n      }\n      note\n      lot\n      personalBests\n      confirmedAt\n    }\n  }\n}": types.SeenPodcastExtraInformationPartFragmentDoc,
 };
 
 /**
@@ -60,7 +62,7 @@ export function graphql(source: string): unknown;
 /**
  * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
  */
-export function graphql(source: "mutation RegisterUser($input: RegisterUserInput!) {\n  registerUser(input: $input) {\n    __typename\n    ... on RegisterError {\n      error\n    }\n    ... on StringIdObject {\n      id\n    }\n  }\n}\n\nmutation LoginUser($input: AuthUserInput!) {\n  loginUser(input: $input) {\n    __typename\n    ... on LoginError {\n      error\n    }\n    ... on LoginResponse {\n      apiKey\n    }\n  }\n}\n\nmutation AddEntityToCollection($input: ChangeCollectionToEntityInput!) {\n  addEntityToCollection(input: $input)\n}\n\nmutation CommitMetadata($input: CommitMediaInput!) {\n  commitMetadata(input: $input) {\n    id\n  }\n}\n\nmutation CommitMetadataGroup($input: CommitMediaInput!) {\n  commitMetadataGroup(input: $input) {\n    id\n  }\n}\n\nmutation CommitPerson($input: CommitPersonInput!) {\n  commitPerson(input: $input) {\n    id\n  }\n}\n\nmutation CreateCustomExercise($input: ExerciseInput!) {\n  createCustomExercise(input: $input)\n}\n\nmutation UpdateCustomExercise($input: UpdateCustomExerciseInput!) {\n  updateCustomExercise(input: $input)\n}\n\nmutation UpdateUserIntegration($input: UpdateUserIntegrationInput!) {\n  updateUserIntegration(input: $input)\n}\n\nmutation CreateCustomMetadata($input: CreateCustomMetadataInput!) {\n  createCustomMetadata(input: $input) {\n    id\n  }\n}\n\nmutation CreateOrUpdateCollection($input: CreateOrUpdateCollectionInput!) {\n  createOrUpdateCollection(input: $input) {\n    id\n  }\n}\n\nmutation CreateReviewComment($input: CreateReviewCommentInput!) {\n  createReviewComment(input: $input)\n}\n\nmutation CreateUserMeasurement($input: UserMeasurementInput!) {\n  createUserMeasurement(input: $input)\n}\n\nmutation CreateUserNotificationPlatform($input: CreateUserNotificationPlatformInput!) {\n  createUserNotificationPlatform(input: $input)\n}\n\nmutation CreateUserIntegration($input: CreateUserIntegrationInput!) {\n  createUserIntegration(input: $input) {\n    id\n  }\n}\n\nmutation CreateUserWorkout($input: UserWorkoutInput!) {\n  createUserWorkout(input: $input)\n}\n\nmutation DeleteCollection($collectionName: String!) {\n  deleteCollection(collectionName: $collectionName)\n}\n\nmutation DeleteReview($reviewId: String!) {\n  deleteReview(reviewId: $reviewId)\n}\n\nmutation DeleteS3Object($key: String!) {\n  deleteS3Object(key: $key)\n}\n\nmutation DeleteSeenItem($seenId: String!) {\n  deleteSeenItem(seenId: $seenId) {\n    id\n  }\n}\n\nmutation DeleteUser($toDeleteUserId: String!) {\n  deleteUser(toDeleteUserId: $toDeleteUserId)\n}\n\nmutation DeleteUserIntegration($integrationId: String!) {\n  deleteUserIntegration(integrationId: $integrationId)\n}\n\nmutation DeleteUserMeasurement($timestamp: DateTime!) {\n  deleteUserMeasurement(timestamp: $timestamp)\n}\n\nmutation DeleteUserNotificationPlatform($notificationId: String!) {\n  deleteUserNotificationPlatform(notificationId: $notificationId)\n}\n\nmutation DeleteUserWorkout($workoutId: String!) {\n  deleteUserWorkout(workoutId: $workoutId)\n}\n\nmutation DeployBackgroundJob($jobName: BackgroundJob!) {\n  deployBackgroundJob(jobName: $jobName)\n}\n\nmutation DeployBulkProgressUpdate($input: [ProgressUpdateInput!]!) {\n  deployBulkProgressUpdate(input: $input)\n}\n\nmutation DeployExportJob {\n  deployExportJob\n}\n\nmutation DeployImportJob($input: DeployImportJobInput!) {\n  deployImportJob(input: $input)\n}\n\nmutation DeployUpdateMetadataJob($metadataId: String!) {\n  deployUpdateMetadataJob(metadataId: $metadataId)\n}\n\nmutation DeployUpdatePersonJob($personId: String!) {\n  deployUpdatePersonJob(personId: $personId)\n}\n\nmutation DeployUpdateMetadataGroupJob($metadataGroupId: String!) {\n  deployUpdateMetadataGroupJob(metadataGroupId: $metadataGroupId)\n}\n\nmutation UpdateSeenItem($input: UpdateSeenItemInput!) {\n  updateSeenItem(input: $input)\n}\n\nmutation UpdateUserNotificationPlatform($input: UpdateUserNotificationPlatformInput!) {\n  updateUserNotificationPlatform(input: $input)\n}\n\nmutation UpdateUserWorkout($input: UpdateUserWorkoutInput!) {\n  updateUserWorkout(input: $input)\n}\n\nmutation GenerateAuthToken {\n  generateAuthToken\n}\n\nmutation MergeMetadata($mergeFrom: String!, $mergeInto: String!) {\n  mergeMetadata(mergeFrom: $mergeFrom, mergeInto: $mergeInto)\n}\n\nmutation DisassociateMetadata($metadataId: String!) {\n  disassociateMetadata(metadataId: $metadataId)\n}\n\nmutation PostReview($input: PostReviewInput!) {\n  postReview(input: $input) {\n    id\n  }\n}\n\nmutation PresignedPutS3Url($input: PresignedPutUrlInput!) {\n  presignedPutS3Url(input: $input) {\n    key\n    uploadUrl\n  }\n}\n\nmutation RemoveEntityFromCollection($input: ChangeCollectionToEntityInput!) {\n  removeEntityFromCollection(input: $input) {\n    id\n  }\n}\n\nmutation TestUserNotificationPlatforms {\n  testUserNotificationPlatforms\n}\n\nmutation UpdateUser($input: UpdateUserInput!) {\n  updateUser(input: $input) {\n    id\n  }\n}\n\nmutation UpdateUserPreference($input: UpdateUserPreferenceInput!) {\n  updateUserPreference(input: $input)\n}"): (typeof documents)["mutation RegisterUser($input: RegisterUserInput!) {\n  registerUser(input: $input) {\n    __typename\n    ... on RegisterError {\n      error\n    }\n    ... on StringIdObject {\n      id\n    }\n  }\n}\n\nmutation LoginUser($input: AuthUserInput!) {\n  loginUser(input: $input) {\n    __typename\n    ... on LoginError {\n      error\n    }\n    ... on LoginResponse {\n      apiKey\n    }\n  }\n}\n\nmutation AddEntityToCollection($input: ChangeCollectionToEntityInput!) {\n  addEntityToCollection(input: $input)\n}\n\nmutation CommitMetadata($input: CommitMediaInput!) {\n  commitMetadata(input: $input) {\n    id\n  }\n}\n\nmutation CommitMetadataGroup($input: CommitMediaInput!) {\n  commitMetadataGroup(input: $input) {\n    id\n  }\n}\n\nmutation CommitPerson($input: CommitPersonInput!) {\n  commitPerson(input: $input) {\n    id\n  }\n}\n\nmutation CreateCustomExercise($input: ExerciseInput!) {\n  createCustomExercise(input: $input)\n}\n\nmutation UpdateCustomExercise($input: UpdateCustomExerciseInput!) {\n  updateCustomExercise(input: $input)\n}\n\nmutation UpdateUserIntegration($input: UpdateUserIntegrationInput!) {\n  updateUserIntegration(input: $input)\n}\n\nmutation CreateCustomMetadata($input: CreateCustomMetadataInput!) {\n  createCustomMetadata(input: $input) {\n    id\n  }\n}\n\nmutation CreateOrUpdateCollection($input: CreateOrUpdateCollectionInput!) {\n  createOrUpdateCollection(input: $input) {\n    id\n  }\n}\n\nmutation CreateReviewComment($input: CreateReviewCommentInput!) {\n  createReviewComment(input: $input)\n}\n\nmutation CreateUserMeasurement($input: UserMeasurementInput!) {\n  createUserMeasurement(input: $input)\n}\n\nmutation CreateUserNotificationPlatform($input: CreateUserNotificationPlatformInput!) {\n  createUserNotificationPlatform(input: $input)\n}\n\nmutation CreateUserIntegration($input: CreateUserIntegrationInput!) {\n  createUserIntegration(input: $input) {\n    id\n  }\n}\n\nmutation CreateUserWorkout($input: UserWorkoutInput!) {\n  createUserWorkout(input: $input)\n}\n\nmutation DeleteCollection($collectionName: String!) {\n  deleteCollection(collectionName: $collectionName)\n}\n\nmutation DeleteReview($reviewId: String!) {\n  deleteReview(reviewId: $reviewId)\n}\n\nmutation DeleteS3Object($key: String!) {\n  deleteS3Object(key: $key)\n}\n\nmutation DeleteSeenItem($seenId: String!) {\n  deleteSeenItem(seenId: $seenId) {\n    id\n  }\n}\n\nmutation DeleteUser($toDeleteUserId: String!) {\n  deleteUser(toDeleteUserId: $toDeleteUserId)\n}\n\nmutation DeleteUserIntegration($integrationId: String!) {\n  deleteUserIntegration(integrationId: $integrationId)\n}\n\nmutation DeleteUserMeasurement($timestamp: DateTime!) {\n  deleteUserMeasurement(timestamp: $timestamp)\n}\n\nmutation DeleteUserNotificationPlatform($notificationId: String!) {\n  deleteUserNotificationPlatform(notificationId: $notificationId)\n}\n\nmutation DeleteUserWorkout($workoutId: String!) {\n  deleteUserWorkout(workoutId: $workoutId)\n}\n\nmutation DeployBackgroundJob($jobName: BackgroundJob!) {\n  deployBackgroundJob(jobName: $jobName)\n}\n\nmutation DeployBulkProgressUpdate($input: [ProgressUpdateInput!]!) {\n  deployBulkProgressUpdate(input: $input)\n}\n\nmutation DeployExportJob {\n  deployExportJob\n}\n\nmutation DeployImportJob($input: DeployImportJobInput!) {\n  deployImportJob(input: $input)\n}\n\nmutation DeployUpdateMetadataJob($metadataId: String!) {\n  deployUpdateMetadataJob(metadataId: $metadataId)\n}\n\nmutation DeployUpdatePersonJob($personId: String!) {\n  deployUpdatePersonJob(personId: $personId)\n}\n\nmutation DeployUpdateMetadataGroupJob($metadataGroupId: String!) {\n  deployUpdateMetadataGroupJob(metadataGroupId: $metadataGroupId)\n}\n\nmutation UpdateSeenItem($input: UpdateSeenItemInput!) {\n  updateSeenItem(input: $input)\n}\n\nmutation UpdateUserNotificationPlatform($input: UpdateUserNotificationPlatformInput!) {\n  updateUserNotificationPlatform(input: $input)\n}\n\nmutation UpdateUserWorkout($input: UpdateUserWorkoutInput!) {\n  updateUserWorkout(input: $input)\n}\n\nmutation GenerateAuthToken {\n  generateAuthToken\n}\n\nmutation MergeMetadata($mergeFrom: String!, $mergeInto: String!) {\n  mergeMetadata(mergeFrom: $mergeFrom, mergeInto: $mergeInto)\n}\n\nmutation DisassociateMetadata($metadataId: String!) {\n  disassociateMetadata(metadataId: $metadataId)\n}\n\nmutation PostReview($input: PostReviewInput!) {\n  postReview(input: $input) {\n    id\n  }\n}\n\nmutation PresignedPutS3Url($input: PresignedPutUrlInput!) {\n  presignedPutS3Url(input: $input) {\n    key\n    uploadUrl\n  }\n}\n\nmutation RemoveEntityFromCollection($input: ChangeCollectionToEntityInput!) {\n  removeEntityFromCollection(input: $input) {\n    id\n  }\n}\n\nmutation TestUserNotificationPlatforms {\n  testUserNotificationPlatforms\n}\n\nmutation UpdateUser($input: UpdateUserInput!) {\n  updateUser(input: $input) {\n    id\n  }\n}\n\nmutation UpdateUserPreference($input: UpdateUserPreferenceInput!) {\n  updateUserPreference(input: $input)\n}"];
+export function graphql(source: "mutation RegisterUser($input: RegisterUserInput!) {\n  registerUser(input: $input) {\n    __typename\n    ... on RegisterError {\n      error\n    }\n    ... on StringIdObject {\n      id\n    }\n  }\n}\n\nmutation LoginUser($input: AuthUserInput!) {\n  loginUser(input: $input) {\n    __typename\n    ... on LoginError {\n      error\n    }\n    ... on LoginResponse {\n      apiKey\n    }\n  }\n}\n\nmutation AddEntityToCollection($input: ChangeCollectionToEntityInput!) {\n  addEntityToCollection(input: $input)\n}\n\nmutation CommitMetadata($input: CommitMediaInput!) {\n  commitMetadata(input: $input) {\n    id\n  }\n}\n\nmutation CommitMetadataGroup($input: CommitMediaInput!) {\n  commitMetadataGroup(input: $input) {\n    id\n  }\n}\n\nmutation CommitPerson($input: CommitPersonInput!) {\n  commitPerson(input: $input) {\n    id\n  }\n}\n\nmutation CreateCustomExercise($input: ExerciseInput!) {\n  createCustomExercise(input: $input)\n}\n\nmutation UpdateCustomExercise($input: UpdateCustomExerciseInput!) {\n  updateCustomExercise(input: $input)\n}\n\nmutation UpdateUserIntegration($input: UpdateUserIntegrationInput!) {\n  updateUserIntegration(input: $input)\n}\n\nmutation CreateCustomMetadata($input: CreateCustomMetadataInput!) {\n  createCustomMetadata(input: $input) {\n    id\n  }\n}\n\nmutation CreateOrUpdateCollection($input: CreateOrUpdateCollectionInput!) {\n  createOrUpdateCollection(input: $input) {\n    id\n  }\n}\n\nmutation CreateReviewComment($input: CreateReviewCommentInput!) {\n  createReviewComment(input: $input)\n}\n\nmutation CreateUserMeasurement($input: UserMeasurementInput!) {\n  createUserMeasurement(input: $input)\n}\n\nmutation CreateUserNotificationPlatform($input: CreateUserNotificationPlatformInput!) {\n  createUserNotificationPlatform(input: $input)\n}\n\nmutation CreateUserIntegration($input: CreateUserIntegrationInput!) {\n  createUserIntegration(input: $input) {\n    id\n  }\n}\n\nmutation CreateUserWorkout($input: UserWorkoutInput!) {\n  createUserWorkout(input: $input)\n}\n\nmutation CreateOrUpdateWorkoutTemplate($input: UserWorkoutInput!) {\n  createOrUpdateWorkoutTemplate(input: $input)\n}\n\nmutation DeleteCollection($collectionName: String!) {\n  deleteCollection(collectionName: $collectionName)\n}\n\nmutation DeleteReview($reviewId: String!) {\n  deleteReview(reviewId: $reviewId)\n}\n\nmutation DeleteS3Object($key: String!) {\n  deleteS3Object(key: $key)\n}\n\nmutation DeleteSeenItem($seenId: String!) {\n  deleteSeenItem(seenId: $seenId) {\n    id\n  }\n}\n\nmutation DeleteUser($toDeleteUserId: String!) {\n  deleteUser(toDeleteUserId: $toDeleteUserId)\n}\n\nmutation DeleteUserIntegration($integrationId: String!) {\n  deleteUserIntegration(integrationId: $integrationId)\n}\n\nmutation DeleteUserMeasurement($timestamp: DateTime!) {\n  deleteUserMeasurement(timestamp: $timestamp)\n}\n\nmutation DeleteUserNotificationPlatform($notificationId: String!) {\n  deleteUserNotificationPlatform(notificationId: $notificationId)\n}\n\nmutation DeleteUserWorkout($workoutId: String!) {\n  deleteUserWorkout(workoutId: $workoutId)\n}\n\nmutation DeleteWorkoutTemplate($workoutTemplateId: String!) {\n  deleteWorkoutTemplate(workoutTemplateId: $workoutTemplateId)\n}\n\nmutation DeployBackgroundJob($jobName: BackgroundJob!) {\n  deployBackgroundJob(jobName: $jobName)\n}\n\nmutation DeployBulkProgressUpdate($input: [ProgressUpdateInput!]!) {\n  deployBulkProgressUpdate(input: $input)\n}\n\nmutation DeployExportJob {\n  deployExportJob\n}\n\nmutation DeployImportJob($input: DeployImportJobInput!) {\n  deployImportJob(input: $input)\n}\n\nmutation DeployUpdateMetadataJob($metadataId: String!) {\n  deployUpdateMetadataJob(metadataId: $metadataId)\n}\n\nmutation DeployUpdatePersonJob($personId: String!) {\n  deployUpdatePersonJob(personId: $personId)\n}\n\nmutation DeployUpdateMetadataGroupJob($metadataGroupId: String!) {\n  deployUpdateMetadataGroupJob(metadataGroupId: $metadataGroupId)\n}\n\nmutation UpdateSeenItem($input: UpdateSeenItemInput!) {\n  updateSeenItem(input: $input)\n}\n\nmutation UpdateUserNotificationPlatform($input: UpdateUserNotificationPlatformInput!) {\n  updateUserNotificationPlatform(input: $input)\n}\n\nmutation UpdateUserWorkout($input: UpdateUserWorkoutInput!) {\n  updateUserWorkout(input: $input)\n}\n\nmutation GenerateAuthToken {\n  generateAuthToken\n}\n\nmutation MergeMetadata($mergeFrom: String!, $mergeInto: String!) {\n  mergeMetadata(mergeFrom: $mergeFrom, mergeInto: $mergeInto)\n}\n\nmutation DisassociateMetadata($metadataId: String!) {\n  disassociateMetadata(metadataId: $metadataId)\n}\n\nmutation PostReview($input: PostReviewInput!) {\n  postReview(input: $input) {\n    id\n  }\n}\n\nmutation PresignedPutS3Url($input: PresignedPutUrlInput!) {\n  presignedPutS3Url(input: $input) {\n    key\n    uploadUrl\n  }\n}\n\nmutation RemoveEntityFromCollection($input: ChangeCollectionToEntityInput!) {\n  removeEntityFromCollection(input: $input) {\n    id\n  }\n}\n\nmutation TestUserNotificationPlatforms {\n  testUserNotificationPlatforms\n}\n\nmutation UpdateUser($input: UpdateUserInput!) {\n  updateUser(input: $input) {\n    id\n  }\n}\n\nmutation UpdateUserPreference($input: UpdateUserPreferenceInput!) {\n  updateUserPreference(input: $input)\n}\n\nmutation CreateAccessLink($input: CreateAccessLinkInput!) {\n  createAccessLink(input: $input) {\n    id\n  }\n}\n\nmutation ProcessAccessLink($input: ProcessAccessLinkInput!) {\n  processAccessLink(input: $input) {\n    __typename\n    ... on ProcessAccessLinkError {\n      error\n    }\n    ... on ProcessAccessLinkResponse {\n      apiKey\n      redirectTo\n      tokenValidForDays\n    }\n  }\n}\n\nmutation revokeAccessLink($accessLinkId: String!) {\n  revokeAccessLink(accessLinkId: $accessLinkId)\n}"): (typeof documents)["mutation RegisterUser($input: RegisterUserInput!) {\n  registerUser(input: $input) {\n    __typename\n    ... on RegisterError {\n      error\n    }\n    ... on StringIdObject {\n      id\n    }\n  }\n}\n\nmutation LoginUser($input: AuthUserInput!) {\n  loginUser(input: $input) {\n    __typename\n    ... on LoginError {\n      error\n    }\n    ... on LoginResponse {\n      apiKey\n    }\n  }\n}\n\nmutation AddEntityToCollection($input: ChangeCollectionToEntityInput!) {\n  addEntityToCollection(input: $input)\n}\n\nmutation CommitMetadata($input: CommitMediaInput!) {\n  commitMetadata(input: $input) {\n    id\n  }\n}\n\nmutation CommitMetadataGroup($input: CommitMediaInput!) {\n  commitMetadataGroup(input: $input) {\n    id\n  }\n}\n\nmutation CommitPerson($input: CommitPersonInput!) {\n  commitPerson(input: $input) {\n    id\n  }\n}\n\nmutation CreateCustomExercise($input: ExerciseInput!) {\n  createCustomExercise(input: $input)\n}\n\nmutation UpdateCustomExercise($input: UpdateCustomExerciseInput!) {\n  updateCustomExercise(input: $input)\n}\n\nmutation UpdateUserIntegration($input: UpdateUserIntegrationInput!) {\n  updateUserIntegration(input: $input)\n}\n\nmutation CreateCustomMetadata($input: CreateCustomMetadataInput!) {\n  createCustomMetadata(input: $input) {\n    id\n  }\n}\n\nmutation CreateOrUpdateCollection($input: CreateOrUpdateCollectionInput!) {\n  createOrUpdateCollection(input: $input) {\n    id\n  }\n}\n\nmutation CreateReviewComment($input: CreateReviewCommentInput!) {\n  createReviewComment(input: $input)\n}\n\nmutation CreateUserMeasurement($input: UserMeasurementInput!) {\n  createUserMeasurement(input: $input)\n}\n\nmutation CreateUserNotificationPlatform($input: CreateUserNotificationPlatformInput!) {\n  createUserNotificationPlatform(input: $input)\n}\n\nmutation CreateUserIntegration($input: CreateUserIntegrationInput!) {\n  createUserIntegration(input: $input) {\n    id\n  }\n}\n\nmutation CreateUserWorkout($input: UserWorkoutInput!) {\n  createUserWorkout(input: $input)\n}\n\nmutation CreateOrUpdateWorkoutTemplate($input: UserWorkoutInput!) {\n  createOrUpdateWorkoutTemplate(input: $input)\n}\n\nmutation DeleteCollection($collectionName: String!) {\n  deleteCollection(collectionName: $collectionName)\n}\n\nmutation DeleteReview($reviewId: String!) {\n  deleteReview(reviewId: $reviewId)\n}\n\nmutation DeleteS3Object($key: String!) {\n  deleteS3Object(key: $key)\n}\n\nmutation DeleteSeenItem($seenId: String!) {\n  deleteSeenItem(seenId: $seenId) {\n    id\n  }\n}\n\nmutation DeleteUser($toDeleteUserId: String!) {\n  deleteUser(toDeleteUserId: $toDeleteUserId)\n}\n\nmutation DeleteUserIntegration($integrationId: String!) {\n  deleteUserIntegration(integrationId: $integrationId)\n}\n\nmutation DeleteUserMeasurement($timestamp: DateTime!) {\n  deleteUserMeasurement(timestamp: $timestamp)\n}\n\nmutation DeleteUserNotificationPlatform($notificationId: String!) {\n  deleteUserNotificationPlatform(notificationId: $notificationId)\n}\n\nmutation DeleteUserWorkout($workoutId: String!) {\n  deleteUserWorkout(workoutId: $workoutId)\n}\n\nmutation DeleteWorkoutTemplate($workoutTemplateId: String!) {\n  deleteWorkoutTemplate(workoutTemplateId: $workoutTemplateId)\n}\n\nmutation DeployBackgroundJob($jobName: BackgroundJob!) {\n  deployBackgroundJob(jobName: $jobName)\n}\n\nmutation DeployBulkProgressUpdate($input: [ProgressUpdateInput!]!) {\n  deployBulkProgressUpdate(input: $input)\n}\n\nmutation DeployExportJob {\n  deployExportJob\n}\n\nmutation DeployImportJob($input: DeployImportJobInput!) {\n  deployImportJob(input: $input)\n}\n\nmutation DeployUpdateMetadataJob($metadataId: String!) {\n  deployUpdateMetadataJob(metadataId: $metadataId)\n}\n\nmutation DeployUpdatePersonJob($personId: String!) {\n  deployUpdatePersonJob(personId: $personId)\n}\n\nmutation DeployUpdateMetadataGroupJob($metadataGroupId: String!) {\n  deployUpdateMetadataGroupJob(metadataGroupId: $metadataGroupId)\n}\n\nmutation UpdateSeenItem($input: UpdateSeenItemInput!) {\n  updateSeenItem(input: $input)\n}\n\nmutation UpdateUserNotificationPlatform($input: UpdateUserNotificationPlatformInput!) {\n  updateUserNotificationPlatform(input: $input)\n}\n\nmutation UpdateUserWorkout($input: UpdateUserWorkoutInput!) {\n  updateUserWorkout(input: $input)\n}\n\nmutation GenerateAuthToken {\n  generateAuthToken\n}\n\nmutation MergeMetadata($mergeFrom: String!, $mergeInto: String!) {\n  mergeMetadata(mergeFrom: $mergeFrom, mergeInto: $mergeInto)\n}\n\nmutation DisassociateMetadata($metadataId: String!) {\n  disassociateMetadata(metadataId: $metadataId)\n}\n\nmutation PostReview($input: PostReviewInput!) {\n  postReview(input: $input) {\n    id\n  }\n}\n\nmutation PresignedPutS3Url($input: PresignedPutUrlInput!) {\n  presignedPutS3Url(input: $input) {\n    key\n    uploadUrl\n  }\n}\n\nmutation RemoveEntityFromCollection($input: ChangeCollectionToEntityInput!) {\n  removeEntityFromCollection(input: $input) {\n    id\n  }\n}\n\nmutation TestUserNotificationPlatforms {\n  testUserNotificationPlatforms\n}\n\nmutation UpdateUser($input: UpdateUserInput!) {\n  updateUser(input: $input) {\n    id\n  }\n}\n\nmutation UpdateUserPreference($input: UpdateUserPreferenceInput!) {\n  updateUserPreference(input: $input)\n}\n\nmutation CreateAccessLink($input: CreateAccessLinkInput!) {\n  createAccessLink(input: $input) {\n    id\n  }\n}\n\nmutation ProcessAccessLink($input: ProcessAccessLinkInput!) {\n  processAccessLink(input: $input) {\n    __typename\n    ... on ProcessAccessLinkError {\n      error\n    }\n    ... on ProcessAccessLinkResponse {\n      apiKey\n      redirectTo\n      tokenValidForDays\n    }\n  }\n}\n\nmutation revokeAccessLink($accessLinkId: String!) {\n  revokeAccessLink(accessLinkId: $accessLinkId)\n}"];
 /**
  * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
  */
@@ -152,7 +154,11 @@ export function graphql(source: "query UserPersonDetails($personId: String!) {\n
 /**
  * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
  */
-export function graphql(source: "query UserPreferences {\n  userPreferences {\n    general {\n      reviewScale\n      displayNsfw\n      disableVideos\n      persistQueries\n      watchProviders\n      disableReviews\n      disableIntegrations\n      disableWatchProviders\n      disableNavigationAnimation\n      dashboard {\n        section\n        hidden\n        numElements\n      }\n    }\n    fitness {\n      measurements {\n        custom {\n          name\n          dataType\n        }\n        inbuilt {\n          weight\n          bodyMassIndex\n          totalBodyWater\n          muscle\n          leanBodyMass\n          bodyFat\n          boneMass\n          visceralFat\n          waistCircumference\n          waistToHeightRatio\n          hipCircumference\n          waistToHipRatio\n          chestCircumference\n          thighCircumference\n          bicepsCircumference\n          neckCircumference\n          bodyFatCaliper\n          chestSkinfold\n          abdominalSkinfold\n          thighSkinfold\n          basalMetabolicRate\n          totalDailyEnergyExpenditure\n          calories\n        }\n      }\n      exercises {\n        unitSystem\n      }\n    }\n    notifications {\n      toSend\n      enabled\n    }\n    featuresEnabled {\n      others {\n        calendar\n        collections\n      }\n      fitness {\n        enabled\n        workouts\n        measurements\n      }\n      media {\n        enabled\n        anime\n        audioBook\n        book\n        manga\n        movie\n        podcast\n        show\n        videoGame\n        visualNovel\n        people\n        groups\n        genres\n      }\n    }\n  }\n}"): (typeof documents)["query UserPreferences {\n  userPreferences {\n    general {\n      reviewScale\n      displayNsfw\n      disableVideos\n      persistQueries\n      watchProviders\n      disableReviews\n      disableIntegrations\n      disableWatchProviders\n      disableNavigationAnimation\n      dashboard {\n        section\n        hidden\n        numElements\n      }\n    }\n    fitness {\n      measurements {\n        custom {\n          name\n          dataType\n        }\n        inbuilt {\n          weight\n          bodyMassIndex\n          totalBodyWater\n          muscle\n          leanBodyMass\n          bodyFat\n          boneMass\n          visceralFat\n          waistCircumference\n          waistToHeightRatio\n          hipCircumference\n          waistToHipRatio\n          chestCircumference\n          thighCircumference\n          bicepsCircumference\n          neckCircumference\n          bodyFatCaliper\n          chestSkinfold\n          abdominalSkinfold\n          thighSkinfold\n          basalMetabolicRate\n          totalDailyEnergyExpenditure\n          calories\n        }\n      }\n      exercises {\n        unitSystem\n      }\n    }\n    notifications {\n      toSend\n      enabled\n    }\n    featuresEnabled {\n      others {\n        calendar\n        collections\n      }\n      fitness {\n        enabled\n        workouts\n        measurements\n      }\n      media {\n        enabled\n        anime\n        audioBook\n        book\n        manga\n        movie\n        podcast\n        show\n        videoGame\n        visualNovel\n        people\n        groups\n        genres\n      }\n    }\n  }\n}"];
+export function graphql(source: "query UserPreferences {\n  userPreferences {\n    general {\n      reviewScale\n      displayNsfw\n      disableVideos\n      persistQueries\n      watchProviders\n      disableReviews\n      disableIntegrations\n      disableWatchProviders\n      disableNavigationAnimation\n      dashboard {\n        section\n        hidden\n        numElements\n      }\n    }\n    fitness {\n      measurements {\n        custom {\n          name\n          dataType\n        }\n        inbuilt {\n          weight\n          bodyMassIndex\n          totalBodyWater\n          muscle\n          leanBodyMass\n          bodyFat\n          boneMass\n          visceralFat\n          waistCircumference\n          waistToHeightRatio\n          hipCircumference\n          waistToHipRatio\n          chestCircumference\n          thighCircumference\n          bicepsCircumference\n          neckCircumference\n          bodyFatCaliper\n          chestSkinfold\n          abdominalSkinfold\n          thighSkinfold\n          basalMetabolicRate\n          totalDailyEnergyExpenditure\n          calories\n        }\n      }\n      exercises {\n        unitSystem\n      }\n    }\n    notifications {\n      toSend\n      enabled\n    }\n    featuresEnabled {\n      others {\n        calendar\n        collections\n      }\n      fitness {\n        enabled\n        workouts\n        templates\n        measurements\n      }\n      media {\n        enabled\n        anime\n        audioBook\n        book\n        manga\n        movie\n        podcast\n        show\n        videoGame\n        visualNovel\n        people\n        groups\n        genres\n      }\n    }\n  }\n}"): (typeof documents)["query UserPreferences {\n  userPreferences {\n    general {\n      reviewScale\n      displayNsfw\n      disableVideos\n      persistQueries\n      watchProviders\n      disableReviews\n      disableIntegrations\n      disableWatchProviders\n      disableNavigationAnimation\n      dashboard {\n        section\n        hidden\n        numElements\n      }\n    }\n    fitness {\n      measurements {\n        custom {\n          name\n          dataType\n        }\n        inbuilt {\n          weight\n          bodyMassIndex\n          totalBodyWater\n          muscle\n          leanBodyMass\n          bodyFat\n          boneMass\n          visceralFat\n          waistCircumference\n          waistToHeightRatio\n          hipCircumference\n          waistToHipRatio\n          chestCircumference\n          thighCircumference\n          bicepsCircumference\n          neckCircumference\n          bodyFatCaliper\n          chestSkinfold\n          abdominalSkinfold\n          thighSkinfold\n          basalMetabolicRate\n          totalDailyEnergyExpenditure\n          calories\n        }\n      }\n      exercises {\n        unitSystem\n      }\n    }\n    notifications {\n      toSend\n      enabled\n    }\n    featuresEnabled {\n      others {\n        calendar\n        collections\n      }\n      fitness {\n        enabled\n        workouts\n        templates\n        measurements\n      }\n      media {\n        enabled\n        anime\n        audioBook\n        book\n        manga\n        movie\n        podcast\n        show\n        videoGame\n        visualNovel\n        people\n        groups\n        genres\n      }\n    }\n  }\n}"];
+/**
+ * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
+ */
+export function graphql(source: "query UserWorkoutTemplatesList($input: SearchInput!) {\n  userWorkoutTemplatesList(input: $input) {\n    details {\n      total\n      nextPage\n    }\n    items {\n      id\n      name\n      createdOn\n      visibility\n      summary {\n        ...WorkoutSummaryPart\n      }\n    }\n  }\n}"): (typeof documents)["query UserWorkoutTemplatesList($input: SearchInput!) {\n  userWorkoutTemplatesList(input: $input) {\n    details {\n      total\n      nextPage\n    }\n    items {\n      id\n      name\n      createdOn\n      visibility\n      summary {\n        ...WorkoutSummaryPart\n      }\n    }\n  }\n}"];
 /**
  * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
  */
@@ -160,15 +166,19 @@ export function graphql(source: "query UserWorkoutsList($input: SearchInput!) {\
 /**
  * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
  */
-export function graphql(source: "query WorkoutDetails($workoutId: String!) {\n  workoutDetails(workoutId: $workoutId) {\n    collections {\n      ...CollectionPart\n    }\n    details {\n      id\n      name\n      endTime\n      duration\n      startTime\n      repeatedFrom\n      summary {\n        ...WorkoutSummaryPart\n      }\n      information {\n        ...WorkoutInformationPart\n      }\n    }\n  }\n}"): (typeof documents)["query WorkoutDetails($workoutId: String!) {\n  workoutDetails(workoutId: $workoutId) {\n    collections {\n      ...CollectionPart\n    }\n    details {\n      id\n      name\n      endTime\n      duration\n      startTime\n      repeatedFrom\n      summary {\n        ...WorkoutSummaryPart\n      }\n      information {\n        ...WorkoutInformationPart\n      }\n    }\n  }\n}"];
+export function graphql(source: "query WorkoutDetails($workoutId: String!) {\n  workoutDetails(workoutId: $workoutId) {\n    collections {\n      ...CollectionPart\n    }\n    details {\n      id\n      name\n      endTime\n      duration\n      startTime\n      templateId\n      repeatedFrom\n      summary {\n        ...WorkoutSummaryPart\n      }\n      information {\n        ...WorkoutInformationPart\n      }\n    }\n  }\n}"): (typeof documents)["query WorkoutDetails($workoutId: String!) {\n  workoutDetails(workoutId: $workoutId) {\n    collections {\n      ...CollectionPart\n    }\n    details {\n      id\n      name\n      endTime\n      duration\n      startTime\n      templateId\n      repeatedFrom\n      summary {\n        ...WorkoutSummaryPart\n      }\n      information {\n        ...WorkoutInformationPart\n      }\n    }\n  }\n}"];
+/**
+ * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
+ */
+export function graphql(source: "query WorkoutTemplateDetails($workoutTemplateId: String!) {\n  workoutTemplateDetails(workoutTemplateId: $workoutTemplateId) {\n    collections {\n      ...CollectionPart\n    }\n    details {\n      id\n      name\n      createdOn\n      visibility\n      defaultRestTimer\n      summary {\n        ...WorkoutSummaryPart\n      }\n      information {\n        ...WorkoutInformationPart\n      }\n    }\n  }\n}"): (typeof documents)["query WorkoutTemplateDetails($workoutTemplateId: String!) {\n  workoutTemplateDetails(workoutTemplateId: $workoutTemplateId) {\n    collections {\n      ...CollectionPart\n    }\n    details {\n      id\n      name\n      createdOn\n      visibility\n      defaultRestTimer\n      summary {\n        ...WorkoutSummaryPart\n      }\n      information {\n        ...WorkoutInformationPart\n      }\n    }\n  }\n}"];
 /**
  * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
  */
-export function graphql(source: "query GetOidcRedirectUrl {\n  getOidcRedirectUrl\n}\n\nquery UserByOidcIssuerId($oidcIssuerId: String!) {\n  userByOidcIssuerId(oidcIssuerId: $oidcIssuerId)\n}\n\nquery GetOidcToken($code: String!) {\n  getOidcToken(code: $code) {\n    subject\n    email\n  }\n}\n\nquery GetPresignedS3Url($key: String!) {\n  getPresignedS3Url(key: $key)\n}\n\nquery ProvidersLanguageInformation {\n  providersLanguageInformation {\n    supported\n    default\n    source\n  }\n}\n\nquery UserExports {\n  userExports {\n    url\n    size\n    endedAt\n    startedAt\n  }\n}\n\nquery UserCollectionsList($name: String) {\n  userCollectionsList(name: $name) {\n    id\n    name\n    count\n    isDefault\n    description\n    creator {\n      id\n      name\n    }\n    collaborators {\n      id\n      name\n    }\n    informationTemplate {\n      lot\n      name\n      required\n      description\n      defaultValue\n    }\n  }\n}\n\nquery UserIntegrations {\n  userIntegrations {\n    id\n    lot\n    provider\n    createdOn\n    isDisabled\n    maximumProgress\n    minimumProgress\n    lastTriggeredOn\n  }\n}\n\nquery UserNotificationPlatforms {\n  userNotificationPlatforms {\n    id\n    lot\n    createdOn\n    isDisabled\n    description\n  }\n}\n\nquery UsersList($query: String) {\n  usersList(query: $query) {\n    id\n    lot\n    name\n    isDisabled\n  }\n}\n\nquery UserUpcomingCalendarEvents($input: UserUpcomingCalendarEventInput!) {\n  userUpcomingCalendarEvents(input: $input) {\n    ...CalendarEventPart\n  }\n}\n\nquery UserCalendarEvents($input: UserCalendarEventInput!) {\n  userCalendarEvents(input: $input) {\n    date\n    events {\n      ...CalendarEventPart\n    }\n  }\n}\n\nquery MetadataPartialDetails($metadataId: String!) {\n  metadataPartialDetails(metadataId: $metadataId) {\n    id\n    lot\n    title\n    image\n    publishYear\n  }\n}\n\nquery MetadataGroupsList($input: MetadataGroupsListInput!) {\n  metadataGroupsList(input: $input) {\n    details {\n      total\n      nextPage\n    }\n    items\n  }\n}\n\nquery PeopleList($input: PeopleListInput!) {\n  peopleList(input: $input) {\n    details {\n      total\n      nextPage\n    }\n    items\n  }\n}\n\nquery DailyUserActivities($input: DailyUserActivitiesInput!) {\n  dailyUserActivities(input: $input) {\n    groupedBy\n    totalCount\n    totalDuration\n    items {\n      day\n      totalReviewCount\n      workoutCount\n      measurementCount\n      audioBookCount\n      animeCount\n      bookCount\n      podcastCount\n      mangaCount\n      showCount\n      movieCount\n      videoGameCount\n      visualNovelCount\n    }\n  }\n}"): (typeof documents)["query GetOidcRedirectUrl {\n  getOidcRedirectUrl\n}\n\nquery UserByOidcIssuerId($oidcIssuerId: String!) {\n  userByOidcIssuerId(oidcIssuerId: $oidcIssuerId)\n}\n\nquery GetOidcToken($code: String!) {\n  getOidcToken(code: $code) {\n    subject\n    email\n  }\n}\n\nquery GetPresignedS3Url($key: String!) {\n  getPresignedS3Url(key: $key)\n}\n\nquery ProvidersLanguageInformation {\n  providersLanguageInformation {\n    supported\n    default\n    source\n  }\n}\n\nquery UserExports {\n  userExports {\n    url\n    size\n    endedAt\n    startedAt\n  }\n}\n\nquery UserCollectionsList($name: String) {\n  userCollectionsList(name: $name) {\n    id\n    name\n    count\n    isDefault\n    description\n    creator {\n      id\n      name\n    }\n    collaborators {\n      id\n      name\n    }\n    informationTemplate {\n      lot\n      name\n      required\n      description\n      defaultValue\n    }\n  }\n}\n\nquery UserIntegrations {\n  userIntegrations {\n    id\n    lot\n    provider\n    createdOn\n    isDisabled\n    maximumProgress\n    minimumProgress\n    lastTriggeredOn\n  }\n}\n\nquery UserNotificationPlatforms {\n  userNotificationPlatforms {\n    id\n    lot\n    createdOn\n    isDisabled\n    description\n  }\n}\n\nquery UsersList($query: String) {\n  usersList(query: $query) {\n    id\n    lot\n    name\n    isDisabled\n  }\n}\n\nquery UserUpcomingCalendarEvents($input: UserUpcomingCalendarEventInput!) {\n  userUpcomingCalendarEvents(input: $input) {\n    ...CalendarEventPart\n  }\n}\n\nquery UserCalendarEvents($input: UserCalendarEventInput!) {\n  userCalendarEvents(input: $input) {\n    date\n    events {\n      ...CalendarEventPart\n    }\n  }\n}\n\nquery MetadataPartialDetails($metadataId: String!) {\n  metadataPartialDetails(metadataId: $metadataId) {\n    id\n    lot\n    title\n    image\n    publishYear\n  }\n}\n\nquery MetadataGroupsList($input: MetadataGroupsListInput!) {\n  metadataGroupsList(input: $input) {\n    details {\n      total\n      nextPage\n    }\n    items\n  }\n}\n\nquery PeopleList($input: PeopleListInput!) {\n  peopleList(input: $input) {\n    details {\n      total\n      nextPage\n    }\n    items\n  }\n}\n\nquery DailyUserActivities($input: DailyUserActivitiesInput!) {\n  dailyUserActivities(input: $input) {\n    groupedBy\n    totalCount\n    totalDuration\n    items {\n      day\n      totalReviewCount\n      workoutCount\n      measurementCount\n      audioBookCount\n      animeCount\n      bookCount\n      podcastCount\n      mangaCount\n      showCount\n      movieCount\n      videoGameCount\n      visualNovelCount\n    }\n  }\n}"];
+export function graphql(source: "query GetOidcRedirectUrl {\n  getOidcRedirectUrl\n}\n\nquery UserByOidcIssuerId($oidcIssuerId: String!) {\n  userByOidcIssuerId(oidcIssuerId: $oidcIssuerId)\n}\n\nquery GetOidcToken($code: String!) {\n  getOidcToken(code: $code) {\n    subject\n    email\n  }\n}\n\nquery GetPresignedS3Url($key: String!) {\n  getPresignedS3Url(key: $key)\n}\n\nquery ProvidersLanguageInformation {\n  providersLanguageInformation {\n    supported\n    default\n    source\n  }\n}\n\nquery UserExports {\n  userExports {\n    url\n    size\n    endedAt\n    startedAt\n  }\n}\n\nquery UserCollectionsList($name: String) {\n  userCollectionsList(name: $name) {\n    id\n    name\n    count\n    isDefault\n    description\n    creator {\n      id\n      name\n    }\n    collaborators {\n      id\n      name\n    }\n    informationTemplate {\n      lot\n      name\n      required\n      description\n      defaultValue\n    }\n  }\n}\n\nquery UserIntegrations {\n  userIntegrations {\n    id\n    lot\n    provider\n    createdOn\n    isDisabled\n    maximumProgress\n    minimumProgress\n    lastTriggeredOn\n    syncToOwnedCollection\n  }\n}\n\nquery UserNotificationPlatforms {\n  userNotificationPlatforms {\n    id\n    lot\n    createdOn\n    isDisabled\n    description\n  }\n}\n\nquery UsersList($query: String) {\n  usersList(query: $query) {\n    id\n    lot\n    name\n    isDisabled\n  }\n}\n\nquery UserRecommendations {\n  userRecommendations\n}\n\nquery UserUpcomingCalendarEvents($input: UserUpcomingCalendarEventInput!) {\n  userUpcomingCalendarEvents(input: $input) {\n    ...CalendarEventPart\n  }\n}\n\nquery UserCalendarEvents($input: UserCalendarEventInput!) {\n  userCalendarEvents(input: $input) {\n    date\n    events {\n      ...CalendarEventPart\n    }\n  }\n}\n\nquery MetadataPartialDetails($metadataId: String!) {\n  metadataPartialDetails(metadataId: $metadataId) {\n    id\n    lot\n    title\n    image\n    publishYear\n  }\n}\n\nquery MetadataGroupsList($input: MetadataGroupsListInput!) {\n  metadataGroupsList(input: $input) {\n    details {\n      total\n      nextPage\n    }\n    items\n  }\n}\n\nquery PeopleList($input: PeopleListInput!) {\n  peopleList(input: $input) {\n    details {\n      total\n      nextPage\n    }\n    items\n  }\n}\n\nquery UserAccessLinks {\n  userAccessLinks {\n    id\n    name\n    isDemo\n    createdOn\n    expiresOn\n    timesUsed\n    isRevoked\n    maximumUses\n    isAccountDefault\n    isMutationAllowed\n  }\n}\n\nquery DailyUserActivities($input: DailyUserActivitiesInput!) {\n  dailyUserActivities(input: $input) {\n    groupedBy\n    totalCount\n    totalDuration\n    items {\n      day\n      totalReviewCount\n      workoutCount\n      measurementCount\n      audioBookCount\n      animeCount\n      bookCount\n      podcastCount\n      mangaCount\n      showCount\n      movieCount\n      videoGameCount\n      visualNovelCount\n    }\n  }\n}"): (typeof documents)["query GetOidcRedirectUrl {\n  getOidcRedirectUrl\n}\n\nquery UserByOidcIssuerId($oidcIssuerId: String!) {\n  userByOidcIssuerId(oidcIssuerId: $oidcIssuerId)\n}\n\nquery GetOidcToken($code: String!) {\n  getOidcToken(code: $code) {\n    subject\n    email\n  }\n}\n\nquery GetPresignedS3Url($key: String!) {\n  getPresignedS3Url(key: $key)\n}\n\nquery ProvidersLanguageInformation {\n  providersLanguageInformation {\n    supported\n    default\n    source\n  }\n}\n\nquery UserExports {\n  userExports {\n    url\n    size\n    endedAt\n    startedAt\n  }\n}\n\nquery UserCollectionsList($name: String) {\n  userCollectionsList(name: $name) {\n    id\n    name\n    count\n    isDefault\n    description\n    creator {\n      id\n      name\n    }\n    collaborators {\n      id\n      name\n    }\n    informationTemplate {\n      lot\n      name\n      required\n      description\n      defaultValue\n    }\n  }\n}\n\nquery UserIntegrations {\n  userIntegrations {\n    id\n    lot\n    provider\n    createdOn\n    isDisabled\n    maximumProgress\n    minimumProgress\n    lastTriggeredOn\n    syncToOwnedCollection\n  }\n}\n\nquery UserNotificationPlatforms {\n  userNotificationPlatforms {\n    id\n    lot\n    createdOn\n    isDisabled\n    description\n  }\n}\n\nquery UsersList($query: String) {\n  usersList(query: $query) {\n    id\n    lot\n    name\n    isDisabled\n  }\n}\n\nquery UserRecommendations {\n  userRecommendations\n}\n\nquery UserUpcomingCalendarEvents($input: UserUpcomingCalendarEventInput!) {\n  userUpcomingCalendarEvents(input: $input) {\n    ...CalendarEventPart\n  }\n}\n\nquery UserCalendarEvents($input: UserCalendarEventInput!) {\n  userCalendarEvents(input: $input) {\n    date\n    events {\n      ...CalendarEventPart\n    }\n  }\n}\n\nquery MetadataPartialDetails($metadataId: String!) {\n  metadataPartialDetails(metadataId: $metadataId) {\n    id\n    lot\n    title\n    image\n    publishYear\n  }\n}\n\nquery MetadataGroupsList($input: MetadataGroupsListInput!) {\n  metadataGroupsList(input: $input) {\n    details {\n      total\n      nextPage\n    }\n    items\n  }\n}\n\nquery PeopleList($input: PeopleListInput!) {\n  peopleList(input: $input) {\n    details {\n      total\n      nextPage\n    }\n    items\n  }\n}\n\nquery UserAccessLinks {\n  userAccessLinks {\n    id\n    name\n    isDemo\n    createdOn\n    expiresOn\n    timesUsed\n    isRevoked\n    maximumUses\n    isAccountDefault\n    isMutationAllowed\n  }\n}\n\nquery DailyUserActivities($input: DailyUserActivitiesInput!) {\n  dailyUserActivities(input: $input) {\n    groupedBy\n    totalCount\n    totalDuration\n    items {\n      day\n      totalReviewCount\n      workoutCount\n      measurementCount\n      audioBookCount\n      animeCount\n      bookCount\n      podcastCount\n      mangaCount\n      showCount\n      movieCount\n      videoGameCount\n      visualNovelCount\n    }\n  }\n}"];
 /**
  * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
  */
-export function graphql(source: "fragment SeenPodcastExtraInformationPart on SeenPodcastExtraInformation {\n  episode\n}\n\nfragment SeenShowExtraInformationPart on SeenShowExtraInformation {\n  episode\n  season\n}\n\nfragment SeenAnimeExtraInformationPart on SeenAnimeExtraInformation {\n  episode\n}\n\nfragment SeenMangaExtraInformationPart on SeenMangaExtraInformation {\n  chapter\n  volume\n}\n\nfragment CalendarEventPart on GraphqlCalendarEvent {\n  date\n  metadataId\n  metadataLot\n  episodeName\n  metadataTitle\n  metadataImage\n  calendarEventId\n  showExtraInformation {\n    ...SeenShowExtraInformationPart\n  }\n  podcastExtraInformation {\n    ...SeenPodcastExtraInformationPart\n  }\n  animeExtraInformation {\n    ...SeenAnimeExtraInformationPart\n  }\n}\n\nfragment SeenPart on Seen {\n  id\n  progress\n  providerWatchedOn\n  state\n  startedOn\n  finishedOn\n  lastUpdatedOn\n  numTimesUpdated\n  showExtraInformation {\n    ...SeenShowExtraInformationPart\n  }\n  podcastExtraInformation {\n    ...SeenPodcastExtraInformationPart\n  }\n  animeExtraInformation {\n    ...SeenAnimeExtraInformationPart\n  }\n  mangaExtraInformation {\n    ...SeenMangaExtraInformationPart\n  }\n}\n\nfragment MetadataSearchItemPart on MetadataSearchItem {\n  identifier\n  title\n  image\n  publishYear\n}\n\nfragment WorkoutOrExerciseTotalsPart on WorkoutOrExerciseTotals {\n  personalBestsAchieved\n  weight\n  reps\n  distance\n  duration\n  restTime\n}\n\nfragment EntityAssetsPart on EntityAssets {\n  images\n  videos\n}\n\nfragment WorkoutSetStatisticPart on WorkoutSetStatistic {\n  duration\n  distance\n  reps\n  weight\n  oneRm\n  pace\n  volume\n}\n\nfragment WorkoutSetRecordPart on WorkoutSetRecord {\n  statistic {\n    ...WorkoutSetStatisticPart\n  }\n  lot\n  personalBests\n}\n\nfragment WorkoutSummaryPart on WorkoutSummary {\n  total {\n    ...WorkoutOrExerciseTotalsPart\n  }\n  exercises {\n    numSets\n    id\n    lot\n    bestSet {\n      ...WorkoutSetRecordPart\n    }\n  }\n}\n\nfragment CollectionPart on Collection {\n  id\n  name\n  userId\n}\n\nfragment ReviewItemPart on ReviewItem {\n  id\n  rating\n  textOriginal\n  textRendered\n  isSpoiler\n  visibility\n  postedOn\n  postedBy {\n    id\n    name\n  }\n  comments {\n    id\n    text\n    createdOn\n    user {\n      id\n      name\n    }\n    likedBy\n  }\n  showExtraInformation {\n    ...SeenShowExtraInformationPart\n  }\n  podcastExtraInformation {\n    ...SeenPodcastExtraInformationPart\n  }\n  animeExtraInformation {\n    ...SeenAnimeExtraInformationPart\n  }\n  mangaExtraInformation {\n    ...SeenMangaExtraInformationPart\n  }\n}\n\nfragment WorkoutInformationPart on WorkoutInformation {\n  comment\n  assets {\n    ...EntityAssetsPart\n  }\n  exercises {\n    name\n    lot\n    notes\n    restTime\n    total {\n      ...WorkoutOrExerciseTotalsPart\n    }\n    supersetWith\n    assets {\n      ...EntityAssetsPart\n    }\n    sets {\n      statistic {\n        ...WorkoutSetStatisticPart\n      }\n      note\n      lot\n      personalBests\n      confirmedAt\n    }\n  }\n}"): (typeof documents)["fragment SeenPodcastExtraInformationPart on SeenPodcastExtraInformation {\n  episode\n}\n\nfragment SeenShowExtraInformationPart on SeenShowExtraInformation {\n  episode\n  season\n}\n\nfragment SeenAnimeExtraInformationPart on SeenAnimeExtraInformation {\n  episode\n}\n\nfragment SeenMangaExtraInformationPart on SeenMangaExtraInformation {\n  chapter\n  volume\n}\n\nfragment CalendarEventPart on GraphqlCalendarEvent {\n  date\n  metadataId\n  metadataLot\n  episodeName\n  metadataTitle\n  metadataImage\n  calendarEventId\n  showExtraInformation {\n    ...SeenShowExtraInformationPart\n  }\n  podcastExtraInformation {\n    ...SeenPodcastExtraInformationPart\n  }\n  animeExtraInformation {\n    ...SeenAnimeExtraInformationPart\n  }\n}\n\nfragment SeenPart on Seen {\n  id\n  progress\n  providerWatchedOn\n  state\n  startedOn\n  finishedOn\n  lastUpdatedOn\n  numTimesUpdated\n  showExtraInformation {\n    ...SeenShowExtraInformationPart\n  }\n  podcastExtraInformation {\n    ...SeenPodcastExtraInformationPart\n  }\n  animeExtraInformation {\n    ...SeenAnimeExtraInformationPart\n  }\n  mangaExtraInformation {\n    ...SeenMangaExtraInformationPart\n  }\n}\n\nfragment MetadataSearchItemPart on MetadataSearchItem {\n  identifier\n  title\n  image\n  publishYear\n}\n\nfragment WorkoutOrExerciseTotalsPart on WorkoutOrExerciseTotals {\n  personalBestsAchieved\n  weight\n  reps\n  distance\n  duration\n  restTime\n}\n\nfragment EntityAssetsPart on EntityAssets {\n  images\n  videos\n}\n\nfragment WorkoutSetStatisticPart on WorkoutSetStatistic {\n  duration\n  distance\n  reps\n  weight\n  oneRm\n  pace\n  volume\n}\n\nfragment WorkoutSetRecordPart on WorkoutSetRecord {\n  statistic {\n    ...WorkoutSetStatisticPart\n  }\n  lot\n  personalBests\n}\n\nfragment WorkoutSummaryPart on WorkoutSummary {\n  total {\n    ...WorkoutOrExerciseTotalsPart\n  }\n  exercises {\n    numSets\n    id\n    lot\n    bestSet {\n      ...WorkoutSetRecordPart\n    }\n  }\n}\n\nfragment CollectionPart on Collection {\n  id\n  name\n  userId\n}\n\nfragment ReviewItemPart on ReviewItem {\n  id\n  rating\n  textOriginal\n  textRendered\n  isSpoiler\n  visibility\n  postedOn\n  postedBy {\n    id\n    name\n  }\n  comments {\n    id\n    text\n    createdOn\n    user {\n      id\n      name\n    }\n    likedBy\n  }\n  showExtraInformation {\n    ...SeenShowExtraInformationPart\n  }\n  podcastExtraInformation {\n    ...SeenPodcastExtraInformationPart\n  }\n  animeExtraInformation {\n    ...SeenAnimeExtraInformationPart\n  }\n  mangaExtraInformation {\n    ...SeenMangaExtraInformationPart\n  }\n}\n\nfragment WorkoutInformationPart on WorkoutInformation {\n  comment\n  assets {\n    ...EntityAssetsPart\n  }\n  exercises {\n    name\n    lot\n    notes\n    restTime\n    total {\n      ...WorkoutOrExerciseTotalsPart\n    }\n    supersetWith\n    assets {\n      ...EntityAssetsPart\n    }\n    sets {\n      statistic {\n        ...WorkoutSetStatisticPart\n      }\n      note\n      lot\n      personalBests\n      confirmedAt\n    }\n  }\n}"];
+export function graphql(source: "fragment SeenPodcastExtraInformationPart on SeenPodcastExtraInformation {\n  episode\n}\n\nfragment SeenShowExtraInformationPart on SeenShowExtraInformation {\n  episode\n  season\n}\n\nfragment SeenAnimeExtraInformationPart on SeenAnimeExtraInformation {\n  episode\n}\n\nfragment SeenMangaExtraInformationPart on SeenMangaExtraInformation {\n  chapter\n  volume\n}\n\nfragment CalendarEventPart on GraphqlCalendarEvent {\n  date\n  metadataId\n  metadataLot\n  episodeName\n  metadataTitle\n  metadataImage\n  calendarEventId\n  showExtraInformation {\n    ...SeenShowExtraInformationPart\n  }\n  podcastExtraInformation {\n    ...SeenPodcastExtraInformationPart\n  }\n  animeExtraInformation {\n    ...SeenAnimeExtraInformationPart\n  }\n}\n\nfragment SeenPart on Seen {\n  id\n  state\n  progress\n  startedOn\n  finishedOn\n  lastUpdatedOn\n  manualTimeSpent\n  numTimesUpdated\n  providerWatchedOn\n  showExtraInformation {\n    ...SeenShowExtraInformationPart\n  }\n  podcastExtraInformation {\n    ...SeenPodcastExtraInformationPart\n  }\n  animeExtraInformation {\n    ...SeenAnimeExtraInformationPart\n  }\n  mangaExtraInformation {\n    ...SeenMangaExtraInformationPart\n  }\n}\n\nfragment MetadataSearchItemPart on MetadataSearchItem {\n  identifier\n  title\n  image\n  publishYear\n}\n\nfragment WorkoutOrExerciseTotalsPart on WorkoutOrExerciseTotals {\n  personalBestsAchieved\n  weight\n  reps\n  distance\n  duration\n  restTime\n}\n\nfragment EntityAssetsPart on EntityAssets {\n  images\n  videos\n}\n\nfragment WorkoutSetStatisticPart on WorkoutSetStatistic {\n  duration\n  distance\n  reps\n  weight\n  oneRm\n  pace\n  volume\n}\n\nfragment WorkoutSetRecordPart on WorkoutSetRecord {\n  statistic {\n    ...WorkoutSetStatisticPart\n  }\n  lot\n  personalBests\n}\n\nfragment WorkoutSummaryPart on WorkoutSummary {\n  total {\n    ...WorkoutOrExerciseTotalsPart\n  }\n  exercises {\n    numSets\n    id\n    lot\n    bestSet {\n      ...WorkoutSetRecordPart\n    }\n  }\n}\n\nfragment CollectionPart on Collection {\n  id\n  name\n  userId\n}\n\nfragment ReviewItemPart on ReviewItem {\n  id\n  rating\n  textOriginal\n  textRendered\n  isSpoiler\n  visibility\n  postedOn\n  postedBy {\n    id\n    name\n  }\n  comments {\n    id\n    text\n    createdOn\n    user {\n      id\n      name\n    }\n    likedBy\n  }\n  showExtraInformation {\n    ...SeenShowExtraInformationPart\n  }\n  podcastExtraInformation {\n    ...SeenPodcastExtraInformationPart\n  }\n  animeExtraInformation {\n    ...SeenAnimeExtraInformationPart\n  }\n  mangaExtraInformation {\n    ...SeenMangaExtraInformationPart\n  }\n}\n\nfragment WorkoutInformationPart on WorkoutInformation {\n  comment\n  assets {\n    ...EntityAssetsPart\n  }\n  exercises {\n    name\n    lot\n    notes\n    restTime\n    total {\n      ...WorkoutOrExerciseTotalsPart\n    }\n    supersetWith\n    assets {\n      ...EntityAssetsPart\n    }\n    sets {\n      statistic {\n        ...WorkoutSetStatisticPart\n      }\n      note\n      lot\n      personalBests\n      confirmedAt\n    }\n  }\n}"): (typeof documents)["fragment SeenPodcastExtraInformationPart on SeenPodcastExtraInformation {\n  episode\n}\n\nfragment SeenShowExtraInformationPart on SeenShowExtraInformation {\n  episode\n  season\n}\n\nfragment SeenAnimeExtraInformationPart on SeenAnimeExtraInformation {\n  episode\n}\n\nfragment SeenMangaExtraInformationPart on SeenMangaExtraInformation {\n  chapter\n  volume\n}\n\nfragment CalendarEventPart on GraphqlCalendarEvent {\n  date\n  metadataId\n  metadataLot\n  episodeName\n  metadataTitle\n  metadataImage\n  calendarEventId\n  showExtraInformation {\n    ...SeenShowExtraInformationPart\n  }\n  podcastExtraInformation {\n    ...SeenPodcastExtraInformationPart\n  }\n  animeExtraInformation {\n    ...SeenAnimeExtraInformationPart\n  }\n}\n\nfragment SeenPart on Seen {\n  id\n  state\n  progress\n  startedOn\n  finishedOn\n  lastUpdatedOn\n  manualTimeSpent\n  numTimesUpdated\n  providerWatchedOn\n  showExtraInformation {\n    ...SeenShowExtraInformationPart\n  }\n  podcastExtraInformation {\n    ...SeenPodcastExtraInformationPart\n  }\n  animeExtraInformation {\n    ...SeenAnimeExtraInformationPart\n  }\n  mangaExtraInformation {\n    ...SeenMangaExtraInformationPart\n  }\n}\n\nfragment MetadataSearchItemPart on MetadataSearchItem {\n  identifier\n  title\n  image\n  publishYear\n}\n\nfragment WorkoutOrExerciseTotalsPart on WorkoutOrExerciseTotals {\n  personalBestsAchieved\n  weight\n  reps\n  distance\n  duration\n  restTime\n}\n\nfragment EntityAssetsPart on EntityAssets {\n  images\n  videos\n}\n\nfragment WorkoutSetStatisticPart on WorkoutSetStatistic {\n  duration\n  distance\n  reps\n  weight\n  oneRm\n  pace\n  volume\n}\n\nfragment WorkoutSetRecordPart on WorkoutSetRecord {\n  statistic {\n    ...WorkoutSetStatisticPart\n  }\n  lot\n  personalBests\n}\n\nfragment WorkoutSummaryPart on WorkoutSummary {\n  total {\n    ...WorkoutOrExerciseTotalsPart\n  }\n  exercises {\n    numSets\n    id\n    lot\n    bestSet {\n      ...WorkoutSetRecordPart\n    }\n  }\n}\n\nfragment CollectionPart on Collection {\n  id\n  name\n  userId\n}\n\nfragment ReviewItemPart on ReviewItem {\n  id\n  rating\n  textOriginal\n  textRendered\n  isSpoiler\n  visibility\n  postedOn\n  postedBy {\n    id\n    name\n  }\n  comments {\n    id\n    text\n    createdOn\n    user {\n      id\n      name\n    }\n    likedBy\n  }\n  showExtraInformation {\n    ...SeenShowExtraInformationPart\n  }\n  podcastExtraInformation {\n    ...SeenPodcastExtraInformationPart\n  }\n  animeExtraInformation {\n    ...SeenAnimeExtraInformationPart\n  }\n  mangaExtraInformation {\n    ...SeenMangaExtraInformationPart\n  }\n}\n\nfragment WorkoutInformationPart on WorkoutInformation {\n  comment\n  assets {\n    ...EntityAssetsPart\n  }\n  exercises {\n    name\n    lot\n    notes\n    restTime\n    total {\n      ...WorkoutOrExerciseTotalsPart\n    }\n    supersetWith\n    assets {\n      ...EntityAssetsPart\n    }\n    sets {\n      statistic {\n        ...WorkoutSetStatisticPart\n      }\n      note\n      lot\n      personalBests\n      confirmedAt\n    }\n  }\n}"];
 
 export function graphql(source: string) {
   return (documents as any)[source] ?? {};
diff --git a/libs/generated/src/graphql/backend/graphql.ts b/libs/generated/src/graphql/backend/graphql.ts
index 786cbf1cf9..291644afe0 100644
--- a/libs/generated/src/graphql/backend/graphql.ts
+++ b/libs/generated/src/graphql/backend/graphql.ts
@@ -45,6 +45,20 @@ export type Scalars = {
   NaiveDateTime: { input: any; output: any; }
 };
 
+export type AccessLink = {
+  createdOn: Scalars['DateTime']['output'];
+  expiresOn?: Maybe<Scalars['DateTime']['output']>;
+  id: Scalars['String']['output'];
+  isAccountDefault?: Maybe<Scalars['Boolean']['output']>;
+  isDemo?: Maybe<Scalars['Boolean']['output']>;
+  isMutationAllowed?: Maybe<Scalars['Boolean']['output']>;
+  isRevoked?: Maybe<Scalars['Boolean']['output']>;
+  maximumUses?: Maybe<Scalars['Int']['output']>;
+  name: Scalars['String']['output'];
+  redirectTo?: Maybe<Scalars['String']['output']>;
+  timesUsed: Scalars['Int']['output'];
+};
+
 export type AnimeAiringScheduleSpecifics = {
   airingAt: Scalars['NaiveDateTime']['output'];
   episode: Scalars['Int']['output'];
@@ -161,6 +175,14 @@ export type CollectionExtraInformation = {
   required?: Maybe<Scalars['Boolean']['output']>;
 };
 
+export type CollectionExtraInformationInput = {
+  defaultValue?: InputMaybe<Scalars['String']['input']>;
+  description: Scalars['String']['input'];
+  lot: CollectionExtraInformationLot;
+  name: Scalars['String']['input'];
+  required?: InputMaybe<Scalars['Boolean']['input']>;
+};
+
 export enum CollectionExtraInformationLot {
   Date = 'DATE',
   DateTime = 'DATE_TIME',
@@ -181,6 +203,7 @@ export type CollectionItem = {
 };
 
 export type CommitMediaInput = {
+  forceUpdate?: InputMaybe<Scalars['Boolean']['input']>;
   identifier: Scalars['String']['input'];
   lot: MediaLot;
   source: MediaSource;
@@ -209,6 +232,15 @@ export type CoreDetails = {
   websiteUrl: Scalars['String']['output'];
 };
 
+export type CreateAccessLinkInput = {
+  expiresOn?: InputMaybe<Scalars['DateTime']['input']>;
+  isAccountDefault?: InputMaybe<Scalars['Boolean']['input']>;
+  isMutationAllowed?: InputMaybe<Scalars['Boolean']['input']>;
+  maximumUses?: InputMaybe<Scalars['Int']['input']>;
+  name: Scalars['String']['input'];
+  redirectTo?: InputMaybe<Scalars['String']['input']>;
+};
+
 export type CreateCustomMetadataInput = {
   animeSpecifics?: InputMaybe<AnimeSpecificsInput>;
   audioBookSpecifics?: InputMaybe<AudioBookSpecificsInput>;
@@ -231,7 +263,9 @@ export type CreateCustomMetadataInput = {
 };
 
 export type CreateOrUpdateCollectionInput = {
+  collaborators?: InputMaybe<Array<Scalars['String']['input']>>;
   description?: InputMaybe<Scalars['String']['input']>;
+  informationTemplate?: InputMaybe<Array<CollectionExtraInformationInput>>;
   name: Scalars['String']['input'];
   updateId?: InputMaybe<Scalars['String']['input']>;
 };
@@ -251,6 +285,7 @@ export type CreateUserIntegrationInput = {
   minimumProgress?: InputMaybe<Scalars['Decimal']['input']>;
   provider: IntegrationProvider;
   providerSpecifics?: InputMaybe<IntegrationSourceSpecificsInput>;
+  syncToOwnedCollection?: InputMaybe<Scalars['Boolean']['input']>;
 };
 
 export type CreateUserNotificationPlatformInput = {
@@ -407,7 +442,8 @@ export enum EntityLot {
   Metadata = 'METADATA',
   MetadataGroup = 'METADATA_GROUP',
   Person = 'PERSON',
-  Workout = 'WORKOUT'
+  Workout = 'WORKOUT',
+  WorkoutTemplate = 'WORKOUT_TEMPLATE'
 }
 
 export type EntityWithLot = {
@@ -762,6 +798,7 @@ export type Integration = {
   maximumProgress?: Maybe<Scalars['Decimal']['output']>;
   minimumProgress?: Maybe<Scalars['Decimal']['output']>;
   provider: IntegrationProvider;
+  syncToOwnedCollection?: Maybe<Scalars['Boolean']['output']>;
 };
 
 export enum IntegrationLot {
@@ -1024,12 +1061,16 @@ export type MutationRoot = {
   commitMetadataGroup: StringIdObject;
   /** Fetches details about a person and creates a person item in the database. */
   commitPerson: StringIdObject;
+  /** Create or edit an access link. */
+  createAccessLink: StringIdObject;
   /** Create a custom exercise. */
   createCustomExercise: Scalars['String']['output'];
   /** Create a custom media item. */
   createCustomMetadata: StringIdObject;
   /** Create a new collection for the logged in user or edit details of an existing one. */
   createOrUpdateCollection: StringIdObject;
+  /** Create or update a workout template. */
+  createOrUpdateWorkoutTemplate: Scalars['String']['output'];
   /** Create, like or delete a comment on a review. */
   createReviewComment: Scalars['Boolean']['output'];
   /** Create an integration for the currently logged in user. */
@@ -1058,6 +1099,8 @@ export type MutationRoot = {
   deleteUserNotificationPlatform: Scalars['Boolean']['output'];
   /** Delete a workout and remove all exercise associations. */
   deleteUserWorkout: Scalars['Boolean']['output'];
+  /** Delete a workout template. */
+  deleteWorkoutTemplate: Scalars['Boolean']['output'];
   /** Start a background job. */
   deployBackgroundJob: Scalars['Boolean']['output'];
   /**
@@ -1098,6 +1141,8 @@ export type MutationRoot = {
   postReview: StringIdObject;
   /** Get a presigned URL (valid for 10 minutes) for a given file name. */
   presignedPutS3Url: PresignedPutUrlResponse;
+  /** Get an access token using an access link. */
+  processAccessLink: ProcessAccessLinkResult;
   /**
    * Create a new user for the service. Also set their `lot` as admin if
    * they are the first user.
@@ -1105,6 +1150,8 @@ export type MutationRoot = {
   registerUser: RegisterResult;
   /** Remove an entity from a collection if it is not there, otherwise do nothing. */
   removeEntityFromCollection: StringIdObject;
+  /** Revoke an access link. */
+  revokeAccessLink: Scalars['Boolean']['output'];
   /** Test all notification platforms for the currently logged in user. */
   testUserNotificationPlatforms: Scalars['Boolean']['output'];
   /** Update a custom exercise. */
@@ -1144,6 +1191,11 @@ export type MutationRootCommitPersonArgs = {
 };
 
 
+export type MutationRootCreateAccessLinkArgs = {
+  input: CreateAccessLinkInput;
+};
+
+
 export type MutationRootCreateCustomExerciseArgs = {
   input: ExerciseInput;
 };
@@ -1159,6 +1211,11 @@ export type MutationRootCreateOrUpdateCollectionArgs = {
 };
 
 
+export type MutationRootCreateOrUpdateWorkoutTemplateArgs = {
+  input: UserWorkoutInput;
+};
+
+
 export type MutationRootCreateReviewCommentArgs = {
   input: CreateReviewCommentInput;
 };
@@ -1229,6 +1286,11 @@ export type MutationRootDeleteUserWorkoutArgs = {
 };
 
 
+export type MutationRootDeleteWorkoutTemplateArgs = {
+  workoutTemplateId: Scalars['String']['input'];
+};
+
+
 export type MutationRootDeployBackgroundJobArgs = {
   jobName: BackgroundJob;
 };
@@ -1285,6 +1347,11 @@ export type MutationRootPresignedPutS3UrlArgs = {
 };
 
 
+export type MutationRootProcessAccessLinkArgs = {
+  input: ProcessAccessLinkInput;
+};
+
+
 export type MutationRootRegisterUserArgs = {
   input: RegisterUserInput;
 };
@@ -1295,6 +1362,11 @@ export type MutationRootRemoveEntityFromCollectionArgs = {
 };
 
 
+export type MutationRootRevokeAccessLinkArgs = {
+  accessLinkId: Scalars['String']['input'];
+};
+
+
 export type MutationRootUpdateCustomExerciseArgs = {
   input: UpdateCustomExerciseInput;
 };
@@ -1497,6 +1569,30 @@ export type PresignedPutUrlResponse = {
   uploadUrl: Scalars['String']['output'];
 };
 
+export type ProcessAccessLinkError = {
+  error: ProcessAccessLinkErrorVariant;
+};
+
+export enum ProcessAccessLinkErrorVariant {
+  Expired = 'EXPIRED',
+  MaximumUsesReached = 'MAXIMUM_USES_REACHED',
+  NotFound = 'NOT_FOUND',
+  Revoked = 'REVOKED'
+}
+
+export type ProcessAccessLinkInput = {
+  id?: InputMaybe<Scalars['String']['input']>;
+  username?: InputMaybe<Scalars['String']['input']>;
+};
+
+export type ProcessAccessLinkResponse = {
+  apiKey: Scalars['String']['output'];
+  redirectTo?: Maybe<Scalars['String']['output']>;
+  tokenValidForDays: Scalars['Int']['output'];
+};
+
+export type ProcessAccessLinkResult = ProcessAccessLinkError | ProcessAccessLinkResponse;
+
 /** An exercise that has been processed and committed to the database. */
 export type ProcessedExercise = {
   assets?: Maybe<EntityAssets>;
@@ -1579,6 +1675,8 @@ export type QueryRoot = {
   personDetails: PersonDetails;
   /** Get all languages supported by all the providers. */
   providersLanguageInformation: Array<ProviderLanguageInformation>;
+  /** Get all access links generated by the currently logged in user. */
+  userAccessLinks: Array<AccessLink>;
   /** Get user by OIDC issuer ID. */
   userByOidcIssuerId?: Maybe<Scalars['String']['output']>;
   /** Get calendar events for a user between a given date range. */
@@ -1605,14 +1703,20 @@ export type QueryRoot = {
   userPersonDetails: UserPersonDetails;
   /** Get a user's preferences. */
   userPreferences: UserPreferences;
+  /** Get media recommendations for the currently logged in user. */
+  userRecommendations: Array<Scalars['String']['output']>;
   /** Get upcoming calendar events for the given filter. */
   userUpcomingCalendarEvents: Array<GraphqlCalendarEvent>;
+  /** Get a paginated list of templates created by the user. */
+  userWorkoutTemplatesList: WorkoutTemplateListResults;
   /** Get a paginated list of workouts done by the user. */
   userWorkoutsList: WorkoutListResults;
   /** Get details about all the users in the service. */
   usersList: Array<User>;
   /** Get details about a workout. */
   workoutDetails: UserWorkoutDetails;
+  /** Get information about a workout template. */
+  workoutTemplateDetails: UserWorkoutTemplateDetails;
 };
 
 
@@ -1751,6 +1855,11 @@ export type QueryRootUserUpcomingCalendarEventsArgs = {
 };
 
 
+export type QueryRootUserWorkoutTemplatesListArgs = {
+  input: SearchInput;
+};
+
+
 export type QueryRootUserWorkoutsListArgs = {
   input: SearchInput;
 };
@@ -1765,6 +1874,11 @@ export type QueryRootWorkoutDetailsArgs = {
   workoutId: Scalars['String']['input'];
 };
 
+
+export type QueryRootWorkoutTemplateDetailsArgs = {
+  workoutTemplateId: Scalars['String']['input'];
+};
+
 export type RegisterError = {
   error: RegisterErrorVariant;
 };
@@ -1814,6 +1928,7 @@ export type Seen = {
   id: Scalars['String']['output'];
   lastUpdatedOn: Scalars['DateTime']['output'];
   mangaExtraInformation?: Maybe<SeenMangaExtraInformation>;
+  manualTimeSpent?: Maybe<Scalars['Decimal']['output']>;
   metadataId: Scalars['String']['output'];
   numTimesUpdated: Scalars['Int']['output'];
   podcastExtraInformation?: Maybe<SeenPodcastExtraInformation>;
@@ -1950,6 +2065,7 @@ export type UpdateCustomExerciseInput = {
 
 export type UpdateSeenItemInput = {
   finishedOn?: InputMaybe<Scalars['NaiveDate']['input']>;
+  manualTimeSpent?: InputMaybe<Scalars['Decimal']['input']>;
   providerWatchedOn?: InputMaybe<Scalars['String']['input']>;
   seenId: Scalars['String']['input'];
   startedOn?: InputMaybe<Scalars['NaiveDate']['input']>;
@@ -1970,6 +2086,7 @@ export type UpdateUserIntegrationInput = {
   isDisabled?: InputMaybe<Scalars['Boolean']['input']>;
   maximumProgress?: InputMaybe<Scalars['Decimal']['input']>;
   minimumProgress?: InputMaybe<Scalars['Decimal']['input']>;
+  syncToOwnedCollection?: InputMaybe<Scalars['Boolean']['input']>;
 };
 
 export type UpdateUserNotificationPlatformInput = {
@@ -2054,6 +2171,7 @@ export type UserFeaturesEnabledPreferences = {
 export type UserFitnessFeaturesEnabledPreferences = {
   enabled: Scalars['Boolean']['output'];
   measurements: Scalars['Boolean']['output'];
+  templates: Scalars['Boolean']['output'];
   workouts: Scalars['Boolean']['output'];
 };
 
@@ -2356,11 +2474,14 @@ export type UserWorkoutDetails = {
 export type UserWorkoutInput = {
   assets?: InputMaybe<EntityAssetsInput>;
   comment?: InputMaybe<Scalars['String']['input']>;
+  defaultRestTimer?: InputMaybe<Scalars['Int']['input']>;
   endTime: Scalars['DateTime']['input'];
   exercises: Array<UserExerciseInput>;
   name: Scalars['String']['input'];
   repeatedFrom?: InputMaybe<Scalars['String']['input']>;
   startTime: Scalars['DateTime']['input'];
+  templateId?: InputMaybe<Scalars['String']['input']>;
+  updateWorkoutTemplateId?: InputMaybe<Scalars['String']['input']>;
 };
 
 export type UserWorkoutSetRecord = {
@@ -2370,6 +2491,11 @@ export type UserWorkoutSetRecord = {
   statistic: SetStatisticInput;
 };
 
+export type UserWorkoutTemplateDetails = {
+  collections: Array<Collection>;
+  details: WorkoutTemplate;
+};
+
 export type VideoGameSpecifics = {
   platforms: Array<Scalars['String']['output']>;
 };
@@ -2407,6 +2533,7 @@ export type Workout = {
   repeatedFrom?: Maybe<Scalars['String']['output']>;
   startTime: Scalars['DateTime']['output'];
   summary: WorkoutSummary;
+  templateId?: Maybe<Scalars['String']['output']>;
 };
 
 /** Information about a workout done. */
@@ -2482,6 +2609,21 @@ export type WorkoutSummaryExercise = {
   numSets: Scalars['Int']['output'];
 };
 
+export type WorkoutTemplate = {
+  createdOn: Scalars['DateTime']['output'];
+  defaultRestTimer?: Maybe<Scalars['Int']['output']>;
+  id: Scalars['String']['output'];
+  information: WorkoutInformation;
+  name: Scalars['String']['output'];
+  summary: WorkoutSummary;
+  visibility: Visibility;
+};
+
+export type WorkoutTemplateListResults = {
+  details: SearchDetails;
+  items: Array<WorkoutTemplate>;
+};
+
 export type RegisterUserMutationVariables = Exact<{
   input: RegisterUserInput;
 }>;
@@ -2594,6 +2736,13 @@ export type CreateUserWorkoutMutationVariables = Exact<{
 
 export type CreateUserWorkoutMutation = { createUserWorkout: string };
 
+export type CreateOrUpdateWorkoutTemplateMutationVariables = Exact<{
+  input: UserWorkoutInput;
+}>;
+
+
+export type CreateOrUpdateWorkoutTemplateMutation = { createOrUpdateWorkoutTemplate: string };
+
 export type DeleteCollectionMutationVariables = Exact<{
   collectionName: Scalars['String']['input'];
 }>;
@@ -2657,6 +2806,13 @@ export type DeleteUserWorkoutMutationVariables = Exact<{
 
 export type DeleteUserWorkoutMutation = { deleteUserWorkout: boolean };
 
+export type DeleteWorkoutTemplateMutationVariables = Exact<{
+  workoutTemplateId: Scalars['String']['input'];
+}>;
+
+
+export type DeleteWorkoutTemplateMutation = { deleteWorkoutTemplate: boolean };
+
 export type DeployBackgroundJobMutationVariables = Exact<{
   jobName: BackgroundJob;
 }>;
@@ -2785,6 +2941,27 @@ export type UpdateUserPreferenceMutationVariables = Exact<{
 
 export type UpdateUserPreferenceMutation = { updateUserPreference: boolean };
 
+export type CreateAccessLinkMutationVariables = Exact<{
+  input: CreateAccessLinkInput;
+}>;
+
+
+export type CreateAccessLinkMutation = { createAccessLink: { id: string } };
+
+export type ProcessAccessLinkMutationVariables = Exact<{
+  input: ProcessAccessLinkInput;
+}>;
+
+
+export type ProcessAccessLinkMutation = { processAccessLink: { __typename: 'ProcessAccessLinkError', error: ProcessAccessLinkErrorVariant } | { __typename: 'ProcessAccessLinkResponse', apiKey: string, redirectTo?: string | null, tokenValidForDays: number } };
+
+export type RevokeAccessLinkMutationVariables = Exact<{
+  accessLinkId: Scalars['String']['input'];
+}>;
+
+
+export type RevokeAccessLinkMutation = { revokeAccessLink: boolean };
+
 export type CollectionContentsQueryVariables = Exact<{
   input: CollectionContentsInput;
 }>;
@@ -2913,7 +3090,7 @@ export type UserMetadataDetailsQueryVariables = Exact<{
 }>;
 
 
-export type UserMetadataDetailsQuery = { userMetadataDetails: { mediaReason?: Array<UserToMediaReason> | null, hasInteracted: boolean, averageRating?: string | null, seenByAllCount: number, seenByUserCount: number, collections: Array<{ id: string, name: string, userId: string }>, inProgress?: { id: string, progress: string, providerWatchedOn?: string | null, state: SeenState, startedOn?: string | null, finishedOn?: string | null, lastUpdatedOn: string, numTimesUpdated: number, showExtraInformation?: { episode: number, season: number } | null, podcastExtraInformation?: { episode: number } | null, animeExtraInformation?: { episode?: number | null } | null, mangaExtraInformation?: { chapter?: string | null, volume?: number | null } | null } | null, history: Array<{ id: string, progress: string, providerWatchedOn?: string | null, state: SeenState, startedOn?: string | null, finishedOn?: string | null, lastUpdatedOn: string, numTimesUpdated: number, showExtraInformation?: { episode: number, season: number } | null, podcastExtraInformation?: { episode: number } | null, animeExtraInformation?: { episode?: number | null } | null, mangaExtraInformation?: { chapter?: string | null, volume?: number | null } | null }>, reviews: Array<{ id: string, rating?: string | null, textOriginal?: string | null, textRendered?: string | null, isSpoiler: boolean, visibility: Visibility, postedOn: string, postedBy: { id: string, name: string }, comments: Array<{ id: string, text: string, createdOn: string, likedBy: Array<string>, user: { id: string, name: string } }>, showExtraInformation?: { episode: number, season: number } | null, podcastExtraInformation?: { episode: number } | null, animeExtraInformation?: { episode?: number | null } | null, mangaExtraInformation?: { chapter?: string | null, volume?: number | null } | null }>, nextEntry?: { season?: number | null, volume?: number | null, episode?: number | null, chapter?: string | null } | null, showProgress?: Array<{ timesSeen: number, seasonNumber: number, episodes: Array<{ episodeNumber: number, timesSeen: number }> }> | null, podcastProgress?: Array<{ episodeNumber: number, timesSeen: number }> | null } };
+export type UserMetadataDetailsQuery = { userMetadataDetails: { mediaReason?: Array<UserToMediaReason> | null, hasInteracted: boolean, averageRating?: string | null, seenByAllCount: number, seenByUserCount: number, collections: Array<{ id: string, name: string, userId: string }>, inProgress?: { id: string, state: SeenState, progress: string, startedOn?: string | null, finishedOn?: string | null, lastUpdatedOn: string, manualTimeSpent?: string | null, numTimesUpdated: number, providerWatchedOn?: string | null, showExtraInformation?: { episode: number, season: number } | null, podcastExtraInformation?: { episode: number } | null, animeExtraInformation?: { episode?: number | null } | null, mangaExtraInformation?: { chapter?: string | null, volume?: number | null } | null } | null, history: Array<{ id: string, state: SeenState, progress: string, startedOn?: string | null, finishedOn?: string | null, lastUpdatedOn: string, manualTimeSpent?: string | null, numTimesUpdated: number, providerWatchedOn?: string | null, showExtraInformation?: { episode: number, season: number } | null, podcastExtraInformation?: { episode: number } | null, animeExtraInformation?: { episode?: number | null } | null, mangaExtraInformation?: { chapter?: string | null, volume?: number | null } | null }>, reviews: Array<{ id: string, rating?: string | null, textOriginal?: string | null, textRendered?: string | null, isSpoiler: boolean, visibility: Visibility, postedOn: string, postedBy: { id: string, name: string }, comments: Array<{ id: string, text: string, createdOn: string, likedBy: Array<string>, user: { id: string, name: string } }>, showExtraInformation?: { episode: number, season: number } | null, podcastExtraInformation?: { episode: number } | null, animeExtraInformation?: { episode?: number | null } | null, mangaExtraInformation?: { chapter?: string | null, volume?: number | null } | null }>, nextEntry?: { season?: number | null, volume?: number | null, episode?: number | null, chapter?: string | null } | null, showProgress?: Array<{ timesSeen: number, seasonNumber: number, episodes: Array<{ episodeNumber: number, timesSeen: number }> }> | null, podcastProgress?: Array<{ episodeNumber: number, timesSeen: number }> | null } };
 
 export type UserMetadataGroupDetailsQueryVariables = Exact<{
   metadataGroupId: Scalars['String']['input'];
@@ -2932,7 +3109,14 @@ export type UserPersonDetailsQuery = { userPersonDetails: { collections: Array<{
 export type UserPreferencesQueryVariables = Exact<{ [key: string]: never; }>;
 
 
-export type UserPreferencesQuery = { userPreferences: { general: { reviewScale: UserReviewScale, displayNsfw: boolean, disableVideos: boolean, persistQueries: boolean, watchProviders: Array<string>, disableReviews: boolean, disableIntegrations: boolean, disableWatchProviders: boolean, disableNavigationAnimation: boolean, dashboard: Array<{ section: DashboardElementLot, hidden: boolean, numElements?: number | null }> }, fitness: { measurements: { custom: Array<{ name: string, dataType: UserCustomMeasurementDataType }>, inbuilt: { weight: boolean, bodyMassIndex: boolean, totalBodyWater: boolean, muscle: boolean, leanBodyMass: boolean, bodyFat: boolean, boneMass: boolean, visceralFat: boolean, waistCircumference: boolean, waistToHeightRatio: boolean, hipCircumference: boolean, waistToHipRatio: boolean, chestCircumference: boolean, thighCircumference: boolean, bicepsCircumference: boolean, neckCircumference: boolean, bodyFatCaliper: boolean, chestSkinfold: boolean, abdominalSkinfold: boolean, thighSkinfold: boolean, basalMetabolicRate: boolean, totalDailyEnergyExpenditure: boolean, calories: boolean } }, exercises: { unitSystem: UserUnitSystem } }, notifications: { toSend: Array<MediaStateChanged>, enabled: boolean }, featuresEnabled: { others: { calendar: boolean, collections: boolean }, fitness: { enabled: boolean, workouts: boolean, measurements: boolean }, media: { enabled: boolean, anime: boolean, audioBook: boolean, book: boolean, manga: boolean, movie: boolean, podcast: boolean, show: boolean, videoGame: boolean, visualNovel: boolean, people: boolean, groups: boolean, genres: boolean } } } };
+export type UserPreferencesQuery = { userPreferences: { general: { reviewScale: UserReviewScale, displayNsfw: boolean, disableVideos: boolean, persistQueries: boolean, watchProviders: Array<string>, disableReviews: boolean, disableIntegrations: boolean, disableWatchProviders: boolean, disableNavigationAnimation: boolean, dashboard: Array<{ section: DashboardElementLot, hidden: boolean, numElements?: number | null }> }, fitness: { measurements: { custom: Array<{ name: string, dataType: UserCustomMeasurementDataType }>, inbuilt: { weight: boolean, bodyMassIndex: boolean, totalBodyWater: boolean, muscle: boolean, leanBodyMass: boolean, bodyFat: boolean, boneMass: boolean, visceralFat: boolean, waistCircumference: boolean, waistToHeightRatio: boolean, hipCircumference: boolean, waistToHipRatio: boolean, chestCircumference: boolean, thighCircumference: boolean, bicepsCircumference: boolean, neckCircumference: boolean, bodyFatCaliper: boolean, chestSkinfold: boolean, abdominalSkinfold: boolean, thighSkinfold: boolean, basalMetabolicRate: boolean, totalDailyEnergyExpenditure: boolean, calories: boolean } }, exercises: { unitSystem: UserUnitSystem } }, notifications: { toSend: Array<MediaStateChanged>, enabled: boolean }, featuresEnabled: { others: { calendar: boolean, collections: boolean }, fitness: { enabled: boolean, workouts: boolean, templates: boolean, measurements: boolean }, media: { enabled: boolean, anime: boolean, audioBook: boolean, book: boolean, manga: boolean, movie: boolean, podcast: boolean, show: boolean, videoGame: boolean, visualNovel: boolean, people: boolean, groups: boolean, genres: boolean } } } };
+
+export type UserWorkoutTemplatesListQueryVariables = Exact<{
+  input: SearchInput;
+}>;
+
+
+export type UserWorkoutTemplatesListQuery = { userWorkoutTemplatesList: { details: { total: number, nextPage?: number | null }, items: Array<{ id: string, name: string, createdOn: string, visibility: Visibility, summary: { total?: { personalBestsAchieved: number, weight: string, reps: string, distance: string, duration: string, restTime: number } | null, exercises: Array<{ numSets: number, id: string, lot?: ExerciseLot | null, bestSet?: { lot: SetLot, personalBests?: Array<WorkoutSetPersonalBest> | null, statistic: { duration?: string | null, distance?: string | null, reps?: string | null, weight?: string | null, oneRm?: string | null, pace?: string | null, volume?: string | null } } | null }> } }> } };
 
 export type UserWorkoutsListQueryVariables = Exact<{
   input: SearchInput;
@@ -2946,7 +3130,14 @@ export type WorkoutDetailsQueryVariables = Exact<{
 }>;
 
 
-export type WorkoutDetailsQuery = { workoutDetails: { collections: Array<{ id: string, name: string, userId: string }>, details: { id: string, name: string, endTime: string, duration: number, startTime: string, repeatedFrom?: string | null, summary: { total?: { personalBestsAchieved: number, weight: string, reps: string, distance: string, duration: string, restTime: number } | null, exercises: Array<{ numSets: number, id: string, lot?: ExerciseLot | null, bestSet?: { lot: SetLot, personalBests?: Array<WorkoutSetPersonalBest> | null, statistic: { duration?: string | null, distance?: string | null, reps?: string | null, weight?: string | null, oneRm?: string | null, pace?: string | null, volume?: string | null } } | null }> }, information: { comment?: string | null, assets?: { images: Array<string>, videos: Array<string> } | null, exercises: Array<{ name: string, lot: ExerciseLot, notes: Array<string>, restTime?: number | null, supersetWith: Array<number>, total?: { personalBestsAchieved: number, weight: string, reps: string, distance: string, duration: string, restTime: number } | null, assets?: { images: Array<string>, videos: Array<string> } | null, sets: Array<{ note?: string | null, lot: SetLot, personalBests?: Array<WorkoutSetPersonalBest> | null, confirmedAt?: string | null, statistic: { duration?: string | null, distance?: string | null, reps?: string | null, weight?: string | null, oneRm?: string | null, pace?: string | null, volume?: string | null } }> }> } } } };
+export type WorkoutDetailsQuery = { workoutDetails: { collections: Array<{ id: string, name: string, userId: string }>, details: { id: string, name: string, endTime: string, duration: number, startTime: string, templateId?: string | null, repeatedFrom?: string | null, summary: { total?: { personalBestsAchieved: number, weight: string, reps: string, distance: string, duration: string, restTime: number } | null, exercises: Array<{ numSets: number, id: string, lot?: ExerciseLot | null, bestSet?: { lot: SetLot, personalBests?: Array<WorkoutSetPersonalBest> | null, statistic: { duration?: string | null, distance?: string | null, reps?: string | null, weight?: string | null, oneRm?: string | null, pace?: string | null, volume?: string | null } } | null }> }, information: { comment?: string | null, assets?: { images: Array<string>, videos: Array<string> } | null, exercises: Array<{ name: string, lot: ExerciseLot, notes: Array<string>, restTime?: number | null, supersetWith: Array<number>, total?: { personalBestsAchieved: number, weight: string, reps: string, distance: string, duration: string, restTime: number } | null, assets?: { images: Array<string>, videos: Array<string> } | null, sets: Array<{ note?: string | null, lot: SetLot, personalBests?: Array<WorkoutSetPersonalBest> | null, confirmedAt?: string | null, statistic: { duration?: string | null, distance?: string | null, reps?: string | null, weight?: string | null, oneRm?: string | null, pace?: string | null, volume?: string | null } }> }> } } } };
+
+export type WorkoutTemplateDetailsQueryVariables = Exact<{
+  workoutTemplateId: Scalars['String']['input'];
+}>;
+
+
+export type WorkoutTemplateDetailsQuery = { workoutTemplateDetails: { collections: Array<{ id: string, name: string, userId: string }>, details: { id: string, name: string, createdOn: string, visibility: Visibility, defaultRestTimer?: number | null, summary: { total?: { personalBestsAchieved: number, weight: string, reps: string, distance: string, duration: string, restTime: number } | null, exercises: Array<{ numSets: number, id: string, lot?: ExerciseLot | null, bestSet?: { lot: SetLot, personalBests?: Array<WorkoutSetPersonalBest> | null, statistic: { duration?: string | null, distance?: string | null, reps?: string | null, weight?: string | null, oneRm?: string | null, pace?: string | null, volume?: string | null } } | null }> }, information: { comment?: string | null, assets?: { images: Array<string>, videos: Array<string> } | null, exercises: Array<{ name: string, lot: ExerciseLot, notes: Array<string>, restTime?: number | null, supersetWith: Array<number>, total?: { personalBestsAchieved: number, weight: string, reps: string, distance: string, duration: string, restTime: number } | null, assets?: { images: Array<string>, videos: Array<string> } | null, sets: Array<{ note?: string | null, lot: SetLot, personalBests?: Array<WorkoutSetPersonalBest> | null, confirmedAt?: string | null, statistic: { duration?: string | null, distance?: string | null, reps?: string | null, weight?: string | null, oneRm?: string | null, pace?: string | null, volume?: string | null } }> }> } } } };
 
 export type GetOidcRedirectUrlQueryVariables = Exact<{ [key: string]: never; }>;
 
@@ -2994,7 +3185,7 @@ export type UserCollectionsListQuery = { userCollectionsList: Array<{ id: string
 export type UserIntegrationsQueryVariables = Exact<{ [key: string]: never; }>;
 
 
-export type UserIntegrationsQuery = { userIntegrations: Array<{ id: string, lot: IntegrationLot, provider: IntegrationProvider, createdOn: string, isDisabled?: boolean | null, maximumProgress?: string | null, minimumProgress?: string | null, lastTriggeredOn?: string | null }> };
+export type UserIntegrationsQuery = { userIntegrations: Array<{ id: string, lot: IntegrationLot, provider: IntegrationProvider, createdOn: string, isDisabled?: boolean | null, maximumProgress?: string | null, minimumProgress?: string | null, lastTriggeredOn?: string | null, syncToOwnedCollection?: boolean | null }> };
 
 export type UserNotificationPlatformsQueryVariables = Exact<{ [key: string]: never; }>;
 
@@ -3008,6 +3199,11 @@ export type UsersListQueryVariables = Exact<{
 
 export type UsersListQuery = { usersList: Array<{ id: string, lot: UserLot, name: string, isDisabled?: boolean | null }> };
 
+export type UserRecommendationsQueryVariables = Exact<{ [key: string]: never; }>;
+
+
+export type UserRecommendationsQuery = { userRecommendations: Array<string> };
+
 export type UserUpcomingCalendarEventsQueryVariables = Exact<{
   input: UserUpcomingCalendarEventInput;
 }>;
@@ -3043,6 +3239,11 @@ export type PeopleListQueryVariables = Exact<{
 
 export type PeopleListQuery = { peopleList: { items: Array<string>, details: { total: number, nextPage?: number | null } } };
 
+export type UserAccessLinksQueryVariables = Exact<{ [key: string]: never; }>;
+
+
+export type UserAccessLinksQuery = { userAccessLinks: Array<{ id: string, name: string, isDemo?: boolean | null, createdOn: string, expiresOn?: string | null, timesUsed: number, isRevoked?: boolean | null, maximumUses?: number | null, isAccountDefault?: boolean | null, isMutationAllowed?: boolean | null }> };
+
 export type DailyUserActivitiesQueryVariables = Exact<{
   input: DailyUserActivitiesInput;
 }>;
@@ -3060,7 +3261,7 @@ export type SeenMangaExtraInformationPartFragment = { chapter?: string | null, v
 
 export type CalendarEventPartFragment = { date: string, metadataId: string, metadataLot: MediaLot, episodeName?: string | null, metadataTitle: string, metadataImage?: string | null, calendarEventId: string, showExtraInformation?: { episode: number, season: number } | null, podcastExtraInformation?: { episode: number } | null, animeExtraInformation?: { episode?: number | null } | null };
 
-export type SeenPartFragment = { id: string, progress: string, providerWatchedOn?: string | null, state: SeenState, startedOn?: string | null, finishedOn?: string | null, lastUpdatedOn: string, numTimesUpdated: number, showExtraInformation?: { episode: number, season: number } | null, podcastExtraInformation?: { episode: number } | null, animeExtraInformation?: { episode?: number | null } | null, mangaExtraInformation?: { chapter?: string | null, volume?: number | null } | null };
+export type SeenPartFragment = { id: string, state: SeenState, progress: string, startedOn?: string | null, finishedOn?: string | null, lastUpdatedOn: string, manualTimeSpent?: string | null, numTimesUpdated: number, providerWatchedOn?: string | null, showExtraInformation?: { episode: number, season: number } | null, podcastExtraInformation?: { episode: number } | null, animeExtraInformation?: { episode?: number | null } | null, mangaExtraInformation?: { chapter?: string | null, volume?: number | null } | null };
 
 export type MetadataSearchItemPartFragment = { identifier: string, title: string, image?: string | null, publishYear?: number | null };
 
@@ -3085,7 +3286,7 @@ export const SeenPodcastExtraInformationPartFragmentDoc = {"kind":"Document","de
 export const SeenAnimeExtraInformationPartFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenAnimeExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenAnimeExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}}]}}]} as unknown as DocumentNode<SeenAnimeExtraInformationPartFragment, unknown>;
 export const CalendarEventPartFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CalendarEventPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"GraphqlCalendarEvent"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"date"}},{"kind":"Field","name":{"kind":"Name","value":"metadataId"}},{"kind":"Field","name":{"kind":"Name","value":"metadataLot"}},{"kind":"Field","name":{"kind":"Name","value":"episodeName"}},{"kind":"Field","name":{"kind":"Name","value":"metadataTitle"}},{"kind":"Field","name":{"kind":"Name","value":"metadataImage"}},{"kind":"Field","name":{"kind":"Name","value":"calendarEventId"}},{"kind":"Field","name":{"kind":"Name","value":"showExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenShowExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"podcastExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenPodcastExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"animeExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenAnimeExtraInformationPart"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenShowExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenShowExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}},{"kind":"Field","name":{"kind":"Name","value":"season"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenPodcastExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenPodcastExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenAnimeExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenAnimeExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}}]}}]} as unknown as DocumentNode<CalendarEventPartFragment, unknown>;
 export const SeenMangaExtraInformationPartFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenMangaExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenMangaExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"chapter"}},{"kind":"Field","name":{"kind":"Name","value":"volume"}}]}}]} as unknown as DocumentNode<SeenMangaExtraInformationPartFragment, unknown>;
-export const SeenPartFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Seen"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"progress"}},{"kind":"Field","name":{"kind":"Name","value":"providerWatchedOn"}},{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"startedOn"}},{"kind":"Field","name":{"kind":"Name","value":"finishedOn"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdatedOn"}},{"kind":"Field","name":{"kind":"Name","value":"numTimesUpdated"}},{"kind":"Field","name":{"kind":"Name","value":"showExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenShowExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"podcastExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenPodcastExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"animeExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenAnimeExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"mangaExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenMangaExtraInformationPart"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenShowExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenShowExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}},{"kind":"Field","name":{"kind":"Name","value":"season"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenPodcastExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenPodcastExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenAnimeExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenAnimeExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenMangaExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenMangaExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"chapter"}},{"kind":"Field","name":{"kind":"Name","value":"volume"}}]}}]} as unknown as DocumentNode<SeenPartFragment, unknown>;
+export const SeenPartFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Seen"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"progress"}},{"kind":"Field","name":{"kind":"Name","value":"startedOn"}},{"kind":"Field","name":{"kind":"Name","value":"finishedOn"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdatedOn"}},{"kind":"Field","name":{"kind":"Name","value":"manualTimeSpent"}},{"kind":"Field","name":{"kind":"Name","value":"numTimesUpdated"}},{"kind":"Field","name":{"kind":"Name","value":"providerWatchedOn"}},{"kind":"Field","name":{"kind":"Name","value":"showExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenShowExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"podcastExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenPodcastExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"animeExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenAnimeExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"mangaExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenMangaExtraInformationPart"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenShowExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenShowExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}},{"kind":"Field","name":{"kind":"Name","value":"season"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenPodcastExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenPodcastExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenAnimeExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenAnimeExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenMangaExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenMangaExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"chapter"}},{"kind":"Field","name":{"kind":"Name","value":"volume"}}]}}]} as unknown as DocumentNode<SeenPartFragment, unknown>;
 export const MetadataSearchItemPartFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"MetadataSearchItemPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"MetadataSearchItem"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"identifier"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"image"}},{"kind":"Field","name":{"kind":"Name","value":"publishYear"}}]}}]} as unknown as DocumentNode<MetadataSearchItemPartFragment, unknown>;
 export const WorkoutOrExerciseTotalsPartFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkoutOrExerciseTotalsPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkoutOrExerciseTotals"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"personalBestsAchieved"}},{"kind":"Field","name":{"kind":"Name","value":"weight"}},{"kind":"Field","name":{"kind":"Name","value":"reps"}},{"kind":"Field","name":{"kind":"Name","value":"distance"}},{"kind":"Field","name":{"kind":"Name","value":"duration"}},{"kind":"Field","name":{"kind":"Name","value":"restTime"}}]}}]} as unknown as DocumentNode<WorkoutOrExerciseTotalsPartFragment, unknown>;
 export const WorkoutSetStatisticPartFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkoutSetStatisticPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkoutSetStatistic"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"duration"}},{"kind":"Field","name":{"kind":"Name","value":"distance"}},{"kind":"Field","name":{"kind":"Name","value":"reps"}},{"kind":"Field","name":{"kind":"Name","value":"weight"}},{"kind":"Field","name":{"kind":"Name","value":"oneRm"}},{"kind":"Field","name":{"kind":"Name","value":"pace"}},{"kind":"Field","name":{"kind":"Name","value":"volume"}}]}}]} as unknown as DocumentNode<WorkoutSetStatisticPartFragment, unknown>;
@@ -3111,6 +3312,7 @@ export const CreateUserMeasurementDocument = {"kind":"Document","definitions":[{
 export const CreateUserNotificationPlatformDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateUserNotificationPlatform"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateUserNotificationPlatformInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createUserNotificationPlatform"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode<CreateUserNotificationPlatformMutation, CreateUserNotificationPlatformMutationVariables>;
 export const CreateUserIntegrationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateUserIntegration"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateUserIntegrationInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createUserIntegration"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode<CreateUserIntegrationMutation, CreateUserIntegrationMutationVariables>;
 export const CreateUserWorkoutDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateUserWorkout"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UserWorkoutInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createUserWorkout"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode<CreateUserWorkoutMutation, CreateUserWorkoutMutationVariables>;
+export const CreateOrUpdateWorkoutTemplateDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateOrUpdateWorkoutTemplate"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UserWorkoutInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createOrUpdateWorkoutTemplate"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode<CreateOrUpdateWorkoutTemplateMutation, CreateOrUpdateWorkoutTemplateMutationVariables>;
 export const DeleteCollectionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteCollection"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"collectionName"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteCollection"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"collectionName"},"value":{"kind":"Variable","name":{"kind":"Name","value":"collectionName"}}}]}]}}]} as unknown as DocumentNode<DeleteCollectionMutation, DeleteCollectionMutationVariables>;
 export const DeleteReviewDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteReview"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"reviewId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteReview"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"reviewId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"reviewId"}}}]}]}}]} as unknown as DocumentNode<DeleteReviewMutation, DeleteReviewMutationVariables>;
 export const DeleteS3ObjectDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteS3Object"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"key"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteS3Object"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"key"},"value":{"kind":"Variable","name":{"kind":"Name","value":"key"}}}]}]}}]} as unknown as DocumentNode<DeleteS3ObjectMutation, DeleteS3ObjectMutationVariables>;
@@ -3120,6 +3322,7 @@ export const DeleteUserIntegrationDocument = {"kind":"Document","definitions":[{
 export const DeleteUserMeasurementDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteUserMeasurement"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"timestamp"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"DateTime"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteUserMeasurement"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"timestamp"},"value":{"kind":"Variable","name":{"kind":"Name","value":"timestamp"}}}]}]}}]} as unknown as DocumentNode<DeleteUserMeasurementMutation, DeleteUserMeasurementMutationVariables>;
 export const DeleteUserNotificationPlatformDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteUserNotificationPlatform"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"notificationId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteUserNotificationPlatform"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"notificationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"notificationId"}}}]}]}}]} as unknown as DocumentNode<DeleteUserNotificationPlatformMutation, DeleteUserNotificationPlatformMutationVariables>;
 export const DeleteUserWorkoutDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteUserWorkout"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workoutId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteUserWorkout"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"workoutId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workoutId"}}}]}]}}]} as unknown as DocumentNode<DeleteUserWorkoutMutation, DeleteUserWorkoutMutationVariables>;
+export const DeleteWorkoutTemplateDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteWorkoutTemplate"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workoutTemplateId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteWorkoutTemplate"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"workoutTemplateId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workoutTemplateId"}}}]}]}}]} as unknown as DocumentNode<DeleteWorkoutTemplateMutation, DeleteWorkoutTemplateMutationVariables>;
 export const DeployBackgroundJobDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeployBackgroundJob"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"jobName"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"BackgroundJob"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deployBackgroundJob"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"jobName"},"value":{"kind":"Variable","name":{"kind":"Name","value":"jobName"}}}]}]}}]} as unknown as DocumentNode<DeployBackgroundJobMutation, DeployBackgroundJobMutationVariables>;
 export const DeployBulkProgressUpdateDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeployBulkProgressUpdate"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ProgressUpdateInput"}}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deployBulkProgressUpdate"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode<DeployBulkProgressUpdateMutation, DeployBulkProgressUpdateMutationVariables>;
 export const DeployExportJobDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeployExportJob"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deployExportJob"}}]}}]} as unknown as DocumentNode<DeployExportJobMutation, DeployExportJobMutationVariables>;
@@ -3139,6 +3342,9 @@ export const RemoveEntityFromCollectionDocument = {"kind":"Document","definition
 export const TestUserNotificationPlatformsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"TestUserNotificationPlatforms"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"testUserNotificationPlatforms"}}]}}]} as unknown as DocumentNode<TestUserNotificationPlatformsMutation, TestUserNotificationPlatformsMutationVariables>;
 export const UpdateUserDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateUser"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateUserInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateUser"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode<UpdateUserMutation, UpdateUserMutationVariables>;
 export const UpdateUserPreferenceDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateUserPreference"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateUserPreferenceInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateUserPreference"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode<UpdateUserPreferenceMutation, UpdateUserPreferenceMutationVariables>;
+export const CreateAccessLinkDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateAccessLink"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateAccessLinkInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createAccessLink"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode<CreateAccessLinkMutation, CreateAccessLinkMutationVariables>;
+export const ProcessAccessLinkDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ProcessAccessLink"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ProcessAccessLinkInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"processAccessLink"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ProcessAccessLinkError"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"error"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ProcessAccessLinkResponse"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"apiKey"}},{"kind":"Field","name":{"kind":"Name","value":"redirectTo"}},{"kind":"Field","name":{"kind":"Name","value":"tokenValidForDays"}}]}}]}}]}}]} as unknown as DocumentNode<ProcessAccessLinkMutation, ProcessAccessLinkMutationVariables>;
+export const RevokeAccessLinkDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"revokeAccessLink"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"accessLinkId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"revokeAccessLink"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"accessLinkId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"accessLinkId"}}}]}]}}]} as unknown as DocumentNode<RevokeAccessLinkMutation, RevokeAccessLinkMutationVariables>;
 export const CollectionContentsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"CollectionContents"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CollectionContentsInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"collectionContents"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"reviews"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ReviewItemPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"results"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"details"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}},{"kind":"Field","name":{"kind":"Name","value":"nextPage"}}]}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"entityId"}},{"kind":"Field","name":{"kind":"Name","value":"entityLot"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"details"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"createdOn"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenShowExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenShowExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}},{"kind":"Field","name":{"kind":"Name","value":"season"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenPodcastExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenPodcastExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenAnimeExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenAnimeExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenMangaExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenMangaExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"chapter"}},{"kind":"Field","name":{"kind":"Name","value":"volume"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ReviewItemPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ReviewItem"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"rating"}},{"kind":"Field","name":{"kind":"Name","value":"textOriginal"}},{"kind":"Field","name":{"kind":"Name","value":"textRendered"}},{"kind":"Field","name":{"kind":"Name","value":"isSpoiler"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"postedOn"}},{"kind":"Field","name":{"kind":"Name","value":"postedBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"comments"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"text"}},{"kind":"Field","name":{"kind":"Name","value":"createdOn"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"likedBy"}}]}},{"kind":"Field","name":{"kind":"Name","value":"showExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenShowExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"podcastExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenPodcastExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"animeExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenAnimeExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"mangaExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenMangaExtraInformationPart"}}]}}]}}]} as unknown as DocumentNode<CollectionContentsQuery, CollectionContentsQueryVariables>;
 export const CoreDetailsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"CoreDetails"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"coreDetails"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"isPro"}},{"kind":"Field","name":{"kind":"Name","value":"version"}},{"kind":"Field","name":{"kind":"Name","value":"docsLink"}},{"kind":"Field","name":{"kind":"Name","value":"pageLimit"}},{"kind":"Field","name":{"kind":"Name","value":"websiteUrl"}},{"kind":"Field","name":{"kind":"Name","value":"smtpEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"oidcEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"signupAllowed"}},{"kind":"Field","name":{"kind":"Name","value":"repositoryLink"}},{"kind":"Field","name":{"kind":"Name","value":"localAuthDisabled"}},{"kind":"Field","name":{"kind":"Name","value":"fileStorageEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"tokenValidForDays"}}]}}]}}]} as unknown as DocumentNode<CoreDetailsQuery, CoreDetailsQueryVariables>;
 export const ExerciseDetailsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ExerciseDetails"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"exerciseId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"exerciseDetails"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"exerciseId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"exerciseId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"source"}},{"kind":"Field","name":{"kind":"Name","value":"level"}},{"kind":"Field","name":{"kind":"Name","value":"force"}},{"kind":"Field","name":{"kind":"Name","value":"mechanic"}},{"kind":"Field","name":{"kind":"Name","value":"equipment"}},{"kind":"Field","name":{"kind":"Name","value":"muscles"}},{"kind":"Field","name":{"kind":"Name","value":"createdByUserId"}},{"kind":"Field","name":{"kind":"Name","value":"attributes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"instructions"}},{"kind":"Field","name":{"kind":"Name","value":"images"}}]}}]}}]}}]} as unknown as DocumentNode<ExerciseDetailsQuery, ExerciseDetailsQueryVariables>;
@@ -3158,12 +3364,14 @@ export const PersonDetailsDocument = {"kind":"Document","definitions":[{"kind":"
 export const UserDetailsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserDetails"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userDetails"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"User"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"oidcIssuerId"}}]}}]}}]}}]} as unknown as DocumentNode<UserDetailsQuery, UserDetailsQueryVariables>;
 export const UserExerciseDetailsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserExerciseDetails"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"exerciseId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userExerciseDetails"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"exerciseId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"exerciseId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"collections"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CollectionPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"reviews"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ReviewItemPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"history"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"idx"}},{"kind":"Field","name":{"kind":"Name","value":"workoutId"}},{"kind":"Field","name":{"kind":"Name","value":"workoutEndOn"}},{"kind":"Field","name":{"kind":"Name","value":"bestSet"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkoutSetRecordPart"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"details"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"exerciseId"}},{"kind":"Field","name":{"kind":"Name","value":"createdOn"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdatedOn"}},{"kind":"Field","name":{"kind":"Name","value":"exerciseNumTimesInteracted"}},{"kind":"Field","name":{"kind":"Name","value":"exerciseExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"lifetimeStats"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"weight"}},{"kind":"Field","name":{"kind":"Name","value":"reps"}},{"kind":"Field","name":{"kind":"Name","value":"distance"}},{"kind":"Field","name":{"kind":"Name","value":"duration"}},{"kind":"Field","name":{"kind":"Name","value":"personalBestsAchieved"}}]}},{"kind":"Field","name":{"kind":"Name","value":"personalBests"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"sets"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workoutId"}},{"kind":"Field","name":{"kind":"Name","value":"exerciseIdx"}},{"kind":"Field","name":{"kind":"Name","value":"setIdx"}}]}}]}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenShowExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenShowExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}},{"kind":"Field","name":{"kind":"Name","value":"season"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenPodcastExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenPodcastExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenAnimeExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenAnimeExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenMangaExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenMangaExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"chapter"}},{"kind":"Field","name":{"kind":"Name","value":"volume"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkoutSetStatisticPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkoutSetStatistic"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"duration"}},{"kind":"Field","name":{"kind":"Name","value":"distance"}},{"kind":"Field","name":{"kind":"Name","value":"reps"}},{"kind":"Field","name":{"kind":"Name","value":"weight"}},{"kind":"Field","name":{"kind":"Name","value":"oneRm"}},{"kind":"Field","name":{"kind":"Name","value":"pace"}},{"kind":"Field","name":{"kind":"Name","value":"volume"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CollectionPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Collection"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"userId"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ReviewItemPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ReviewItem"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"rating"}},{"kind":"Field","name":{"kind":"Name","value":"textOriginal"}},{"kind":"Field","name":{"kind":"Name","value":"textRendered"}},{"kind":"Field","name":{"kind":"Name","value":"isSpoiler"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"postedOn"}},{"kind":"Field","name":{"kind":"Name","value":"postedBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"comments"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"text"}},{"kind":"Field","name":{"kind":"Name","value":"createdOn"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"likedBy"}}]}},{"kind":"Field","name":{"kind":"Name","value":"showExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenShowExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"podcastExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenPodcastExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"animeExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenAnimeExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"mangaExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenMangaExtraInformationPart"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkoutSetRecordPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkoutSetRecord"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"statistic"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkoutSetStatisticPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"personalBests"}}]}}]} as unknown as DocumentNode<UserExerciseDetailsQuery, UserExerciseDetailsQueryVariables>;
 export const UserMeasurementsListDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserMeasurementsList"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UserMeasurementsListInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userMeasurementsList"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"timestamp"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"comment"}},{"kind":"Field","name":{"kind":"Name","value":"stats"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"weight"}},{"kind":"Field","name":{"kind":"Name","value":"bodyMassIndex"}},{"kind":"Field","name":{"kind":"Name","value":"totalBodyWater"}},{"kind":"Field","name":{"kind":"Name","value":"muscle"}},{"kind":"Field","name":{"kind":"Name","value":"leanBodyMass"}},{"kind":"Field","name":{"kind":"Name","value":"bodyFat"}},{"kind":"Field","name":{"kind":"Name","value":"boneMass"}},{"kind":"Field","name":{"kind":"Name","value":"visceralFat"}},{"kind":"Field","name":{"kind":"Name","value":"waistCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"waistToHeightRatio"}},{"kind":"Field","name":{"kind":"Name","value":"hipCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"waistToHipRatio"}},{"kind":"Field","name":{"kind":"Name","value":"chestCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"thighCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"bicepsCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"neckCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"bodyFatCaliper"}},{"kind":"Field","name":{"kind":"Name","value":"chestSkinfold"}},{"kind":"Field","name":{"kind":"Name","value":"abdominalSkinfold"}},{"kind":"Field","name":{"kind":"Name","value":"thighSkinfold"}},{"kind":"Field","name":{"kind":"Name","value":"basalMetabolicRate"}},{"kind":"Field","name":{"kind":"Name","value":"totalDailyEnergyExpenditure"}},{"kind":"Field","name":{"kind":"Name","value":"calories"}},{"kind":"Field","name":{"kind":"Name","value":"custom"}}]}}]}}]}}]} as unknown as DocumentNode<UserMeasurementsListQuery, UserMeasurementsListQueryVariables>;
-export const UserMetadataDetailsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserMetadataDetails"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"metadataId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userMetadataDetails"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"metadataId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"metadataId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"mediaReason"}},{"kind":"Field","name":{"kind":"Name","value":"hasInteracted"}},{"kind":"Field","name":{"kind":"Name","value":"collections"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CollectionPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"inProgress"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"history"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"averageRating"}},{"kind":"Field","name":{"kind":"Name","value":"reviews"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ReviewItemPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"seenByAllCount"}},{"kind":"Field","name":{"kind":"Name","value":"seenByUserCount"}},{"kind":"Field","name":{"kind":"Name","value":"nextEntry"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"season"}},{"kind":"Field","name":{"kind":"Name","value":"volume"}},{"kind":"Field","name":{"kind":"Name","value":"episode"}},{"kind":"Field","name":{"kind":"Name","value":"chapter"}}]}},{"kind":"Field","name":{"kind":"Name","value":"showProgress"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"timesSeen"}},{"kind":"Field","name":{"kind":"Name","value":"seasonNumber"}},{"kind":"Field","name":{"kind":"Name","value":"episodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episodeNumber"}},{"kind":"Field","name":{"kind":"Name","value":"timesSeen"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"podcastProgress"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episodeNumber"}},{"kind":"Field","name":{"kind":"Name","value":"timesSeen"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenShowExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenShowExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}},{"kind":"Field","name":{"kind":"Name","value":"season"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenPodcastExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenPodcastExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenAnimeExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenAnimeExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenMangaExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenMangaExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"chapter"}},{"kind":"Field","name":{"kind":"Name","value":"volume"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CollectionPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Collection"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"userId"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Seen"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"progress"}},{"kind":"Field","name":{"kind":"Name","value":"providerWatchedOn"}},{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"startedOn"}},{"kind":"Field","name":{"kind":"Name","value":"finishedOn"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdatedOn"}},{"kind":"Field","name":{"kind":"Name","value":"numTimesUpdated"}},{"kind":"Field","name":{"kind":"Name","value":"showExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenShowExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"podcastExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenPodcastExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"animeExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenAnimeExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"mangaExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenMangaExtraInformationPart"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ReviewItemPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ReviewItem"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"rating"}},{"kind":"Field","name":{"kind":"Name","value":"textOriginal"}},{"kind":"Field","name":{"kind":"Name","value":"textRendered"}},{"kind":"Field","name":{"kind":"Name","value":"isSpoiler"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"postedOn"}},{"kind":"Field","name":{"kind":"Name","value":"postedBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"comments"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"text"}},{"kind":"Field","name":{"kind":"Name","value":"createdOn"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"likedBy"}}]}},{"kind":"Field","name":{"kind":"Name","value":"showExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenShowExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"podcastExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenPodcastExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"animeExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenAnimeExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"mangaExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenMangaExtraInformationPart"}}]}}]}}]} as unknown as DocumentNode<UserMetadataDetailsQuery, UserMetadataDetailsQueryVariables>;
+export const UserMetadataDetailsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserMetadataDetails"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"metadataId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userMetadataDetails"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"metadataId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"metadataId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"mediaReason"}},{"kind":"Field","name":{"kind":"Name","value":"hasInteracted"}},{"kind":"Field","name":{"kind":"Name","value":"collections"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CollectionPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"inProgress"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"history"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"averageRating"}},{"kind":"Field","name":{"kind":"Name","value":"reviews"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ReviewItemPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"seenByAllCount"}},{"kind":"Field","name":{"kind":"Name","value":"seenByUserCount"}},{"kind":"Field","name":{"kind":"Name","value":"nextEntry"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"season"}},{"kind":"Field","name":{"kind":"Name","value":"volume"}},{"kind":"Field","name":{"kind":"Name","value":"episode"}},{"kind":"Field","name":{"kind":"Name","value":"chapter"}}]}},{"kind":"Field","name":{"kind":"Name","value":"showProgress"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"timesSeen"}},{"kind":"Field","name":{"kind":"Name","value":"seasonNumber"}},{"kind":"Field","name":{"kind":"Name","value":"episodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episodeNumber"}},{"kind":"Field","name":{"kind":"Name","value":"timesSeen"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"podcastProgress"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episodeNumber"}},{"kind":"Field","name":{"kind":"Name","value":"timesSeen"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenShowExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenShowExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}},{"kind":"Field","name":{"kind":"Name","value":"season"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenPodcastExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenPodcastExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenAnimeExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenAnimeExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenMangaExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenMangaExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"chapter"}},{"kind":"Field","name":{"kind":"Name","value":"volume"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CollectionPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Collection"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"userId"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Seen"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"progress"}},{"kind":"Field","name":{"kind":"Name","value":"startedOn"}},{"kind":"Field","name":{"kind":"Name","value":"finishedOn"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdatedOn"}},{"kind":"Field","name":{"kind":"Name","value":"manualTimeSpent"}},{"kind":"Field","name":{"kind":"Name","value":"numTimesUpdated"}},{"kind":"Field","name":{"kind":"Name","value":"providerWatchedOn"}},{"kind":"Field","name":{"kind":"Name","value":"showExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenShowExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"podcastExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenPodcastExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"animeExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenAnimeExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"mangaExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenMangaExtraInformationPart"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ReviewItemPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ReviewItem"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"rating"}},{"kind":"Field","name":{"kind":"Name","value":"textOriginal"}},{"kind":"Field","name":{"kind":"Name","value":"textRendered"}},{"kind":"Field","name":{"kind":"Name","value":"isSpoiler"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"postedOn"}},{"kind":"Field","name":{"kind":"Name","value":"postedBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"comments"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"text"}},{"kind":"Field","name":{"kind":"Name","value":"createdOn"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"likedBy"}}]}},{"kind":"Field","name":{"kind":"Name","value":"showExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenShowExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"podcastExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenPodcastExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"animeExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenAnimeExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"mangaExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenMangaExtraInformationPart"}}]}}]}}]} as unknown as DocumentNode<UserMetadataDetailsQuery, UserMetadataDetailsQueryVariables>;
 export const UserMetadataGroupDetailsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserMetadataGroupDetails"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"metadataGroupId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userMetadataGroupDetails"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"metadataGroupId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"metadataGroupId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"reviews"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ReviewItemPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"collections"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CollectionPart"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenShowExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenShowExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}},{"kind":"Field","name":{"kind":"Name","value":"season"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenPodcastExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenPodcastExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenAnimeExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenAnimeExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenMangaExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenMangaExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"chapter"}},{"kind":"Field","name":{"kind":"Name","value":"volume"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ReviewItemPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ReviewItem"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"rating"}},{"kind":"Field","name":{"kind":"Name","value":"textOriginal"}},{"kind":"Field","name":{"kind":"Name","value":"textRendered"}},{"kind":"Field","name":{"kind":"Name","value":"isSpoiler"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"postedOn"}},{"kind":"Field","name":{"kind":"Name","value":"postedBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"comments"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"text"}},{"kind":"Field","name":{"kind":"Name","value":"createdOn"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"likedBy"}}]}},{"kind":"Field","name":{"kind":"Name","value":"showExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenShowExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"podcastExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenPodcastExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"animeExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenAnimeExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"mangaExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenMangaExtraInformationPart"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CollectionPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Collection"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"userId"}}]}}]} as unknown as DocumentNode<UserMetadataGroupDetailsQuery, UserMetadataGroupDetailsQueryVariables>;
 export const UserPersonDetailsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserPersonDetails"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"personId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userPersonDetails"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"personId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"personId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"collections"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CollectionPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"reviews"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ReviewItemPart"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenShowExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenShowExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}},{"kind":"Field","name":{"kind":"Name","value":"season"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenPodcastExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenPodcastExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenAnimeExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenAnimeExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenMangaExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenMangaExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"chapter"}},{"kind":"Field","name":{"kind":"Name","value":"volume"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CollectionPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Collection"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"userId"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ReviewItemPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ReviewItem"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"rating"}},{"kind":"Field","name":{"kind":"Name","value":"textOriginal"}},{"kind":"Field","name":{"kind":"Name","value":"textRendered"}},{"kind":"Field","name":{"kind":"Name","value":"isSpoiler"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"postedOn"}},{"kind":"Field","name":{"kind":"Name","value":"postedBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"comments"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"text"}},{"kind":"Field","name":{"kind":"Name","value":"createdOn"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"likedBy"}}]}},{"kind":"Field","name":{"kind":"Name","value":"showExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenShowExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"podcastExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenPodcastExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"animeExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenAnimeExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"mangaExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenMangaExtraInformationPart"}}]}}]}}]} as unknown as DocumentNode<UserPersonDetailsQuery, UserPersonDetailsQueryVariables>;
-export const UserPreferencesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserPreferences"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userPreferences"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"general"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"reviewScale"}},{"kind":"Field","name":{"kind":"Name","value":"displayNsfw"}},{"kind":"Field","name":{"kind":"Name","value":"disableVideos"}},{"kind":"Field","name":{"kind":"Name","value":"persistQueries"}},{"kind":"Field","name":{"kind":"Name","value":"watchProviders"}},{"kind":"Field","name":{"kind":"Name","value":"disableReviews"}},{"kind":"Field","name":{"kind":"Name","value":"disableIntegrations"}},{"kind":"Field","name":{"kind":"Name","value":"disableWatchProviders"}},{"kind":"Field","name":{"kind":"Name","value":"disableNavigationAnimation"}},{"kind":"Field","name":{"kind":"Name","value":"dashboard"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"section"}},{"kind":"Field","name":{"kind":"Name","value":"hidden"}},{"kind":"Field","name":{"kind":"Name","value":"numElements"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"fitness"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"measurements"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"custom"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"dataType"}}]}},{"kind":"Field","name":{"kind":"Name","value":"inbuilt"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"weight"}},{"kind":"Field","name":{"kind":"Name","value":"bodyMassIndex"}},{"kind":"Field","name":{"kind":"Name","value":"totalBodyWater"}},{"kind":"Field","name":{"kind":"Name","value":"muscle"}},{"kind":"Field","name":{"kind":"Name","value":"leanBodyMass"}},{"kind":"Field","name":{"kind":"Name","value":"bodyFat"}},{"kind":"Field","name":{"kind":"Name","value":"boneMass"}},{"kind":"Field","name":{"kind":"Name","value":"visceralFat"}},{"kind":"Field","name":{"kind":"Name","value":"waistCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"waistToHeightRatio"}},{"kind":"Field","name":{"kind":"Name","value":"hipCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"waistToHipRatio"}},{"kind":"Field","name":{"kind":"Name","value":"chestCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"thighCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"bicepsCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"neckCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"bodyFatCaliper"}},{"kind":"Field","name":{"kind":"Name","value":"chestSkinfold"}},{"kind":"Field","name":{"kind":"Name","value":"abdominalSkinfold"}},{"kind":"Field","name":{"kind":"Name","value":"thighSkinfold"}},{"kind":"Field","name":{"kind":"Name","value":"basalMetabolicRate"}},{"kind":"Field","name":{"kind":"Name","value":"totalDailyEnergyExpenditure"}},{"kind":"Field","name":{"kind":"Name","value":"calories"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"exercises"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"unitSystem"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"notifications"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"toSend"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}}]}},{"kind":"Field","name":{"kind":"Name","value":"featuresEnabled"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"others"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"calendar"}},{"kind":"Field","name":{"kind":"Name","value":"collections"}}]}},{"kind":"Field","name":{"kind":"Name","value":"fitness"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"workouts"}},{"kind":"Field","name":{"kind":"Name","value":"measurements"}}]}},{"kind":"Field","name":{"kind":"Name","value":"media"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"anime"}},{"kind":"Field","name":{"kind":"Name","value":"audioBook"}},{"kind":"Field","name":{"kind":"Name","value":"book"}},{"kind":"Field","name":{"kind":"Name","value":"manga"}},{"kind":"Field","name":{"kind":"Name","value":"movie"}},{"kind":"Field","name":{"kind":"Name","value":"podcast"}},{"kind":"Field","name":{"kind":"Name","value":"show"}},{"kind":"Field","name":{"kind":"Name","value":"videoGame"}},{"kind":"Field","name":{"kind":"Name","value":"visualNovel"}},{"kind":"Field","name":{"kind":"Name","value":"people"}},{"kind":"Field","name":{"kind":"Name","value":"groups"}},{"kind":"Field","name":{"kind":"Name","value":"genres"}}]}}]}}]}}]}}]} as unknown as DocumentNode<UserPreferencesQuery, UserPreferencesQueryVariables>;
+export const UserPreferencesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserPreferences"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userPreferences"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"general"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"reviewScale"}},{"kind":"Field","name":{"kind":"Name","value":"displayNsfw"}},{"kind":"Field","name":{"kind":"Name","value":"disableVideos"}},{"kind":"Field","name":{"kind":"Name","value":"persistQueries"}},{"kind":"Field","name":{"kind":"Name","value":"watchProviders"}},{"kind":"Field","name":{"kind":"Name","value":"disableReviews"}},{"kind":"Field","name":{"kind":"Name","value":"disableIntegrations"}},{"kind":"Field","name":{"kind":"Name","value":"disableWatchProviders"}},{"kind":"Field","name":{"kind":"Name","value":"disableNavigationAnimation"}},{"kind":"Field","name":{"kind":"Name","value":"dashboard"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"section"}},{"kind":"Field","name":{"kind":"Name","value":"hidden"}},{"kind":"Field","name":{"kind":"Name","value":"numElements"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"fitness"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"measurements"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"custom"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"dataType"}}]}},{"kind":"Field","name":{"kind":"Name","value":"inbuilt"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"weight"}},{"kind":"Field","name":{"kind":"Name","value":"bodyMassIndex"}},{"kind":"Field","name":{"kind":"Name","value":"totalBodyWater"}},{"kind":"Field","name":{"kind":"Name","value":"muscle"}},{"kind":"Field","name":{"kind":"Name","value":"leanBodyMass"}},{"kind":"Field","name":{"kind":"Name","value":"bodyFat"}},{"kind":"Field","name":{"kind":"Name","value":"boneMass"}},{"kind":"Field","name":{"kind":"Name","value":"visceralFat"}},{"kind":"Field","name":{"kind":"Name","value":"waistCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"waistToHeightRatio"}},{"kind":"Field","name":{"kind":"Name","value":"hipCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"waistToHipRatio"}},{"kind":"Field","name":{"kind":"Name","value":"chestCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"thighCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"bicepsCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"neckCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"bodyFatCaliper"}},{"kind":"Field","name":{"kind":"Name","value":"chestSkinfold"}},{"kind":"Field","name":{"kind":"Name","value":"abdominalSkinfold"}},{"kind":"Field","name":{"kind":"Name","value":"thighSkinfold"}},{"kind":"Field","name":{"kind":"Name","value":"basalMetabolicRate"}},{"kind":"Field","name":{"kind":"Name","value":"totalDailyEnergyExpenditure"}},{"kind":"Field","name":{"kind":"Name","value":"calories"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"exercises"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"unitSystem"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"notifications"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"toSend"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}}]}},{"kind":"Field","name":{"kind":"Name","value":"featuresEnabled"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"others"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"calendar"}},{"kind":"Field","name":{"kind":"Name","value":"collections"}}]}},{"kind":"Field","name":{"kind":"Name","value":"fitness"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"workouts"}},{"kind":"Field","name":{"kind":"Name","value":"templates"}},{"kind":"Field","name":{"kind":"Name","value":"measurements"}}]}},{"kind":"Field","name":{"kind":"Name","value":"media"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"anime"}},{"kind":"Field","name":{"kind":"Name","value":"audioBook"}},{"kind":"Field","name":{"kind":"Name","value":"book"}},{"kind":"Field","name":{"kind":"Name","value":"manga"}},{"kind":"Field","name":{"kind":"Name","value":"movie"}},{"kind":"Field","name":{"kind":"Name","value":"podcast"}},{"kind":"Field","name":{"kind":"Name","value":"show"}},{"kind":"Field","name":{"kind":"Name","value":"videoGame"}},{"kind":"Field","name":{"kind":"Name","value":"visualNovel"}},{"kind":"Field","name":{"kind":"Name","value":"people"}},{"kind":"Field","name":{"kind":"Name","value":"groups"}},{"kind":"Field","name":{"kind":"Name","value":"genres"}}]}}]}}]}}]}}]} as unknown as DocumentNode<UserPreferencesQuery, UserPreferencesQueryVariables>;
+export const UserWorkoutTemplatesListDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserWorkoutTemplatesList"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SearchInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userWorkoutTemplatesList"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"details"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}},{"kind":"Field","name":{"kind":"Name","value":"nextPage"}}]}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"createdOn"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"summary"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkoutSummaryPart"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkoutOrExerciseTotalsPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkoutOrExerciseTotals"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"personalBestsAchieved"}},{"kind":"Field","name":{"kind":"Name","value":"weight"}},{"kind":"Field","name":{"kind":"Name","value":"reps"}},{"kind":"Field","name":{"kind":"Name","value":"distance"}},{"kind":"Field","name":{"kind":"Name","value":"duration"}},{"kind":"Field","name":{"kind":"Name","value":"restTime"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkoutSetStatisticPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkoutSetStatistic"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"duration"}},{"kind":"Field","name":{"kind":"Name","value":"distance"}},{"kind":"Field","name":{"kind":"Name","value":"reps"}},{"kind":"Field","name":{"kind":"Name","value":"weight"}},{"kind":"Field","name":{"kind":"Name","value":"oneRm"}},{"kind":"Field","name":{"kind":"Name","value":"pace"}},{"kind":"Field","name":{"kind":"Name","value":"volume"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkoutSetRecordPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkoutSetRecord"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"statistic"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkoutSetStatisticPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"personalBests"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkoutSummaryPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkoutSummary"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkoutOrExerciseTotalsPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"exercises"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"numSets"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"bestSet"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkoutSetRecordPart"}}]}}]}}]}}]} as unknown as DocumentNode<UserWorkoutTemplatesListQuery, UserWorkoutTemplatesListQueryVariables>;
 export const UserWorkoutsListDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserWorkoutsList"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SearchInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userWorkoutsList"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"details"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}},{"kind":"Field","name":{"kind":"Name","value":"nextPage"}}]}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"endTime"}},{"kind":"Field","name":{"kind":"Name","value":"duration"}},{"kind":"Field","name":{"kind":"Name","value":"startTime"}},{"kind":"Field","name":{"kind":"Name","value":"summary"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkoutSummaryPart"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkoutOrExerciseTotalsPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkoutOrExerciseTotals"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"personalBestsAchieved"}},{"kind":"Field","name":{"kind":"Name","value":"weight"}},{"kind":"Field","name":{"kind":"Name","value":"reps"}},{"kind":"Field","name":{"kind":"Name","value":"distance"}},{"kind":"Field","name":{"kind":"Name","value":"duration"}},{"kind":"Field","name":{"kind":"Name","value":"restTime"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkoutSetStatisticPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkoutSetStatistic"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"duration"}},{"kind":"Field","name":{"kind":"Name","value":"distance"}},{"kind":"Field","name":{"kind":"Name","value":"reps"}},{"kind":"Field","name":{"kind":"Name","value":"weight"}},{"kind":"Field","name":{"kind":"Name","value":"oneRm"}},{"kind":"Field","name":{"kind":"Name","value":"pace"}},{"kind":"Field","name":{"kind":"Name","value":"volume"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkoutSetRecordPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkoutSetRecord"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"statistic"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkoutSetStatisticPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"personalBests"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkoutSummaryPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkoutSummary"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkoutOrExerciseTotalsPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"exercises"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"numSets"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"bestSet"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkoutSetRecordPart"}}]}}]}}]}}]} as unknown as DocumentNode<UserWorkoutsListQuery, UserWorkoutsListQueryVariables>;
-export const WorkoutDetailsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"WorkoutDetails"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workoutId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workoutDetails"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"workoutId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workoutId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"collections"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CollectionPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"details"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"endTime"}},{"kind":"Field","name":{"kind":"Name","value":"duration"}},{"kind":"Field","name":{"kind":"Name","value":"startTime"}},{"kind":"Field","name":{"kind":"Name","value":"repeatedFrom"}},{"kind":"Field","name":{"kind":"Name","value":"summary"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkoutSummaryPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"information"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkoutInformationPart"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkoutOrExerciseTotalsPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkoutOrExerciseTotals"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"personalBestsAchieved"}},{"kind":"Field","name":{"kind":"Name","value":"weight"}},{"kind":"Field","name":{"kind":"Name","value":"reps"}},{"kind":"Field","name":{"kind":"Name","value":"distance"}},{"kind":"Field","name":{"kind":"Name","value":"duration"}},{"kind":"Field","name":{"kind":"Name","value":"restTime"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkoutSetStatisticPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkoutSetStatistic"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"duration"}},{"kind":"Field","name":{"kind":"Name","value":"distance"}},{"kind":"Field","name":{"kind":"Name","value":"reps"}},{"kind":"Field","name":{"kind":"Name","value":"weight"}},{"kind":"Field","name":{"kind":"Name","value":"oneRm"}},{"kind":"Field","name":{"kind":"Name","value":"pace"}},{"kind":"Field","name":{"kind":"Name","value":"volume"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkoutSetRecordPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkoutSetRecord"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"statistic"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkoutSetStatisticPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"personalBests"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"EntityAssetsPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"EntityAssets"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"images"}},{"kind":"Field","name":{"kind":"Name","value":"videos"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CollectionPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Collection"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"userId"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkoutSummaryPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkoutSummary"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkoutOrExerciseTotalsPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"exercises"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"numSets"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"bestSet"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkoutSetRecordPart"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkoutInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkoutInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"comment"}},{"kind":"Field","name":{"kind":"Name","value":"assets"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"EntityAssetsPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"exercises"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"notes"}},{"kind":"Field","name":{"kind":"Name","value":"restTime"}},{"kind":"Field","name":{"kind":"Name","value":"total"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkoutOrExerciseTotalsPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"supersetWith"}},{"kind":"Field","name":{"kind":"Name","value":"assets"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"EntityAssetsPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"sets"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"statistic"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkoutSetStatisticPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"note"}},{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"personalBests"}},{"kind":"Field","name":{"kind":"Name","value":"confirmedAt"}}]}}]}}]}}]} as unknown as DocumentNode<WorkoutDetailsQuery, WorkoutDetailsQueryVariables>;
+export const WorkoutDetailsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"WorkoutDetails"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workoutId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workoutDetails"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"workoutId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workoutId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"collections"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CollectionPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"details"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"endTime"}},{"kind":"Field","name":{"kind":"Name","value":"duration"}},{"kind":"Field","name":{"kind":"Name","value":"startTime"}},{"kind":"Field","name":{"kind":"Name","value":"templateId"}},{"kind":"Field","name":{"kind":"Name","value":"repeatedFrom"}},{"kind":"Field","name":{"kind":"Name","value":"summary"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkoutSummaryPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"information"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkoutInformationPart"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkoutOrExerciseTotalsPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkoutOrExerciseTotals"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"personalBestsAchieved"}},{"kind":"Field","name":{"kind":"Name","value":"weight"}},{"kind":"Field","name":{"kind":"Name","value":"reps"}},{"kind":"Field","name":{"kind":"Name","value":"distance"}},{"kind":"Field","name":{"kind":"Name","value":"duration"}},{"kind":"Field","name":{"kind":"Name","value":"restTime"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkoutSetStatisticPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkoutSetStatistic"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"duration"}},{"kind":"Field","name":{"kind":"Name","value":"distance"}},{"kind":"Field","name":{"kind":"Name","value":"reps"}},{"kind":"Field","name":{"kind":"Name","value":"weight"}},{"kind":"Field","name":{"kind":"Name","value":"oneRm"}},{"kind":"Field","name":{"kind":"Name","value":"pace"}},{"kind":"Field","name":{"kind":"Name","value":"volume"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkoutSetRecordPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkoutSetRecord"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"statistic"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkoutSetStatisticPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"personalBests"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"EntityAssetsPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"EntityAssets"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"images"}},{"kind":"Field","name":{"kind":"Name","value":"videos"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CollectionPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Collection"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"userId"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkoutSummaryPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkoutSummary"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkoutOrExerciseTotalsPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"exercises"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"numSets"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"bestSet"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkoutSetRecordPart"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkoutInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkoutInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"comment"}},{"kind":"Field","name":{"kind":"Name","value":"assets"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"EntityAssetsPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"exercises"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"notes"}},{"kind":"Field","name":{"kind":"Name","value":"restTime"}},{"kind":"Field","name":{"kind":"Name","value":"total"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkoutOrExerciseTotalsPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"supersetWith"}},{"kind":"Field","name":{"kind":"Name","value":"assets"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"EntityAssetsPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"sets"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"statistic"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkoutSetStatisticPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"note"}},{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"personalBests"}},{"kind":"Field","name":{"kind":"Name","value":"confirmedAt"}}]}}]}}]}}]} as unknown as DocumentNode<WorkoutDetailsQuery, WorkoutDetailsQueryVariables>;
+export const WorkoutTemplateDetailsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"WorkoutTemplateDetails"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workoutTemplateId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workoutTemplateDetails"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"workoutTemplateId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workoutTemplateId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"collections"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CollectionPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"details"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"createdOn"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"defaultRestTimer"}},{"kind":"Field","name":{"kind":"Name","value":"summary"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkoutSummaryPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"information"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkoutInformationPart"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkoutOrExerciseTotalsPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkoutOrExerciseTotals"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"personalBestsAchieved"}},{"kind":"Field","name":{"kind":"Name","value":"weight"}},{"kind":"Field","name":{"kind":"Name","value":"reps"}},{"kind":"Field","name":{"kind":"Name","value":"distance"}},{"kind":"Field","name":{"kind":"Name","value":"duration"}},{"kind":"Field","name":{"kind":"Name","value":"restTime"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkoutSetStatisticPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkoutSetStatistic"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"duration"}},{"kind":"Field","name":{"kind":"Name","value":"distance"}},{"kind":"Field","name":{"kind":"Name","value":"reps"}},{"kind":"Field","name":{"kind":"Name","value":"weight"}},{"kind":"Field","name":{"kind":"Name","value":"oneRm"}},{"kind":"Field","name":{"kind":"Name","value":"pace"}},{"kind":"Field","name":{"kind":"Name","value":"volume"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkoutSetRecordPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkoutSetRecord"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"statistic"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkoutSetStatisticPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"personalBests"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"EntityAssetsPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"EntityAssets"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"images"}},{"kind":"Field","name":{"kind":"Name","value":"videos"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CollectionPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Collection"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"userId"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkoutSummaryPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkoutSummary"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkoutOrExerciseTotalsPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"exercises"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"numSets"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"bestSet"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkoutSetRecordPart"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkoutInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkoutInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"comment"}},{"kind":"Field","name":{"kind":"Name","value":"assets"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"EntityAssetsPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"exercises"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"notes"}},{"kind":"Field","name":{"kind":"Name","value":"restTime"}},{"kind":"Field","name":{"kind":"Name","value":"total"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkoutOrExerciseTotalsPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"supersetWith"}},{"kind":"Field","name":{"kind":"Name","value":"assets"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"EntityAssetsPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"sets"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"statistic"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkoutSetStatisticPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"note"}},{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"personalBests"}},{"kind":"Field","name":{"kind":"Name","value":"confirmedAt"}}]}}]}}]}}]} as unknown as DocumentNode<WorkoutTemplateDetailsQuery, WorkoutTemplateDetailsQueryVariables>;
 export const GetOidcRedirectUrlDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetOidcRedirectUrl"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getOidcRedirectUrl"}}]}}]} as unknown as DocumentNode<GetOidcRedirectUrlQuery, GetOidcRedirectUrlQueryVariables>;
 export const UserByOidcIssuerIdDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserByOidcIssuerId"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"oidcIssuerId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userByOidcIssuerId"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"oidcIssuerId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"oidcIssuerId"}}}]}]}}]} as unknown as DocumentNode<UserByOidcIssuerIdQuery, UserByOidcIssuerIdQueryVariables>;
 export const GetOidcTokenDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetOidcToken"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"code"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getOidcToken"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"code"},"value":{"kind":"Variable","name":{"kind":"Name","value":"code"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subject"}},{"kind":"Field","name":{"kind":"Name","value":"email"}}]}}]}}]} as unknown as DocumentNode<GetOidcTokenQuery, GetOidcTokenQueryVariables>;
@@ -3171,12 +3379,14 @@ export const GetPresignedS3UrlDocument = {"kind":"Document","definitions":[{"kin
 export const ProvidersLanguageInformationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ProvidersLanguageInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"providersLanguageInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"supported"}},{"kind":"Field","name":{"kind":"Name","value":"default"}},{"kind":"Field","name":{"kind":"Name","value":"source"}}]}}]}}]} as unknown as DocumentNode<ProvidersLanguageInformationQuery, ProvidersLanguageInformationQueryVariables>;
 export const UserExportsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserExports"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userExports"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"size"}},{"kind":"Field","name":{"kind":"Name","value":"endedAt"}},{"kind":"Field","name":{"kind":"Name","value":"startedAt"}}]}}]}}]} as unknown as DocumentNode<UserExportsQuery, UserExportsQueryVariables>;
 export const UserCollectionsListDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserCollectionsList"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"name"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userCollectionsList"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"name"},"value":{"kind":"Variable","name":{"kind":"Name","value":"name"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"count"}},{"kind":"Field","name":{"kind":"Name","value":"isDefault"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"creator"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"collaborators"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"informationTemplate"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"required"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"defaultValue"}}]}}]}}]}}]} as unknown as DocumentNode<UserCollectionsListQuery, UserCollectionsListQueryVariables>;
-export const UserIntegrationsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserIntegrations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userIntegrations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"provider"}},{"kind":"Field","name":{"kind":"Name","value":"createdOn"}},{"kind":"Field","name":{"kind":"Name","value":"isDisabled"}},{"kind":"Field","name":{"kind":"Name","value":"maximumProgress"}},{"kind":"Field","name":{"kind":"Name","value":"minimumProgress"}},{"kind":"Field","name":{"kind":"Name","value":"lastTriggeredOn"}}]}}]}}]} as unknown as DocumentNode<UserIntegrationsQuery, UserIntegrationsQueryVariables>;
+export const UserIntegrationsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserIntegrations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userIntegrations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"provider"}},{"kind":"Field","name":{"kind":"Name","value":"createdOn"}},{"kind":"Field","name":{"kind":"Name","value":"isDisabled"}},{"kind":"Field","name":{"kind":"Name","value":"maximumProgress"}},{"kind":"Field","name":{"kind":"Name","value":"minimumProgress"}},{"kind":"Field","name":{"kind":"Name","value":"lastTriggeredOn"}},{"kind":"Field","name":{"kind":"Name","value":"syncToOwnedCollection"}}]}}]}}]} as unknown as DocumentNode<UserIntegrationsQuery, UserIntegrationsQueryVariables>;
 export const UserNotificationPlatformsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserNotificationPlatforms"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userNotificationPlatforms"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"createdOn"}},{"kind":"Field","name":{"kind":"Name","value":"isDisabled"}},{"kind":"Field","name":{"kind":"Name","value":"description"}}]}}]}}]} as unknown as DocumentNode<UserNotificationPlatformsQuery, UserNotificationPlatformsQueryVariables>;
 export const UsersListDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UsersList"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"query"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"usersList"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"query"},"value":{"kind":"Variable","name":{"kind":"Name","value":"query"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"isDisabled"}}]}}]}}]} as unknown as DocumentNode<UsersListQuery, UsersListQueryVariables>;
+export const UserRecommendationsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserRecommendations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userRecommendations"}}]}}]} as unknown as DocumentNode<UserRecommendationsQuery, UserRecommendationsQueryVariables>;
 export const UserUpcomingCalendarEventsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserUpcomingCalendarEvents"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UserUpcomingCalendarEventInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userUpcomingCalendarEvents"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CalendarEventPart"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenShowExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenShowExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}},{"kind":"Field","name":{"kind":"Name","value":"season"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenPodcastExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenPodcastExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenAnimeExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenAnimeExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CalendarEventPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"GraphqlCalendarEvent"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"date"}},{"kind":"Field","name":{"kind":"Name","value":"metadataId"}},{"kind":"Field","name":{"kind":"Name","value":"metadataLot"}},{"kind":"Field","name":{"kind":"Name","value":"episodeName"}},{"kind":"Field","name":{"kind":"Name","value":"metadataTitle"}},{"kind":"Field","name":{"kind":"Name","value":"metadataImage"}},{"kind":"Field","name":{"kind":"Name","value":"calendarEventId"}},{"kind":"Field","name":{"kind":"Name","value":"showExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenShowExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"podcastExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenPodcastExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"animeExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenAnimeExtraInformationPart"}}]}}]}}]} as unknown as DocumentNode<UserUpcomingCalendarEventsQuery, UserUpcomingCalendarEventsQueryVariables>;
 export const UserCalendarEventsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserCalendarEvents"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UserCalendarEventInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userCalendarEvents"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"date"}},{"kind":"Field","name":{"kind":"Name","value":"events"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CalendarEventPart"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenShowExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenShowExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}},{"kind":"Field","name":{"kind":"Name","value":"season"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenPodcastExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenPodcastExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenAnimeExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenAnimeExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CalendarEventPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"GraphqlCalendarEvent"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"date"}},{"kind":"Field","name":{"kind":"Name","value":"metadataId"}},{"kind":"Field","name":{"kind":"Name","value":"metadataLot"}},{"kind":"Field","name":{"kind":"Name","value":"episodeName"}},{"kind":"Field","name":{"kind":"Name","value":"metadataTitle"}},{"kind":"Field","name":{"kind":"Name","value":"metadataImage"}},{"kind":"Field","name":{"kind":"Name","value":"calendarEventId"}},{"kind":"Field","name":{"kind":"Name","value":"showExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenShowExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"podcastExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenPodcastExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"animeExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenAnimeExtraInformationPart"}}]}}]}}]} as unknown as DocumentNode<UserCalendarEventsQuery, UserCalendarEventsQueryVariables>;
 export const MetadataPartialDetailsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MetadataPartialDetails"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"metadataId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"metadataPartialDetails"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"metadataId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"metadataId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"image"}},{"kind":"Field","name":{"kind":"Name","value":"publishYear"}}]}}]}}]} as unknown as DocumentNode<MetadataPartialDetailsQuery, MetadataPartialDetailsQueryVariables>;
 export const MetadataGroupsListDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MetadataGroupsList"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"MetadataGroupsListInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"metadataGroupsList"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"details"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}},{"kind":"Field","name":{"kind":"Name","value":"nextPage"}}]}},{"kind":"Field","name":{"kind":"Name","value":"items"}}]}}]}}]} as unknown as DocumentNode<MetadataGroupsListQuery, MetadataGroupsListQueryVariables>;
 export const PeopleListDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"PeopleList"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"PeopleListInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"peopleList"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"details"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}},{"kind":"Field","name":{"kind":"Name","value":"nextPage"}}]}},{"kind":"Field","name":{"kind":"Name","value":"items"}}]}}]}}]} as unknown as DocumentNode<PeopleListQuery, PeopleListQueryVariables>;
+export const UserAccessLinksDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserAccessLinks"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userAccessLinks"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"isDemo"}},{"kind":"Field","name":{"kind":"Name","value":"createdOn"}},{"kind":"Field","name":{"kind":"Name","value":"expiresOn"}},{"kind":"Field","name":{"kind":"Name","value":"timesUsed"}},{"kind":"Field","name":{"kind":"Name","value":"isRevoked"}},{"kind":"Field","name":{"kind":"Name","value":"maximumUses"}},{"kind":"Field","name":{"kind":"Name","value":"isAccountDefault"}},{"kind":"Field","name":{"kind":"Name","value":"isMutationAllowed"}}]}}]}}]} as unknown as DocumentNode<UserAccessLinksQuery, UserAccessLinksQueryVariables>;
 export const DailyUserActivitiesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"DailyUserActivities"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"DailyUserActivitiesInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"dailyUserActivities"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"groupedBy"}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"totalDuration"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"day"}},{"kind":"Field","name":{"kind":"Name","value":"totalReviewCount"}},{"kind":"Field","name":{"kind":"Name","value":"workoutCount"}},{"kind":"Field","name":{"kind":"Name","value":"measurementCount"}},{"kind":"Field","name":{"kind":"Name","value":"audioBookCount"}},{"kind":"Field","name":{"kind":"Name","value":"animeCount"}},{"kind":"Field","name":{"kind":"Name","value":"bookCount"}},{"kind":"Field","name":{"kind":"Name","value":"podcastCount"}},{"kind":"Field","name":{"kind":"Name","value":"mangaCount"}},{"kind":"Field","name":{"kind":"Name","value":"showCount"}},{"kind":"Field","name":{"kind":"Name","value":"movieCount"}},{"kind":"Field","name":{"kind":"Name","value":"videoGameCount"}},{"kind":"Field","name":{"kind":"Name","value":"visualNovelCount"}}]}}]}}]}}]} as unknown as DocumentNode<DailyUserActivitiesQuery, DailyUserActivitiesQueryVariables>;
\ No newline at end of file
diff --git a/libs/graphql/src/backend/mutations/combined.gql b/libs/graphql/src/backend/mutations/combined.gql
index 5c1af92eeb..2d87f8f9bd 100644
--- a/libs/graphql/src/backend/mutations/combined.gql
+++ b/libs/graphql/src/backend/mutations/combined.gql
@@ -90,6 +90,10 @@ mutation CreateUserWorkout($input: UserWorkoutInput!) {
   createUserWorkout(input: $input)
 }
 
+mutation CreateOrUpdateWorkoutTemplate($input: UserWorkoutInput!) {
+  createOrUpdateWorkoutTemplate(input: $input)
+}
+
 mutation DeleteCollection($collectionName: String!) {
   deleteCollection(collectionName: $collectionName)
 }
@@ -128,6 +132,10 @@ mutation DeleteUserWorkout($workoutId: String!) {
   deleteUserWorkout(workoutId: $workoutId)
 }
 
+mutation DeleteWorkoutTemplate($workoutTemplateId: String!) {
+  deleteWorkoutTemplate(workoutTemplateId: $workoutTemplateId)
+}
+
 mutation DeployBackgroundJob($jobName: BackgroundJob!) {
   deployBackgroundJob(jobName: $jobName)
 }
@@ -213,3 +221,27 @@ mutation UpdateUserPreference($input: UpdateUserPreferenceInput!) {
   updateUserPreference(input: $input)
 }
 
+
+mutation CreateAccessLink($input: CreateAccessLinkInput!) {
+  createAccessLink(input: $input) {
+    id
+  }
+}
+
+mutation ProcessAccessLink($input: ProcessAccessLinkInput!) {
+  processAccessLink(input: $input) {
+    __typename
+    ... on ProcessAccessLinkError {
+      error
+    }
+    ... on ProcessAccessLinkResponse {
+      apiKey
+      redirectTo
+      tokenValidForDays
+    }
+  }
+}
+
+mutation revokeAccessLink($accessLinkId: String!) {
+  revokeAccessLink(accessLinkId: $accessLinkId)
+}
diff --git a/libs/graphql/src/backend/queries/UserPreferences.gql b/libs/graphql/src/backend/queries/UserPreferences.gql
index b1b99f3d60..5995f85e91 100644
--- a/libs/graphql/src/backend/queries/UserPreferences.gql
+++ b/libs/graphql/src/backend/queries/UserPreferences.gql
@@ -64,6 +64,7 @@ query UserPreferences {
       fitness {
         enabled
         workouts
+        templates
         measurements
       }
       media {
diff --git a/libs/graphql/src/backend/queries/UserWorkoutTemplatesList.gql b/libs/graphql/src/backend/queries/UserWorkoutTemplatesList.gql
new file mode 100644
index 0000000000..30a6c13583
--- /dev/null
+++ b/libs/graphql/src/backend/queries/UserWorkoutTemplatesList.gql
@@ -0,0 +1,17 @@
+query UserWorkoutTemplatesList($input: SearchInput!) {
+  userWorkoutTemplatesList(input: $input) {
+    details {
+      total
+      nextPage
+    }
+    items {
+      id
+      name
+      createdOn
+      visibility
+      summary {
+        ...WorkoutSummaryPart
+      }
+    }
+  }
+}
diff --git a/libs/graphql/src/backend/queries/WorkoutDetails.gql b/libs/graphql/src/backend/queries/WorkoutDetails.gql
index 7ab2b9d7f4..e6ece02939 100644
--- a/libs/graphql/src/backend/queries/WorkoutDetails.gql
+++ b/libs/graphql/src/backend/queries/WorkoutDetails.gql
@@ -9,6 +9,7 @@ query WorkoutDetails($workoutId: String!) {
       endTime
       duration
       startTime
+      templateId
       repeatedFrom
       summary {
         ...WorkoutSummaryPart
diff --git a/libs/graphql/src/backend/queries/WorkoutTemplateDetails.gql b/libs/graphql/src/backend/queries/WorkoutTemplateDetails.gql
new file mode 100644
index 0000000000..b20e7ac0e3
--- /dev/null
+++ b/libs/graphql/src/backend/queries/WorkoutTemplateDetails.gql
@@ -0,0 +1,20 @@
+query WorkoutTemplateDetails($workoutTemplateId: String!) {
+  workoutTemplateDetails(workoutTemplateId: $workoutTemplateId) {
+    collections {
+      ...CollectionPart
+    }
+    details {
+      id
+      name
+      createdOn
+      visibility
+      defaultRestTimer
+      summary {
+        ...WorkoutSummaryPart
+      }
+      information {
+        ...WorkoutInformationPart
+      }
+    }
+  }
+}
diff --git a/libs/graphql/src/backend/queries/combined.gql b/libs/graphql/src/backend/queries/combined.gql
index c090d3530e..be673a7b59 100644
--- a/libs/graphql/src/backend/queries/combined.gql
+++ b/libs/graphql/src/backend/queries/combined.gql
@@ -69,6 +69,7 @@ query UserIntegrations {
     maximumProgress
     minimumProgress
     lastTriggeredOn
+    syncToOwnedCollection
   }
 }
 
@@ -91,6 +92,10 @@ query UsersList($query: String) {
   }
 }
 
+query UserRecommendations {
+  userRecommendations
+}
+
 query UserUpcomingCalendarEvents($input: UserUpcomingCalendarEventInput!) {
   userUpcomingCalendarEvents(input: $input) {
     ...CalendarEventPart
@@ -136,6 +141,21 @@ query PeopleList($input: PeopleListInput!) {
   }
 }
 
+query UserAccessLinks {
+  userAccessLinks {
+    id
+    name
+    isDemo
+    createdOn
+    expiresOn
+    timesUsed
+    isRevoked
+    maximumUses
+    isAccountDefault
+    isMutationAllowed
+  }
+}
+
 query DailyUserActivities($input: DailyUserActivitiesInput!) {
   dailyUserActivities(input: $input) {
     groupedBy
diff --git a/libs/graphql/src/backend/queries/fragments.gql b/libs/graphql/src/backend/queries/fragments.gql
index d9ac78c36a..c898f668e4 100644
--- a/libs/graphql/src/backend/queries/fragments.gql
+++ b/libs/graphql/src/backend/queries/fragments.gql
@@ -37,13 +37,14 @@ fragment CalendarEventPart on GraphqlCalendarEvent {
 
 fragment SeenPart on Seen {
   id
-  progress
-  providerWatchedOn
   state
+  progress
   startedOn
   finishedOn
   lastUpdatedOn
+  manualTimeSpent
   numTimesUpdated
+  providerWatchedOn
   showExtraInformation {
     ...SeenShowExtraInformationPart
   }
diff --git a/libs/transactional/emails/LoginCode.tsx b/libs/transactional/emails/LoginCode.tsx
new file mode 100644
index 0000000000..5ccd6a4063
--- /dev/null
+++ b/libs/transactional/emails/LoginCode.tsx
@@ -0,0 +1,21 @@
+import { Text } from "@react-email/components";
+import Layout from "../components/Layout";
+
+type LoginCodeEmailProps = {
+	code: string;
+};
+
+const subject = "Login request for Ryot";
+
+const LoginCodeEmail = (props: LoginCodeEmailProps) => (
+	<Layout headingText={subject}>
+		<Text>
+			Your login code for Ryot is <strong>{props.code}</strong>. Please use this
+			code to login to your account.
+		</Text>
+	</Layout>
+);
+
+LoginCodeEmail.subject = subject;
+
+export default LoginCodeEmail;
diff --git a/libs/transactional/emails/PurchaseComplete.tsx b/libs/transactional/emails/PurchaseComplete.tsx
new file mode 100644
index 0000000000..1acbb530e4
--- /dev/null
+++ b/libs/transactional/emails/PurchaseComplete.tsx
@@ -0,0 +1,61 @@
+import { Link, Text } from "@react-email/components";
+import { isString } from "@ryot/ts-utils";
+import Layout from "../components/Layout";
+
+const proLink = "https://pro.ryot.io";
+
+type PurchaseCompleteEmailProps = {
+	planType: string;
+	renewOn?: string;
+	data:
+		| { __typename: "self_hosted"; key: string }
+		| {
+				__typename: "cloud";
+				auth: string | { username: string; password: string };
+		  };
+};
+
+const subject = "Thank you for buying Ryot!";
+
+const PurchaseCompleteEmail = (props: PurchaseCompleteEmailProps) =>
+	props.data ? (
+		<Layout headingText={subject}>
+			<Text>
+				You have successfully purchased a {props.planType} plan for Ryot Pro (
+				{props.data.__typename}).{" "}
+				{props.renewOn
+					? `Your subscription will renew on ${props.renewOn}.`
+					: null}
+			</Text>
+			<Text>
+				{props.data.__typename === "self_hosted" ? (
+					<>
+						Your Pro Key is <strong>{props.data.key}</strong>. Please follow{" "}
+						<Link href="https://docs.ryot.io#upgrading-to-pro">these</Link>{" "}
+						instructions to install/upgrade Ryot with your key.
+					</>
+				) : (
+					<>
+						Your account has been created on{" "}
+						<Link href={proLink}>{proLink}</Link> with{" "}
+						{isString(props.data.auth) ? (
+							`Google using the email ${props.data.auth}. Please login to get started`
+						) : (
+							<>
+								the username <strong>{props.data.auth.username}</strong> and
+								password <strong>{props.data.auth.password}</strong>. Please
+								login and change your password from the profile settings
+							</>
+						)}
+						.
+					</>
+				)}
+			</Text>
+		</Layout>
+	) : (
+		<></>
+	);
+
+PurchaseCompleteEmail.subject = subject;
+
+export default PurchaseCompleteEmail;
diff --git a/libs/transactional/package.json b/libs/transactional/package.json
index 9e34f24116..14e7734301 100644
--- a/libs/transactional/package.json
+++ b/libs/transactional/package.json
@@ -3,6 +3,7 @@
 	"private": true,
 	"dependencies": {
 		"@react-email/components": "0.0.22",
+		"@ryot/ts-utils": "workspace:*",
 		"react-dom": "18.3.1",
 		"react-email": "2.1.6"
 	}
diff --git a/libs/transactional/tsconfig.json b/libs/transactional/tsconfig.json
index 156ff2e432..7554890679 100644
--- a/libs/transactional/tsconfig.json
+++ b/libs/transactional/tsconfig.json
@@ -1,9 +1,22 @@
 {
 	"extends": "../../tsconfig.options.json",
-	"include": ["**/*"],
-	"references": [],
+	"include": [
+		"**/*"
+	],
+	"references": [
+		{
+			"path": "../ts-utils"
+		}
+	],
 	"compilerOptions": {
-		"paths": {},
+		"paths": {
+			"@ryot/ts-utils": [
+				"../ts-utils/src/index.ts"
+			],
+			"@ryot/ts-utils/*": [
+				"../ts-utils/src/*"
+			]
+		},
 		"module": "ESNext",
 		"jsx": "react-jsx",
 		"moduleResolution": "Bundler",
diff --git a/mprocs.yaml b/mprocs.yaml
index 8a2eebbfe2..9272f880f2 100644
--- a/mprocs.yaml
+++ b/mprocs.yaml
@@ -1,6 +1,7 @@
 procs:
   Backend: "cargo watch --ignore 'docs/includes' --ignore 'libs/generated' -qx 'run'"
   Frontend: "moon run frontend:dev"
+  Website: "moon run website:dev"
   Transactional: "moon run transactional:dev"
   Main Reverse Proxy: "caddy run --config 'ci/Caddyfile'"
   Minio Reverse Proxy: "caddy reverse-proxy --from ':9000' --to 'ryot-minio:9000'"
diff --git a/package.json b/package.json
index 5cbc884756..d7641da1af 100644
--- a/package.json
+++ b/package.json
@@ -3,6 +3,7 @@
 	"packageManager": "yarn@4.1.1",
 	"workspaces": [
 		"apps/frontend",
+		"apps/website",
 		"libs/generated",
 		"libs/graphql",
 		"libs/transactional",
diff --git a/tsconfig.json b/tsconfig.json
index bea4a6e13e..69268b52fd 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -5,6 +5,9 @@
 		{
 			"path": "apps/frontend"
 		},
+		{
+			"path": "apps/website"
+		},
 		{
 			"path": "libs/generated"
 		},
diff --git a/yarn.lock b/yarn.lock
index c5b9b55857..56e27ff1d7 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1861,6 +1861,26 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild-kit/core-utils@npm:^3.3.2":
+  version: 3.3.2
+  resolution: "@esbuild-kit/core-utils@npm:3.3.2"
+  dependencies:
+    esbuild: "npm:~0.18.20"
+    source-map-support: "npm:^0.5.21"
+  checksum: 10/012387ed407c57b9735bf2cc5bc3b9b35888f69376bb7d749e9e16415248deeb66b7d93444458239ba15872738a4b5d4e8466b7260e721c6de477dd5e87ef945
+  languageName: node
+  linkType: hard
+
+"@esbuild-kit/esm-loader@npm:^2.5.5":
+  version: 2.6.5
+  resolution: "@esbuild-kit/esm-loader@npm:2.6.5"
+  dependencies:
+    "@esbuild-kit/core-utils": "npm:^3.3.2"
+    get-tsconfig: "npm:^4.7.0"
+  checksum: 10/e7dac48017fbb7e13f0519ff25f700c38c78ccc8ae2d7d117216c6658d8d21499d01b4381287acdf2f7dde36d812b9a3e57a3c4b38cec4717173cd65c13037e0
+  languageName: node
+  linkType: hard
+
 "@esbuild/aix-ppc64@npm:0.19.11":
   version: 0.19.11
   resolution: "@esbuild/aix-ppc64@npm:0.19.11"
@@ -1868,6 +1888,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/aix-ppc64@npm:0.19.12":
+  version: 0.19.12
+  resolution: "@esbuild/aix-ppc64@npm:0.19.12"
+  conditions: os=aix & cpu=ppc64
+  languageName: node
+  linkType: hard
+
 "@esbuild/aix-ppc64@npm:0.21.5":
   version: 0.21.5
   resolution: "@esbuild/aix-ppc64@npm:0.21.5"
@@ -1896,6 +1923,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/android-arm64@npm:0.19.12":
+  version: 0.19.12
+  resolution: "@esbuild/android-arm64@npm:0.19.12"
+  conditions: os=android & cpu=arm64
+  languageName: node
+  linkType: hard
+
 "@esbuild/android-arm64@npm:0.21.5":
   version: 0.21.5
   resolution: "@esbuild/android-arm64@npm:0.21.5"
@@ -1924,6 +1958,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/android-arm@npm:0.19.12":
+  version: 0.19.12
+  resolution: "@esbuild/android-arm@npm:0.19.12"
+  conditions: os=android & cpu=arm
+  languageName: node
+  linkType: hard
+
 "@esbuild/android-arm@npm:0.21.5":
   version: 0.21.5
   resolution: "@esbuild/android-arm@npm:0.21.5"
@@ -1952,6 +1993,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/android-x64@npm:0.19.12":
+  version: 0.19.12
+  resolution: "@esbuild/android-x64@npm:0.19.12"
+  conditions: os=android & cpu=x64
+  languageName: node
+  linkType: hard
+
 "@esbuild/android-x64@npm:0.21.5":
   version: 0.21.5
   resolution: "@esbuild/android-x64@npm:0.21.5"
@@ -1980,6 +2028,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/darwin-arm64@npm:0.19.12":
+  version: 0.19.12
+  resolution: "@esbuild/darwin-arm64@npm:0.19.12"
+  conditions: os=darwin & cpu=arm64
+  languageName: node
+  linkType: hard
+
 "@esbuild/darwin-arm64@npm:0.21.5":
   version: 0.21.5
   resolution: "@esbuild/darwin-arm64@npm:0.21.5"
@@ -2008,6 +2063,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/darwin-x64@npm:0.19.12":
+  version: 0.19.12
+  resolution: "@esbuild/darwin-x64@npm:0.19.12"
+  conditions: os=darwin & cpu=x64
+  languageName: node
+  linkType: hard
+
 "@esbuild/darwin-x64@npm:0.21.5":
   version: 0.21.5
   resolution: "@esbuild/darwin-x64@npm:0.21.5"
@@ -2036,6 +2098,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/freebsd-arm64@npm:0.19.12":
+  version: 0.19.12
+  resolution: "@esbuild/freebsd-arm64@npm:0.19.12"
+  conditions: os=freebsd & cpu=arm64
+  languageName: node
+  linkType: hard
+
 "@esbuild/freebsd-arm64@npm:0.21.5":
   version: 0.21.5
   resolution: "@esbuild/freebsd-arm64@npm:0.21.5"
@@ -2064,6 +2133,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/freebsd-x64@npm:0.19.12":
+  version: 0.19.12
+  resolution: "@esbuild/freebsd-x64@npm:0.19.12"
+  conditions: os=freebsd & cpu=x64
+  languageName: node
+  linkType: hard
+
 "@esbuild/freebsd-x64@npm:0.21.5":
   version: 0.21.5
   resolution: "@esbuild/freebsd-x64@npm:0.21.5"
@@ -2092,6 +2168,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/linux-arm64@npm:0.19.12":
+  version: 0.19.12
+  resolution: "@esbuild/linux-arm64@npm:0.19.12"
+  conditions: os=linux & cpu=arm64
+  languageName: node
+  linkType: hard
+
 "@esbuild/linux-arm64@npm:0.21.5":
   version: 0.21.5
   resolution: "@esbuild/linux-arm64@npm:0.21.5"
@@ -2120,6 +2203,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/linux-arm@npm:0.19.12":
+  version: 0.19.12
+  resolution: "@esbuild/linux-arm@npm:0.19.12"
+  conditions: os=linux & cpu=arm
+  languageName: node
+  linkType: hard
+
 "@esbuild/linux-arm@npm:0.21.5":
   version: 0.21.5
   resolution: "@esbuild/linux-arm@npm:0.21.5"
@@ -2148,6 +2238,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/linux-ia32@npm:0.19.12":
+  version: 0.19.12
+  resolution: "@esbuild/linux-ia32@npm:0.19.12"
+  conditions: os=linux & cpu=ia32
+  languageName: node
+  linkType: hard
+
 "@esbuild/linux-ia32@npm:0.21.5":
   version: 0.21.5
   resolution: "@esbuild/linux-ia32@npm:0.21.5"
@@ -2176,6 +2273,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/linux-loong64@npm:0.19.12":
+  version: 0.19.12
+  resolution: "@esbuild/linux-loong64@npm:0.19.12"
+  conditions: os=linux & cpu=loong64
+  languageName: node
+  linkType: hard
+
 "@esbuild/linux-loong64@npm:0.21.5":
   version: 0.21.5
   resolution: "@esbuild/linux-loong64@npm:0.21.5"
@@ -2204,6 +2308,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/linux-mips64el@npm:0.19.12":
+  version: 0.19.12
+  resolution: "@esbuild/linux-mips64el@npm:0.19.12"
+  conditions: os=linux & cpu=mips64el
+  languageName: node
+  linkType: hard
+
 "@esbuild/linux-mips64el@npm:0.21.5":
   version: 0.21.5
   resolution: "@esbuild/linux-mips64el@npm:0.21.5"
@@ -2232,6 +2343,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/linux-ppc64@npm:0.19.12":
+  version: 0.19.12
+  resolution: "@esbuild/linux-ppc64@npm:0.19.12"
+  conditions: os=linux & cpu=ppc64
+  languageName: node
+  linkType: hard
+
 "@esbuild/linux-ppc64@npm:0.21.5":
   version: 0.21.5
   resolution: "@esbuild/linux-ppc64@npm:0.21.5"
@@ -2260,6 +2378,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/linux-riscv64@npm:0.19.12":
+  version: 0.19.12
+  resolution: "@esbuild/linux-riscv64@npm:0.19.12"
+  conditions: os=linux & cpu=riscv64
+  languageName: node
+  linkType: hard
+
 "@esbuild/linux-riscv64@npm:0.21.5":
   version: 0.21.5
   resolution: "@esbuild/linux-riscv64@npm:0.21.5"
@@ -2288,6 +2413,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/linux-s390x@npm:0.19.12":
+  version: 0.19.12
+  resolution: "@esbuild/linux-s390x@npm:0.19.12"
+  conditions: os=linux & cpu=s390x
+  languageName: node
+  linkType: hard
+
 "@esbuild/linux-s390x@npm:0.21.5":
   version: 0.21.5
   resolution: "@esbuild/linux-s390x@npm:0.21.5"
@@ -2316,6 +2448,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/linux-x64@npm:0.19.12":
+  version: 0.19.12
+  resolution: "@esbuild/linux-x64@npm:0.19.12"
+  conditions: os=linux & cpu=x64
+  languageName: node
+  linkType: hard
+
 "@esbuild/linux-x64@npm:0.21.5":
   version: 0.21.5
   resolution: "@esbuild/linux-x64@npm:0.21.5"
@@ -2344,6 +2483,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/netbsd-x64@npm:0.19.12":
+  version: 0.19.12
+  resolution: "@esbuild/netbsd-x64@npm:0.19.12"
+  conditions: os=netbsd & cpu=x64
+  languageName: node
+  linkType: hard
+
 "@esbuild/netbsd-x64@npm:0.21.5":
   version: 0.21.5
   resolution: "@esbuild/netbsd-x64@npm:0.21.5"
@@ -2372,6 +2518,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/openbsd-x64@npm:0.19.12":
+  version: 0.19.12
+  resolution: "@esbuild/openbsd-x64@npm:0.19.12"
+  conditions: os=openbsd & cpu=x64
+  languageName: node
+  linkType: hard
+
 "@esbuild/openbsd-x64@npm:0.21.5":
   version: 0.21.5
   resolution: "@esbuild/openbsd-x64@npm:0.21.5"
@@ -2400,6 +2553,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/sunos-x64@npm:0.19.12":
+  version: 0.19.12
+  resolution: "@esbuild/sunos-x64@npm:0.19.12"
+  conditions: os=sunos & cpu=x64
+  languageName: node
+  linkType: hard
+
 "@esbuild/sunos-x64@npm:0.21.5":
   version: 0.21.5
   resolution: "@esbuild/sunos-x64@npm:0.21.5"
@@ -2428,6 +2588,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/win32-arm64@npm:0.19.12":
+  version: 0.19.12
+  resolution: "@esbuild/win32-arm64@npm:0.19.12"
+  conditions: os=win32 & cpu=arm64
+  languageName: node
+  linkType: hard
+
 "@esbuild/win32-arm64@npm:0.21.5":
   version: 0.21.5
   resolution: "@esbuild/win32-arm64@npm:0.21.5"
@@ -2456,6 +2623,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/win32-ia32@npm:0.19.12":
+  version: 0.19.12
+  resolution: "@esbuild/win32-ia32@npm:0.19.12"
+  conditions: os=win32 & cpu=ia32
+  languageName: node
+  linkType: hard
+
 "@esbuild/win32-ia32@npm:0.21.5":
   version: 0.21.5
   resolution: "@esbuild/win32-ia32@npm:0.21.5"
@@ -2484,6 +2658,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/win32-x64@npm:0.19.12":
+  version: 0.19.12
+  resolution: "@esbuild/win32-x64@npm:0.19.12"
+  conditions: os=win32 & cpu=x64
+  languageName: node
+  linkType: hard
+
 "@esbuild/win32-x64@npm:0.21.5":
   version: 0.21.5
   resolution: "@esbuild/win32-x64@npm:0.21.5"
@@ -3348,6 +3529,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@isaacs/ttlcache@npm:1.4.1":
+  version: 1.4.1
+  resolution: "@isaacs/ttlcache@npm:1.4.1"
+  checksum: 10/57f2b00b58845d48a173c7668c58c27c3e6f91a56c17d6d4c58b38780a475a858ce3b4fc2cd4304469eee9f49818b79a187f0e13120b3617c4f67e4abc475698
+  languageName: node
+  linkType: hard
+
 "@jridgewell/gen-mapping@npm:^0.3.0, @jridgewell/gen-mapping@npm:^0.3.2":
   version: 0.3.3
   resolution: "@jridgewell/gen-mapping@npm:0.3.3"
@@ -3796,6 +3984,23 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@paddle/paddle-js@npm:1.2.0":
+  version: 1.2.0
+  resolution: "@paddle/paddle-js@npm:1.2.0"
+  checksum: 10/87bf72de59f6ef728dcba9581c893608c09b8d897414f8fff4544461db3a54231709e723a79c3090768072169b9981f92fbb51b6ea836142328741593c077192
+  languageName: node
+  linkType: hard
+
+"@paddle/paddle-node-sdk@npm:1.4.1":
+  version: 1.4.1
+  resolution: "@paddle/paddle-node-sdk@npm:1.4.1"
+  dependencies:
+    lodash: "npm:^4.17.21"
+    node-fetch: "npm:^2.7.0"
+  checksum: 10/7b2b4f8a11b3740f87311fba3fbf12498044cfa0631adade896bb11603901d3b1e84b84251a9a1107e21e3a0ba15e352d3791bc5aa967212798e6df1387cca0d
+  languageName: node
+  linkType: hard
+
 "@peculiar/asn1-schema@npm:^2.3.6":
   version: 2.3.6
   resolution: "@peculiar/asn1-schema@npm:2.3.6"
@@ -4238,6 +4443,25 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@radix-ui/react-label@npm:2.1.0":
+  version: 2.1.0
+  resolution: "@radix-ui/react-label@npm:2.1.0"
+  dependencies:
+    "@radix-ui/react-primitive": "npm:2.0.0"
+  peerDependencies:
+    "@types/react": "*"
+    "@types/react-dom": "*"
+    react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+  peerDependenciesMeta:
+    "@types/react":
+      optional: true
+    "@types/react-dom":
+      optional: true
+  checksum: 10/462f3b47f43bd69c937e660dee229eb05990441e1b214923cdcf848aea62226e9f1c4673de6664bb4ad66f8a480acdb0f5d2ce4c88c425652b47432876b3f32f
+  languageName: node
+  linkType: hard
+
 "@radix-ui/react-popover@npm:1.1.1":
   version: 1.1.1
   resolution: "@radix-ui/react-popover@npm:1.1.1"
@@ -4601,6 +4825,36 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@radix-ui/react-tooltip@npm:1.1.2":
+  version: 1.1.2
+  resolution: "@radix-ui/react-tooltip@npm:1.1.2"
+  dependencies:
+    "@radix-ui/primitive": "npm:1.1.0"
+    "@radix-ui/react-compose-refs": "npm:1.1.0"
+    "@radix-ui/react-context": "npm:1.1.0"
+    "@radix-ui/react-dismissable-layer": "npm:1.1.0"
+    "@radix-ui/react-id": "npm:1.1.0"
+    "@radix-ui/react-popper": "npm:1.2.0"
+    "@radix-ui/react-portal": "npm:1.1.1"
+    "@radix-ui/react-presence": "npm:1.1.0"
+    "@radix-ui/react-primitive": "npm:2.0.0"
+    "@radix-ui/react-slot": "npm:1.1.0"
+    "@radix-ui/react-use-controllable-state": "npm:1.1.0"
+    "@radix-ui/react-visually-hidden": "npm:1.1.0"
+  peerDependencies:
+    "@types/react": "*"
+    "@types/react-dom": "*"
+    react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+  peerDependenciesMeta:
+    "@types/react":
+      optional: true
+    "@types/react-dom":
+      optional: true
+  checksum: 10/d8386260d2710720b3d87fdacc04597935e5f6eb1d0375516dd362c2f00bf07ea1fceb4c4bda76ec332ebbcf6a4b88f04050add1a9a4db0f13a5ac12b1190355
+  languageName: node
+  linkType: hard
+
 "@radix-ui/react-use-callback-ref@npm:1.0.1":
   version: 1.0.1
   resolution: "@radix-ui/react-use-callback-ref@npm:1.0.1"
@@ -5072,6 +5326,84 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@remix-run/dev@npm:2.10.3":
+  version: 2.10.3
+  resolution: "@remix-run/dev@npm:2.10.3"
+  dependencies:
+    "@babel/core": "npm:^7.21.8"
+    "@babel/generator": "npm:^7.21.5"
+    "@babel/parser": "npm:^7.21.8"
+    "@babel/plugin-syntax-decorators": "npm:^7.22.10"
+    "@babel/plugin-syntax-jsx": "npm:^7.21.4"
+    "@babel/preset-typescript": "npm:^7.21.5"
+    "@babel/traverse": "npm:^7.23.2"
+    "@babel/types": "npm:^7.22.5"
+    "@mdx-js/mdx": "npm:^2.3.0"
+    "@npmcli/package-json": "npm:^4.0.1"
+    "@remix-run/node": "npm:2.10.3"
+    "@remix-run/router": "npm:1.18.0"
+    "@remix-run/server-runtime": "npm:2.10.3"
+    "@types/mdx": "npm:^2.0.5"
+    "@vanilla-extract/integration": "npm:^6.2.0"
+    arg: "npm:^5.0.1"
+    cacache: "npm:^17.1.3"
+    chalk: "npm:^4.1.2"
+    chokidar: "npm:^3.5.1"
+    cross-spawn: "npm:^7.0.3"
+    dotenv: "npm:^16.0.0"
+    es-module-lexer: "npm:^1.3.1"
+    esbuild: "npm:0.17.6"
+    esbuild-plugins-node-modules-polyfill: "npm:^1.6.0"
+    execa: "npm:5.1.1"
+    exit-hook: "npm:2.2.1"
+    express: "npm:^4.19.2"
+    fs-extra: "npm:^10.0.0"
+    get-port: "npm:^5.1.1"
+    gunzip-maybe: "npm:^1.4.2"
+    jsesc: "npm:3.0.2"
+    json5: "npm:^2.2.2"
+    lodash: "npm:^4.17.21"
+    lodash.debounce: "npm:^4.0.8"
+    minimatch: "npm:^9.0.0"
+    ora: "npm:^5.4.1"
+    picocolors: "npm:^1.0.0"
+    picomatch: "npm:^2.3.1"
+    pidtree: "npm:^0.6.0"
+    postcss: "npm:^8.4.19"
+    postcss-discard-duplicates: "npm:^5.1.0"
+    postcss-load-config: "npm:^4.0.1"
+    postcss-modules: "npm:^6.0.0"
+    prettier: "npm:^2.7.1"
+    pretty-ms: "npm:^7.0.1"
+    react-refresh: "npm:^0.14.0"
+    remark-frontmatter: "npm:4.0.1"
+    remark-mdx-frontmatter: "npm:^1.0.1"
+    semver: "npm:^7.3.7"
+    set-cookie-parser: "npm:^2.6.0"
+    tar-fs: "npm:^2.1.1"
+    tsconfig-paths: "npm:^4.0.0"
+    ws: "npm:^7.4.5"
+  peerDependencies:
+    "@remix-run/react": ^2.10.3
+    "@remix-run/serve": ^2.10.3
+    typescript: ^5.1.0
+    vite: ^5.1.0
+    wrangler: ^3.28.2
+  peerDependenciesMeta:
+    "@remix-run/serve":
+      optional: true
+    typescript:
+      optional: true
+    vite:
+      optional: true
+    wrangler:
+      optional: true
+  bin:
+    remix: dist/cli.js
+  checksum: 10/44e5429ff2efd35a882d4050b4485b4b28b9bd2a74b394b7d2442ba2df164e2b2539af3db4501e628bba2888c08d39324bed600f5117e5d3184650faaf9a53a1
+  languageName: node
+  linkType: hard
+
 "@remix-run/dev@npm:2.11.2":
   version: 2.11.2
   resolution: "@remix-run/dev@npm:2.11.2"
@@ -5150,6 +5482,21 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@remix-run/express@npm:2.10.3":
+  version: 2.10.3
+  resolution: "@remix-run/express@npm:2.10.3"
+  dependencies:
+    "@remix-run/node": "npm:2.10.3"
+  peerDependencies:
+    express: ^4.19.2
+    typescript: ^5.1.0
+  peerDependenciesMeta:
+    typescript:
+      optional: true
+  checksum: 10/b635fffa1beee32cad4603427766962728106a9daca081caf8538f1831c670a83bbfdfe91b314655c762e88d6ab6987f8d9e544dbe2189922f41b235aa4828c7
+  languageName: node
+  linkType: hard
+
 "@remix-run/express@npm:2.11.2":
   version: 2.11.2
   resolution: "@remix-run/express@npm:2.11.2"
@@ -5165,6 +5512,26 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@remix-run/node@npm:2.10.3":
+  version: 2.10.3
+  resolution: "@remix-run/node@npm:2.10.3"
+  dependencies:
+    "@remix-run/server-runtime": "npm:2.10.3"
+    "@remix-run/web-fetch": "npm:^4.4.2"
+    "@web3-storage/multipart-parser": "npm:^1.0.0"
+    cookie-signature: "npm:^1.1.0"
+    source-map-support: "npm:^0.5.21"
+    stream-slice: "npm:^0.1.2"
+    undici: "npm:^6.11.1"
+  peerDependencies:
+    typescript: ^5.1.0
+  peerDependenciesMeta:
+    typescript:
+      optional: true
+  checksum: 10/70fa59c6ee56f64f89c93a844a18b8781386262d2f49edcd34c3e14f2ec6aeae90070fa8f35213133267e73ec91719efc35be38310b97ae0a0e5c738ac4718f7
+  languageName: node
+  linkType: hard
+
 "@remix-run/node@npm:2.11.2":
   version: 2.11.2
   resolution: "@remix-run/node@npm:2.11.2"
@@ -5185,6 +5552,26 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@remix-run/react@npm:2.10.3":
+  version: 2.10.3
+  resolution: "@remix-run/react@npm:2.10.3"
+  dependencies:
+    "@remix-run/router": "npm:1.18.0"
+    "@remix-run/server-runtime": "npm:2.10.3"
+    react-router: "npm:6.25.0"
+    react-router-dom: "npm:6.25.0"
+    turbo-stream: "npm:2.2.0"
+  peerDependencies:
+    react: ^18.0.0
+    react-dom: ^18.0.0
+    typescript: ^5.1.0
+  peerDependenciesMeta:
+    typescript:
+      optional: true
+  checksum: 10/0766017c3b9497f8d6ee51ab643a9fe85cc65dd50e6afa8c4601bfa4d72fc4dc5ca42711e46aff9f069ca002bd1a583a86d1278007d70144c16552568f5e7394
+  languageName: node
+  linkType: hard
+
 "@remix-run/react@npm:2.11.2":
   version: 2.11.2
   resolution: "@remix-run/react@npm:2.11.2"
@@ -5205,6 +5592,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@remix-run/router@npm:1.18.0":
+  version: 1.18.0
+  resolution: "@remix-run/router@npm:1.18.0"
+  checksum: 10/f878cf246b94368f431a51363f1d33dc35ad11cb910d930476d988825b024a152de87a7f74f0891c3e7182228f892c7f64f94409aae27084c320338dee82caa1
+  languageName: node
+  linkType: hard
+
 "@remix-run/router@npm:1.19.1":
   version: 1.19.1
   resolution: "@remix-run/router@npm:1.19.1"
@@ -5212,6 +5606,24 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@remix-run/serve@npm:2.10.3":
+  version: 2.10.3
+  resolution: "@remix-run/serve@npm:2.10.3"
+  dependencies:
+    "@remix-run/express": "npm:2.10.3"
+    "@remix-run/node": "npm:2.10.3"
+    chokidar: "npm:^3.5.3"
+    compression: "npm:^1.7.4"
+    express: "npm:^4.19.2"
+    get-port: "npm:5.1.1"
+    morgan: "npm:^1.10.0"
+    source-map-support: "npm:^0.5.21"
+  bin:
+    remix-serve: dist/cli.js
+  checksum: 10/05c179518f4c1fe853077d50759e9441434bab824e7b63a010d236bb411eba2f06aa50430938c91d2ab2270828b29023333e3b6083b038c08df44c334d3b51cf
+  languageName: node
+  linkType: hard
+
 "@remix-run/serve@npm:2.11.2":
   version: 2.11.2
   resolution: "@remix-run/serve@npm:2.11.2"
@@ -5230,6 +5642,26 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@remix-run/server-runtime@npm:2.10.3":
+  version: 2.10.3
+  resolution: "@remix-run/server-runtime@npm:2.10.3"
+  dependencies:
+    "@remix-run/router": "npm:1.18.0"
+    "@types/cookie": "npm:^0.6.0"
+    "@web3-storage/multipart-parser": "npm:^1.0.0"
+    cookie: "npm:^0.6.0"
+    set-cookie-parser: "npm:^2.4.8"
+    source-map: "npm:^0.7.3"
+    turbo-stream: "npm:2.2.0"
+  peerDependencies:
+    typescript: ^5.1.0
+  peerDependenciesMeta:
+    typescript:
+      optional: true
+  checksum: 10/7abcc23cb0585c591e2ed2e19fa87c18a0a572ba8cf1103268c4209beda047b574c22f4a76ec55c90af8522af1c7037b1588e33ba0538f3f136f547e399b6aa7
+  languageName: node
+  linkType: hard
+
 "@remix-run/server-runtime@npm:2.11.2":
   version: 2.11.2
   resolution: "@remix-run/server-runtime@npm:2.11.2"
@@ -5310,6 +5742,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@rollup/rollup-android-arm-eabi@npm:4.19.0":
+  version: 4.19.0
+  resolution: "@rollup/rollup-android-arm-eabi@npm:4.19.0"
+  conditions: os=android & cpu=arm
+  languageName: node
+  linkType: hard
+
 "@rollup/rollup-android-arm-eabi@npm:4.21.0":
   version: 4.21.0
   resolution: "@rollup/rollup-android-arm-eabi@npm:4.21.0"
@@ -5317,6 +5756,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@rollup/rollup-android-arm64@npm:4.19.0":
+  version: 4.19.0
+  resolution: "@rollup/rollup-android-arm64@npm:4.19.0"
+  conditions: os=android & cpu=arm64
+  languageName: node
+  linkType: hard
+
 "@rollup/rollup-android-arm64@npm:4.21.0":
   version: 4.21.0
   resolution: "@rollup/rollup-android-arm64@npm:4.21.0"
@@ -5324,6 +5770,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@rollup/rollup-darwin-arm64@npm:4.19.0":
+  version: 4.19.0
+  resolution: "@rollup/rollup-darwin-arm64@npm:4.19.0"
+  conditions: os=darwin & cpu=arm64
+  languageName: node
+  linkType: hard
+
 "@rollup/rollup-darwin-arm64@npm:4.21.0":
   version: 4.21.0
   resolution: "@rollup/rollup-darwin-arm64@npm:4.21.0"
@@ -5331,6 +5784,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@rollup/rollup-darwin-x64@npm:4.19.0":
+  version: 4.19.0
+  resolution: "@rollup/rollup-darwin-x64@npm:4.19.0"
+  conditions: os=darwin & cpu=x64
+  languageName: node
+  linkType: hard
+
 "@rollup/rollup-darwin-x64@npm:4.21.0":
   version: 4.21.0
   resolution: "@rollup/rollup-darwin-x64@npm:4.21.0"
@@ -5338,6 +5798,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@rollup/rollup-linux-arm-gnueabihf@npm:4.19.0":
+  version: 4.19.0
+  resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.19.0"
+  conditions: os=linux & cpu=arm & libc=glibc
+  languageName: node
+  linkType: hard
+
 "@rollup/rollup-linux-arm-gnueabihf@npm:4.21.0":
   version: 4.21.0
   resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.21.0"
@@ -5345,6 +5812,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@rollup/rollup-linux-arm-musleabihf@npm:4.19.0":
+  version: 4.19.0
+  resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.19.0"
+  conditions: os=linux & cpu=arm & libc=musl
+  languageName: node
+  linkType: hard
+
 "@rollup/rollup-linux-arm-musleabihf@npm:4.21.0":
   version: 4.21.0
   resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.21.0"
@@ -5352,6 +5826,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@rollup/rollup-linux-arm64-gnu@npm:4.19.0":
+  version: 4.19.0
+  resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.19.0"
+  conditions: os=linux & cpu=arm64 & libc=glibc
+  languageName: node
+  linkType: hard
+
 "@rollup/rollup-linux-arm64-gnu@npm:4.21.0":
   version: 4.21.0
   resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.21.0"
@@ -5359,6 +5840,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@rollup/rollup-linux-arm64-musl@npm:4.19.0":
+  version: 4.19.0
+  resolution: "@rollup/rollup-linux-arm64-musl@npm:4.19.0"
+  conditions: os=linux & cpu=arm64 & libc=musl
+  languageName: node
+  linkType: hard
+
 "@rollup/rollup-linux-arm64-musl@npm:4.21.0":
   version: 4.21.0
   resolution: "@rollup/rollup-linux-arm64-musl@npm:4.21.0"
@@ -5366,6 +5854,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@rollup/rollup-linux-powerpc64le-gnu@npm:4.19.0":
+  version: 4.19.0
+  resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.19.0"
+  conditions: os=linux & cpu=ppc64 & libc=glibc
+  languageName: node
+  linkType: hard
+
 "@rollup/rollup-linux-powerpc64le-gnu@npm:4.21.0":
   version: 4.21.0
   resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.21.0"
@@ -5373,6 +5868,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@rollup/rollup-linux-riscv64-gnu@npm:4.19.0":
+  version: 4.19.0
+  resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.19.0"
+  conditions: os=linux & cpu=riscv64 & libc=glibc
+  languageName: node
+  linkType: hard
+
 "@rollup/rollup-linux-riscv64-gnu@npm:4.21.0":
   version: 4.21.0
   resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.21.0"
@@ -5380,6 +5882,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@rollup/rollup-linux-s390x-gnu@npm:4.19.0":
+  version: 4.19.0
+  resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.19.0"
+  conditions: os=linux & cpu=s390x & libc=glibc
+  languageName: node
+  linkType: hard
+
 "@rollup/rollup-linux-s390x-gnu@npm:4.21.0":
   version: 4.21.0
   resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.21.0"
@@ -5387,6 +5896,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@rollup/rollup-linux-x64-gnu@npm:4.19.0":
+  version: 4.19.0
+  resolution: "@rollup/rollup-linux-x64-gnu@npm:4.19.0"
+  conditions: os=linux & cpu=x64 & libc=glibc
+  languageName: node
+  linkType: hard
+
 "@rollup/rollup-linux-x64-gnu@npm:4.21.0":
   version: 4.21.0
   resolution: "@rollup/rollup-linux-x64-gnu@npm:4.21.0"
@@ -5394,6 +5910,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@rollup/rollup-linux-x64-musl@npm:4.19.0":
+  version: 4.19.0
+  resolution: "@rollup/rollup-linux-x64-musl@npm:4.19.0"
+  conditions: os=linux & cpu=x64 & libc=musl
+  languageName: node
+  linkType: hard
+
 "@rollup/rollup-linux-x64-musl@npm:4.21.0":
   version: 4.21.0
   resolution: "@rollup/rollup-linux-x64-musl@npm:4.21.0"
@@ -5401,6 +5924,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@rollup/rollup-win32-arm64-msvc@npm:4.19.0":
+  version: 4.19.0
+  resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.19.0"
+  conditions: os=win32 & cpu=arm64
+  languageName: node
+  linkType: hard
+
 "@rollup/rollup-win32-arm64-msvc@npm:4.21.0":
   version: 4.21.0
   resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.21.0"
@@ -5408,6 +5938,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@rollup/rollup-win32-ia32-msvc@npm:4.19.0":
+  version: 4.19.0
+  resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.19.0"
+  conditions: os=win32 & cpu=ia32
+  languageName: node
+  linkType: hard
+
 "@rollup/rollup-win32-ia32-msvc@npm:4.21.0":
   version: 4.21.0
   resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.21.0"
@@ -5415,6 +5952,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@rollup/rollup-win32-x64-msvc@npm:4.19.0":
+  version: 4.19.0
+  resolution: "@rollup/rollup-win32-x64-msvc@npm:4.19.0"
+  conditions: os=win32 & cpu=x64
+  languageName: node
+  linkType: hard
+
 "@rollup/rollup-win32-x64-msvc@npm:4.21.0":
   version: 4.21.0
   resolution: "@rollup/rollup-win32-x64-msvc@npm:4.21.0"
@@ -5471,6 +6015,7 @@ __metadata:
     isbot: "npm:5.1.17"
     jotai: "npm:2.9.3"
     js-cookie: "npm:3.0.5"
+    jwt-decode: "npm:4.0.0"
     mantine-datatable: "npm:7.11.3"
     postcss: "npm:8.4.41"
     postcss-preset-mantine: "npm:1.17.0"
@@ -5530,11 +6075,12 @@ __metadata:
   languageName: unknown
   linkType: soft
 
-"@ryot/transactional@workspace:libs/transactional":
+"@ryot/transactional@workspace:*, @ryot/transactional@workspace:libs/transactional":
   version: 0.0.0-use.local
   resolution: "@ryot/transactional@workspace:libs/transactional"
   dependencies:
     "@react-email/components": "npm:0.0.22"
+    "@ryot/ts-utils": "workspace:*"
     react-dom: "npm:18.3.1"
     react-email: "npm:2.1.6"
   languageName: unknown
@@ -5553,6 +6099,74 @@ __metadata:
   languageName: unknown
   linkType: soft
 
+"@ryot/website@workspace:apps/website":
+  version: 0.0.0-use.local
+  resolution: "@ryot/website@workspace:apps/website"
+  dependencies:
+    "@conform-to/zod": "npm:1.1.5"
+    "@isaacs/ttlcache": "npm:1.4.1"
+    "@paddle/paddle-js": "npm:1.2.0"
+    "@paddle/paddle-node-sdk": "npm:1.4.1"
+    "@radix-ui/react-label": "npm:2.1.0"
+    "@radix-ui/react-slot": "npm:1.1.0"
+    "@radix-ui/react-tooltip": "npm:1.1.2"
+    "@react-email/components": "npm:0.0.22"
+    "@remix-run/dev": "npm:2.10.3"
+    "@remix-run/node": "npm:2.10.3"
+    "@remix-run/react": "npm:2.10.3"
+    "@remix-run/serve": "npm:2.10.3"
+    "@ryot/generated": "workspace:*"
+    "@ryot/graphql": "workspace:*"
+    "@ryot/transactional": "workspace:*"
+    "@ryot/ts-utils": "workspace:*"
+    "@tabler/icons-react": "npm:3.11.0"
+    "@tailwindcss/typography": "npm:0.5.13"
+    "@types/crypto-js": "npm:^4"
+    "@types/lodash": "npm:4.17.7"
+    "@types/nodemailer": "npm:6"
+    "@types/react": "npm:18.2.20"
+    "@types/react-dom": "npm:18.2.7"
+    "@unkey/api": "npm:0.23.0"
+    autoprefixer: "npm:10.4.19"
+    class-variance-authority: "npm:0.7.0"
+    clsx: "npm:2.1.1"
+    crypto-js: "npm:4.2.0"
+    dayjs: "npm:1.11.12"
+    drizzle-kit: "npm:0.23.0"
+    drizzle-orm: "npm:0.32.1"
+    embla-carousel-autoplay: "npm:8.1.7"
+    embla-carousel-react: "npm:8.1.7"
+    graphql: "npm:16.9.0"
+    graphql-request: "npm:7.1.0"
+    humanize-duration-ts: "npm:2.1.1"
+    input-otp: "npm:1.2.4"
+    isbot: "npm:4.1.0"
+    lodash: "npm:4.17.21"
+    lucide-react: "npm:0.414.0"
+    next-themes: "npm:0.3.0"
+    nodemailer: "npm:6.9.14"
+    openid-client: "npm:5.6.5"
+    postcss: "npm:8.4.38"
+    postgres: "npm:3.4.4"
+    react: "npm:18.2.0"
+    react-dom: "npm:18.2.0"
+    remix-routes: "npm:1.7.6"
+    remix-utils: "npm:7.6.0"
+    sonner: "npm:1.5.0"
+    tailwind-merge: "npm:2.4.0"
+    tailwindcss: "npm:3.4.4"
+    tailwindcss-animate: "npm:1.0.7"
+    ts-pattern: "npm:5.2.0"
+    typescript: "npm:5.1.6"
+    typescript-remix-routes-plugin: "npm:1.0.1"
+    ufo: "npm:1.5.4"
+    vite: "npm:5.1.0"
+    vite-tsconfig-paths: "npm:4.2.1"
+    zod: "npm:3.23.8"
+    zodix: "npm:0.4.4"
+  languageName: unknown
+  linkType: soft
+
 "@selderee/plugin-htmlparser2@npm:^0.11.0":
   version: 0.11.0
   resolution: "@selderee/plugin-htmlparser2@npm:0.11.0"
@@ -5711,6 +6325,17 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@tabler/icons-react@npm:3.11.0":
+  version: 3.11.0
+  resolution: "@tabler/icons-react@npm:3.11.0"
+  dependencies:
+    "@tabler/icons": "npm:3.11.0"
+  peerDependencies:
+    react: ">= 16"
+  checksum: 10/1011b55f0a1efe327a863c6fdaf6522685d6492b3cddef0e8dfc2fa8326a88c661e13417f3b07a5710b4bbbde6c6830cb3c2309db81a7b20c0b2242f3e89a982
+  languageName: node
+  linkType: hard
+
 "@tabler/icons-react@npm:3.12.0":
   version: 3.12.0
   resolution: "@tabler/icons-react@npm:3.12.0"
@@ -5722,6 +6347,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@tabler/icons@npm:3.11.0":
+  version: 3.11.0
+  resolution: "@tabler/icons@npm:3.11.0"
+  checksum: 10/1815b5ba0b1016db4902248f5fbc79dff217b9f41ece950bfa0222cfd92d75740bc9d08ab227d7b9fee93e972df57c0c4d8da8b9769a92e065067aa7d8370a13
+  languageName: node
+  linkType: hard
+
 "@tabler/icons@npm:3.12.0":
   version: 3.12.0
   resolution: "@tabler/icons@npm:3.12.0"
@@ -5729,6 +6361,20 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@tailwindcss/typography@npm:0.5.13":
+  version: 0.5.13
+  resolution: "@tailwindcss/typography@npm:0.5.13"
+  dependencies:
+    lodash.castarray: "npm:^4.4.0"
+    lodash.isplainobject: "npm:^4.0.6"
+    lodash.merge: "npm:^4.6.2"
+    postcss-selector-parser: "npm:6.0.10"
+  peerDependencies:
+    tailwindcss: "*"
+  checksum: 10/ffa5fee725d390591f1654fee8275b025248ef0e25a54b1749db23ec82d6117729b933e2326b6d52348ff2844839755f89d5d82fe5900d91b399c9380041b4e9
+  languageName: node
+  linkType: hard
+
 "@tanstack/query-core@npm:5.52.0":
   version: 5.52.0
   resolution: "@tanstack/query-core@npm:5.52.0"
@@ -5805,7 +6451,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@types/crypto-js@npm:4.2.2":
+"@types/crypto-js@npm:4.2.2, @types/crypto-js@npm:^4":
   version: 4.2.2
   resolution: "@types/crypto-js@npm:4.2.2"
   checksum: 10/a40fc5a9219fd33f54ba3e094c5e5ab2904d3106681a76f1029bb038976591e9c8959099963bf4474fde21c2d8d00c1f896445206a3a58f85588f9cb1bd96a9a
@@ -6040,6 +6686,15 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@types/nodemailer@npm:6":
+  version: 6.4.15
+  resolution: "@types/nodemailer@npm:6.4.15"
+  dependencies:
+    "@types/node": "npm:*"
+  checksum: 10/1b400b694ed1bc51c242a719de44b484911481596c288407fff18b81ee76945d2d0c6e411371dfa4293747ebf4146a1f0df0e188ff7b9d2adaccd1183dfa1c2c
+  languageName: node
+  linkType: hard
+
 "@types/normalize-package-data@npm:^2.4.0":
   version: 2.4.4
   resolution: "@types/normalize-package-data@npm:2.4.4"
@@ -6086,6 +6741,15 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@types/react-dom@npm:18.2.7":
+  version: 18.2.7
+  resolution: "@types/react-dom@npm:18.2.7"
+  dependencies:
+    "@types/react": "npm:*"
+  checksum: 10/9b70ef66cbe2d2898ea37eb79ee3697e0e4ad3d950e769a601f79be94097d43b8ef45b98a0b29528203c7d731c81666f637b2b7032deeced99214b4bc0662614
+  languageName: node
+  linkType: hard
+
 "@types/react-dom@npm:18.3.0, @types/react-dom@npm:^18.2.0":
   version: 18.3.0
   resolution: "@types/react-dom@npm:18.3.0"
@@ -6146,6 +6810,34 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@unkey/api@npm:0.23.0":
+  version: 0.23.0
+  resolution: "@unkey/api@npm:0.23.0"
+  dependencies:
+    "@unkey/rbac": "npm:^0.3.1"
+  checksum: 10/7f3d519c54fbd5097086f61ddd0c0ee5d1771711ea508a45b35ba8cc7df697dd6f62f2baf9e4225663b77dce5411e565a89a3eca3620a7d20dbf357d4ee6d62e
+  languageName: node
+  linkType: hard
+
+"@unkey/error@npm:^0.2.0":
+  version: 0.2.0
+  resolution: "@unkey/error@npm:0.2.0"
+  dependencies:
+    zod: "npm:^3.23.5"
+  checksum: 10/7bae43e8d70a559252880a31193e7511d0c877faf149801daaa7b0fcadad90464f5117f4c399fb7586c6f61d1949b6c01c8d7a35db283523acf71b2e9685b508
+  languageName: node
+  linkType: hard
+
+"@unkey/rbac@npm:^0.3.1":
+  version: 0.3.1
+  resolution: "@unkey/rbac@npm:0.3.1"
+  dependencies:
+    "@unkey/error": "npm:^0.2.0"
+    zod: "npm:^3.23.5"
+  checksum: 10/1a977e65de51a22ca205dc58d0f2becfea11ad4eae23ee5d812abe8630f91dfeadbff33d89e9ae10a7c978992fc0be1bd4e100be2ee3898b2a8b3add0c6586bf
+  languageName: node
+  linkType: hard
+
 "@vanilla-extract/babel-plugin-debug-ids@npm:^1.0.2":
   version: 1.0.3
   resolution: "@vanilla-extract/babel-plugin-debug-ids@npm:1.0.3"
@@ -6800,6 +7492,24 @@ __metadata:
   languageName: node
   linkType: hard
 
+"autoprefixer@npm:10.4.19":
+  version: 10.4.19
+  resolution: "autoprefixer@npm:10.4.19"
+  dependencies:
+    browserslist: "npm:^4.23.0"
+    caniuse-lite: "npm:^1.0.30001599"
+    fraction.js: "npm:^4.3.7"
+    normalize-range: "npm:^0.1.2"
+    picocolors: "npm:^1.0.0"
+    postcss-value-parser: "npm:^4.2.0"
+  peerDependencies:
+    postcss: ^8.1.0
+  bin:
+    autoprefixer: bin/autoprefixer
+  checksum: 10/98378eae37b8bf0f1515e4c91b4c9c1ce69ede311d4dea7e934f5afe147d23712c577f112c4019a4c40461c585d82d474d08044f8eb6cb8a063c3d5b7aca52d2
+  languageName: node
+  linkType: hard
+
 "available-typed-arrays@npm:^1.0.5":
   version: 1.0.5
   resolution: "available-typed-arrays@npm:1.0.5"
@@ -6998,7 +7708,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"browserslist@npm:^4.21.10, browserslist@npm:^4.21.5, browserslist@npm:^4.23.1":
+"browserslist@npm:^4.21.10, browserslist@npm:^4.21.5, browserslist@npm:^4.23.0, browserslist@npm:^4.23.1":
   version: 4.23.2
   resolution: "browserslist@npm:4.23.2"
   dependencies:
@@ -7231,7 +7941,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"caniuse-lite@npm:^1.0.30001464, caniuse-lite@npm:^1.0.30001579, caniuse-lite@npm:^1.0.30001640":
+"caniuse-lite@npm:^1.0.30001464, caniuse-lite@npm:^1.0.30001579":
   version: 1.0.30001643
   resolution: "caniuse-lite@npm:1.0.30001643"
   checksum: 10/dddbda29fa24fbc435873309c71070461cbfc915d9bce3216180524c20c5637b2bee1a14b45972e9ac19e1fdf63fba3f63608b9e7d68de32f5ee1953c8c69e05
@@ -7245,6 +7955,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"caniuse-lite@npm:^1.0.30001599, caniuse-lite@npm:^1.0.30001640":
+  version: 1.0.30001642
+  resolution: "caniuse-lite@npm:1.0.30001642"
+  checksum: 10/8d80ea82be453ae0fdfea8766d82740a4945c1b99189650f29bfc458d4e235d7e99027a8f8bc5a4228d8c4457ba896315284b0703f300353ad5f09d8e693de10
+  languageName: node
+  linkType: hard
+
 "capital-case@npm:^1.0.4":
   version: 1.0.4
   resolution: "capital-case@npm:1.0.4"
@@ -7404,6 +8121,15 @@ __metadata:
   languageName: node
   linkType: hard
 
+"class-variance-authority@npm:0.7.0":
+  version: 0.7.0
+  resolution: "class-variance-authority@npm:0.7.0"
+  dependencies:
+    clsx: "npm:2.0.0"
+  checksum: 10/06646e82953e577fb8834100d763f1e5ecb808b8a1fba6244e94b20603865b134ff2296e30432449793baaeb02282cce617afba6981afe18b6846f1f8e9485ca
+  languageName: node
+  linkType: hard
+
 "classnames@npm:^2.3.2":
   version: 2.5.1
   resolution: "classnames@npm:2.5.1"
@@ -7494,6 +8220,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"clsx@npm:2.0.0, clsx@npm:^2.0.0":
+  version: 2.0.0
+  resolution: "clsx@npm:2.0.0"
+  checksum: 10/943766d1b02fee3538c871e56638d87f973fbc2d6291ce221215ea436fdecb9be97ad323f411839c2d52c45640c449b1a53fbfe7e8b3d529b4e263308b630c9a
+  languageName: node
+  linkType: hard
+
 "clsx@npm:2.1.0":
   version: 2.1.0
   resolution: "clsx@npm:2.1.0"
@@ -7515,13 +8248,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"clsx@npm:^2.0.0":
-  version: 2.0.0
-  resolution: "clsx@npm:2.0.0"
-  checksum: 10/943766d1b02fee3538c871e56638d87f973fbc2d6291ce221215ea436fdecb9be97ad323f411839c2d52c45640c449b1a53fbfe7e8b3d529b4e263308b630c9a
-  languageName: node
-  linkType: hard
-
 "color-convert@npm:^1.9.0":
   version: 1.9.3
   resolution: "color-convert@npm:1.9.3"
@@ -7821,6 +8547,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"crypto-js@npm:4.2.0":
+  version: 4.2.0
+  resolution: "crypto-js@npm:4.2.0"
+  checksum: 10/c7bcc56a6e01c3c397e95aa4a74e4241321f04677f9a618a8f48a63b5781617248afb9adb0629824792e7ec20ca0d4241a49b6b2938ae6f973ec4efc5c53c924
+  languageName: node
+  linkType: hard
+
 "css-box-model@npm:^1.2.1":
   version: 1.2.1
   resolution: "css-box-model@npm:1.2.1"
@@ -8037,6 +8770,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"dayjs@npm:1.11.12":
+  version: 1.11.12
+  resolution: "dayjs@npm:1.11.12"
+  checksum: 10/8ee7c1e14961fd08d40b788d0c0e930dc6288b3d32911bb911b2fb31bb703c262788164fbe678ee9e50e2a35268d667b8c8ba43fd1806771c1f404c300a2b428
+  languageName: node
+  linkType: hard
+
 "dayjs@npm:1.11.13":
   version: 1.11.13
   resolution: "dayjs@npm:1.11.13"
@@ -8339,6 +9079,111 @@ __metadata:
   languageName: node
   linkType: hard
 
+"drizzle-kit@npm:0.23.0":
+  version: 0.23.0
+  resolution: "drizzle-kit@npm:0.23.0"
+  dependencies:
+    "@esbuild-kit/esm-loader": "npm:^2.5.5"
+    esbuild: "npm:^0.19.7"
+    esbuild-register: "npm:^3.5.0"
+  bin:
+    drizzle-kit: bin.cjs
+  checksum: 10/e1e2403e1deed9dd9bfd96e100860c658b44d11ea50659e21c328640f22e6ec89a05345a09af8d94913a526a23693aa244d0964bdd30658bdb2ec6ec4998128d
+  languageName: node
+  linkType: hard
+
+"drizzle-orm@npm:0.32.1":
+  version: 0.32.1
+  resolution: "drizzle-orm@npm:0.32.1"
+  peerDependencies:
+    "@aws-sdk/client-rds-data": ">=3"
+    "@cloudflare/workers-types": ">=3"
+    "@electric-sql/pglite": ">=0.1.1"
+    "@libsql/client": "*"
+    "@neondatabase/serverless": ">=0.1"
+    "@op-engineering/op-sqlite": ">=2"
+    "@opentelemetry/api": ^1.4.1
+    "@planetscale/database": ">=1"
+    "@prisma/client": "*"
+    "@tidbcloud/serverless": "*"
+    "@types/better-sqlite3": "*"
+    "@types/pg": "*"
+    "@types/react": ">=18"
+    "@types/sql.js": "*"
+    "@vercel/postgres": ">=0.8.0"
+    "@xata.io/client": "*"
+    better-sqlite3: ">=7"
+    bun-types: "*"
+    expo-sqlite: ">=13.2.0"
+    knex: "*"
+    kysely: "*"
+    mysql2: ">=2"
+    pg: ">=8"
+    postgres: ">=3"
+    react: ">=18"
+    sql.js: ">=1"
+    sqlite3: ">=5"
+  peerDependenciesMeta:
+    "@aws-sdk/client-rds-data":
+      optional: true
+    "@cloudflare/workers-types":
+      optional: true
+    "@electric-sql/pglite":
+      optional: true
+    "@libsql/client":
+      optional: true
+    "@neondatabase/serverless":
+      optional: true
+    "@op-engineering/op-sqlite":
+      optional: true
+    "@opentelemetry/api":
+      optional: true
+    "@planetscale/database":
+      optional: true
+    "@prisma/client":
+      optional: true
+    "@tidbcloud/serverless":
+      optional: true
+    "@types/better-sqlite3":
+      optional: true
+    "@types/pg":
+      optional: true
+    "@types/react":
+      optional: true
+    "@types/sql.js":
+      optional: true
+    "@vercel/postgres":
+      optional: true
+    "@xata.io/client":
+      optional: true
+    better-sqlite3:
+      optional: true
+    bun-types:
+      optional: true
+    expo-sqlite:
+      optional: true
+    knex:
+      optional: true
+    kysely:
+      optional: true
+    mysql2:
+      optional: true
+    pg:
+      optional: true
+    postgres:
+      optional: true
+    prisma:
+      optional: true
+    react:
+      optional: true
+    sql.js:
+      optional: true
+    sqlite3:
+      optional: true
+  checksum: 10/a17a0bf757b67606d44fd938e89670b1595a75258a7685e642e428a64a0e6c867322da327312ff68b1f2439b1a4059cff8588108b369328309738910cc2c7697
+  languageName: node
+  linkType: hard
+
 "dset@npm:^3.1.2":
   version: 3.1.2
   resolution: "dset@npm:3.1.2"
@@ -8397,6 +9242,17 @@ __metadata:
   languageName: node
   linkType: hard
 
+"ejs@npm:^3.1.9":
+  version: 3.1.9
+  resolution: "ejs@npm:3.1.9"
+  dependencies:
+    jake: "npm:^10.8.5"
+  bin:
+    ejs: bin/cli.js
+  checksum: 10/71f56d37540d2c2d71701f0116710c676f75314a3e997ef8b83515d5d4d2b111c5a72725377caeecb928671bacb84a0d38135f345904812e989847057d59f21a
+  languageName: node
+  linkType: hard
+
 "electron-to-chromium@npm:^1.4.284":
   version: 1.4.356
   resolution: "electron-to-chromium@npm:1.4.356"
@@ -8412,9 +9268,18 @@ __metadata:
   linkType: hard
 
 "electron-to-chromium@npm:^1.4.820":
-  version: 1.5.2
-  resolution: "electron-to-chromium@npm:1.5.2"
-  checksum: 10/5b397518bf3347e39935d1bf9ff3dd37064619783419c0cb6507c53812b3cea7b2cfd9c54664e6fc36aae28a29562af6339fa8e8fe165845355056ce3df63bde
+  version: 1.4.830
+  resolution: "electron-to-chromium@npm:1.4.830"
+  checksum: 10/fc7cee451e4b2f5279a8dd57c26816eac6e73171fc96454d718dbdb4509865455d71e6b373c30ef77a378e90de3ebbfabb3501225625c225ec15db68adf81c65
+  languageName: node
+  linkType: hard
+
+"embla-carousel-autoplay@npm:8.1.7":
+  version: 8.1.7
+  resolution: "embla-carousel-autoplay@npm:8.1.7"
+  peerDependencies:
+    embla-carousel: 8.1.7
+  checksum: 10/8abc97d3405a55377813f247ccc040db94e6994a36b69a0a4c8017973376b4ed056d5501f085af0b051b815b0d55e7f9a360e7fa53e21c2919eba194976f6833
   languageName: node
   linkType: hard
 
@@ -8429,6 +9294,27 @@ __metadata:
   languageName: node
   linkType: hard
 
+"embla-carousel-react@npm:8.1.7":
+  version: 8.1.7
+  resolution: "embla-carousel-react@npm:8.1.7"
+  dependencies:
+    embla-carousel: "npm:8.1.7"
+    embla-carousel-reactive-utils: "npm:8.1.7"
+  peerDependencies:
+    react: ^16.8.0 || ^17.0.1 || ^18.0.0
+  checksum: 10/623a903e04d4707dabb8e7ce18b326be610b0394eb9237a78c738477c945f32fee4b5a3681289227080ace467a037ea5fabdae2f36cc8c02a9e5afc0e421308a
+  languageName: node
+  linkType: hard
+
+"embla-carousel-reactive-utils@npm:8.1.7":
+  version: 8.1.7
+  resolution: "embla-carousel-reactive-utils@npm:8.1.7"
+  peerDependencies:
+    embla-carousel: 8.1.7
+  checksum: 10/ad6b24dca37e7918dae32cc490e2fdcfa2ccbbc9f3059c7c76fd3b0ce8e5e2d6e9aa5fcd5338a20cf6908ae7dffeeec7547c798acda8bb7011ddfbb148a6f965
+  languageName: node
+  linkType: hard
+
 "embla-carousel@npm:7.1.0":
   version: 7.1.0
   resolution: "embla-carousel@npm:7.1.0"
@@ -8436,6 +9322,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"embla-carousel@npm:8.1.7":
+  version: 8.1.7
+  resolution: "embla-carousel@npm:8.1.7"
+  checksum: 10/9721d494c35a576254eb69d3d6cc6dea320a335ffd45dd1c71be54af29e5d2db616777d3808affad209cc4c5a1e488a0f2fc641ef8df83ea0f28c9720bf2c464
+  languageName: node
+  linkType: hard
+
 "emoji-regex@npm:^8.0.0":
   version: 8.0.0
   resolution: "emoji-regex@npm:8.0.0"
@@ -8591,6 +9484,17 @@ __metadata:
   languageName: node
   linkType: hard
 
+"esbuild-register@npm:^3.5.0":
+  version: 3.5.0
+  resolution: "esbuild-register@npm:3.5.0"
+  dependencies:
+    debug: "npm:^4.3.4"
+  peerDependencies:
+    esbuild: ">=0.12 <1"
+  checksum: 10/af6874ce9b5fcdb0974c9d9e9f16530a5b9bd80c699b2ba9d7ace33439c1af1be6948535c775d9a6439e2bf23fb31cfd54ac882cfa38308a3f182039f4b98a01
+  languageName: node
+  linkType: hard
+
 "esbuild@npm:0.17.6":
   version: 0.17.6
   resolution: "esbuild@npm:0.17.6"
@@ -8748,7 +9652,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"esbuild@npm:^0.18.10":
+"esbuild@npm:^0.18.10, esbuild@npm:~0.18.20":
   version: 0.18.20
   resolution: "esbuild@npm:0.18.20"
   dependencies:
@@ -8825,6 +9729,86 @@ __metadata:
   languageName: node
   linkType: hard
 
+"esbuild@npm:^0.19.3, esbuild@npm:^0.19.7":
+  version: 0.19.12
+  resolution: "esbuild@npm:0.19.12"
+  dependencies:
+    "@esbuild/aix-ppc64": "npm:0.19.12"
+    "@esbuild/android-arm": "npm:0.19.12"
+    "@esbuild/android-arm64": "npm:0.19.12"
+    "@esbuild/android-x64": "npm:0.19.12"
+    "@esbuild/darwin-arm64": "npm:0.19.12"
+    "@esbuild/darwin-x64": "npm:0.19.12"
+    "@esbuild/freebsd-arm64": "npm:0.19.12"
+    "@esbuild/freebsd-x64": "npm:0.19.12"
+    "@esbuild/linux-arm": "npm:0.19.12"
+    "@esbuild/linux-arm64": "npm:0.19.12"
+    "@esbuild/linux-ia32": "npm:0.19.12"
+    "@esbuild/linux-loong64": "npm:0.19.12"
+    "@esbuild/linux-mips64el": "npm:0.19.12"
+    "@esbuild/linux-ppc64": "npm:0.19.12"
+    "@esbuild/linux-riscv64": "npm:0.19.12"
+    "@esbuild/linux-s390x": "npm:0.19.12"
+    "@esbuild/linux-x64": "npm:0.19.12"
+    "@esbuild/netbsd-x64": "npm:0.19.12"
+    "@esbuild/openbsd-x64": "npm:0.19.12"
+    "@esbuild/sunos-x64": "npm:0.19.12"
+    "@esbuild/win32-arm64": "npm:0.19.12"
+    "@esbuild/win32-ia32": "npm:0.19.12"
+    "@esbuild/win32-x64": "npm:0.19.12"
+  dependenciesMeta:
+    "@esbuild/aix-ppc64":
+      optional: true
+    "@esbuild/android-arm":
+      optional: true
+    "@esbuild/android-arm64":
+      optional: true
+    "@esbuild/android-x64":
+      optional: true
+    "@esbuild/darwin-arm64":
+      optional: true
+    "@esbuild/darwin-x64":
+      optional: true
+    "@esbuild/freebsd-arm64":
+      optional: true
+    "@esbuild/freebsd-x64":
+      optional: true
+    "@esbuild/linux-arm":
+      optional: true
+    "@esbuild/linux-arm64":
+      optional: true
+    "@esbuild/linux-ia32":
+      optional: true
+    "@esbuild/linux-loong64":
+      optional: true
+    "@esbuild/linux-mips64el":
+      optional: true
+    "@esbuild/linux-ppc64":
+      optional: true
+    "@esbuild/linux-riscv64":
+      optional: true
+    "@esbuild/linux-s390x":
+      optional: true
+    "@esbuild/linux-x64":
+      optional: true
+    "@esbuild/netbsd-x64":
+      optional: true
+    "@esbuild/openbsd-x64":
+      optional: true
+    "@esbuild/sunos-x64":
+      optional: true
+    "@esbuild/win32-arm64":
+      optional: true
+    "@esbuild/win32-ia32":
+      optional: true
+    "@esbuild/win32-x64":
+      optional: true
+  bin:
+    esbuild: bin/esbuild
+  checksum: 10/861fa8eb2428e8d6521a4b7c7930139e3f45e8d51a86985cc29408172a41f6b18df7b3401e7e5e2d528cdf83742da601ddfdc77043ddc4f1c715a8ddb2d8a255
+  languageName: node
+  linkType: hard
+
 "esbuild@npm:^0.21.3":
   version: 0.21.5
   resolution: "esbuild@npm:0.21.5"
@@ -9476,7 +10460,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"fraction.js@npm:^4.2.0":
+"fraction.js@npm:^4.2.0, fraction.js@npm:^4.3.7":
   version: 4.3.7
   resolution: "fraction.js@npm:4.3.7"
   checksum: 10/bb5ebcdeeffcdc37b68ead3bdfc244e68de188e0c64e9702197333c72963b95cc798883ad16adc21588088b942bca5b6a6ff4aeb1362d19f6f3b629035dc15f5
@@ -9690,6 +10674,15 @@ __metadata:
   languageName: node
   linkType: hard
 
+"get-tsconfig@npm:^4.7.0":
+  version: 4.7.6
+  resolution: "get-tsconfig@npm:4.7.6"
+  dependencies:
+    resolve-pkg-maps: "npm:^1.0.0"
+  checksum: 10/32da95a89f3ddbabd2a2e36f2a4add51a5e3c2b28f32e3c81494fcdbd43b7d9b42baea77784e62d10f87bb564c5ee908416aabf4c5ca9cdbb2950aa3c247f124
+  languageName: node
+  linkType: hard
+
 "glob-parent@npm:^5.1.2, glob-parent@npm:~5.1.2":
   version: 5.1.2
   resolution: "glob-parent@npm:5.1.2"
@@ -10356,6 +11349,16 @@ __metadata:
   languageName: node
   linkType: hard
 
+"input-otp@npm:1.2.4":
+  version: 1.2.4
+  resolution: "input-otp@npm:1.2.4"
+  peerDependencies:
+    react: ^16.8 || ^17.0 || ^18.0
+    react-dom: ^16.8 || ^17.0 || ^18.0
+  checksum: 10/dde21c35334864f0add026f6a66e87eac572278c24edb72c97ba9541e5133f3d6750a4230b9a9c6093f4bd84633e4e417c8849ef844a565834392b9119b4110f
+  languageName: node
+  linkType: hard
+
 "inquirer@npm:^8.0.0":
   version: 8.2.5
   resolution: "inquirer@npm:8.2.5"
@@ -10676,6 +11679,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"isbot@npm:4.1.0":
+  version: 4.1.0
+  resolution: "isbot@npm:4.1.0"
+  checksum: 10/ee76a2c937e701dbc47043fe71ab887dd3e7a5ba7c61f12bb7053b5634c1e3d413325f1adee3bc615cb8b75b9a95e9c79f36c66f11809cc4108488cb8e21d1dc
+  languageName: node
+  linkType: hard
+
 "isbot@npm:5.1.17":
   version: 5.1.17
   resolution: "isbot@npm:5.1.17"
@@ -10775,7 +11785,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"jiti@npm:^1.19.1":
+"jiti@npm:^1.19.1, jiti@npm:^1.21.0":
   version: 1.21.6
   resolution: "jiti@npm:1.21.6"
   bin:
@@ -10791,6 +11801,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"jose@npm:^4.15.5":
+  version: 4.15.9
+  resolution: "jose@npm:4.15.9"
+  checksum: 10/256234b6f85cdc080b1331f2d475bd58c8ccf459cb20f70ac5e4200b271bce10002b1c2f8e5b96dd975d83065ae5a586d52cdf89d28471d56de5d297992f9905
+  languageName: node
+  linkType: hard
+
 "jotai@npm:2.9.3":
   version: 2.9.3
   resolution: "jotai@npm:2.9.3"
@@ -10942,6 +11959,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"jwt-decode@npm:4.0.0":
+  version: 4.0.0
+  resolution: "jwt-decode@npm:4.0.0"
+  checksum: 10/87b569e4a9a0067fb0d592bcf3b2ac3e638e49beee28620eeb07bef1b4470f4077dea68c15d191dd68e076846c3af8394be3bcaecffedc6e97433b221fdbbcf3
+  languageName: node
+  linkType: hard
+
 "kind-of@npm:^6.0.3":
   version: 6.0.3
   resolution: "kind-of@npm:6.0.3"
@@ -11086,6 +12110,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"lodash.castarray@npm:^4.4.0":
+  version: 4.4.0
+  resolution: "lodash.castarray@npm:4.4.0"
+  checksum: 10/fca8c7047e0ae2738b0b2503fb00157ae0ff6d8a1b716f87ed715b22560e09de438c75b65e01a7e44ceb41c5b31dce2eb576e46db04beb9c699c498e03cbd00f
+  languageName: node
+  linkType: hard
+
 "lodash.debounce@npm:^4.0.8":
   version: 4.0.8
   resolution: "lodash.debounce@npm:4.0.8"
@@ -11100,6 +12131,20 @@ __metadata:
   languageName: node
   linkType: hard
 
+"lodash.isplainobject@npm:^4.0.6":
+  version: 4.0.6
+  resolution: "lodash.isplainobject@npm:4.0.6"
+  checksum: 10/29c6351f281e0d9a1d58f1a4c8f4400924b4c79f18dfc4613624d7d54784df07efaff97c1ff2659f3e085ecf4fff493300adc4837553104cef2634110b0d5337
+  languageName: node
+  linkType: hard
+
+"lodash.merge@npm:^4.6.2":
+  version: 4.6.2
+  resolution: "lodash.merge@npm:4.6.2"
+  checksum: 10/d0ea2dd0097e6201be083865d50c3fb54fbfbdb247d9cc5950e086c991f448b7ab0cdab0d57eacccb43473d3f2acd21e134db39f22dac2d6c9ba6bf26978e3d6
+  languageName: node
+  linkType: hard
+
 "lodash.snakecase@npm:^4.1.1":
   version: 4.1.1
   resolution: "lodash.snakecase@npm:4.1.1"
@@ -11218,6 +12263,15 @@ __metadata:
   languageName: node
   linkType: hard
 
+"lucide-react@npm:0.414.0":
+  version: 0.414.0
+  resolution: "lucide-react@npm:0.414.0"
+  peerDependencies:
+    react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
+  checksum: 10/d2524481f14f690e15add2a517d5bcda1432029ae212e82fa8e8624bec0501f8131514d0a03519a60d9d543252f33d8ab0e7a592dee365ec9cc49fe403d5ff1b
+  languageName: node
+  linkType: hard
+
 "make-dir@npm:^2.1.0":
   version: 2.1.0
   resolution: "make-dir@npm:2.1.0"
@@ -12254,6 +13308,16 @@ __metadata:
   languageName: node
   linkType: hard
 
+"next-themes@npm:0.3.0":
+  version: 0.3.0
+  resolution: "next-themes@npm:0.3.0"
+  peerDependencies:
+    react: ^16.8 || ^17 || ^18
+    react-dom: ^16.8 || ^17 || ^18
+  checksum: 10/4285c4969eac517ad7addd773bcb71e7d14bc6c6e3b24eb97b80a6e06ac03fb6cb345e75dfb448156d14430d06289948eb8cfdeb52402ca7ce786093d01d2878
+  languageName: node
+  linkType: hard
+
 "next@npm:14.1.4":
   version: 14.1.4
   resolution: "next@npm:14.1.4"
@@ -12347,6 +13411,20 @@ __metadata:
   languageName: node
   linkType: hard
 
+"node-fetch@npm:^2.7.0":
+  version: 2.7.0
+  resolution: "node-fetch@npm:2.7.0"
+  dependencies:
+    whatwg-url: "npm:^5.0.0"
+  peerDependencies:
+    encoding: ^0.1.0
+  peerDependenciesMeta:
+    encoding:
+      optional: true
+  checksum: 10/b24f8a3dc937f388192e59bcf9d0857d7b6940a2496f328381641cb616efccc9866e89ec43f2ec956bbd6c3d3ee05524ce77fe7b29ccd34692b3a16f237d6676
+  languageName: node
+  linkType: hard
+
 "node-gyp@npm:latest":
   version: 9.3.1
   resolution: "node-gyp@npm:9.3.1"
@@ -12382,9 +13460,9 @@ __metadata:
   linkType: hard
 
 "node-releases@npm:^2.0.14":
-  version: 2.0.18
-  resolution: "node-releases@npm:2.0.18"
-  checksum: 10/241e5fa9556f1c12bafb83c6c3e94f8cf3d8f2f8f904906ecef6e10bcaa1d59aa61212d4651bec70052015fc54bd3fdcdbe7fc0f638a17e6685aa586c076ec4e
+  version: 2.0.17
+  resolution: "node-releases@npm:2.0.17"
+  checksum: 10/199e4128db3c415368abf344c0bff527c1300c67094b2d5592a67f9388b434cdad8bf76829dace65c9318fbb27cda8ecb91c5758057bbdf0b59e9408ab92408b
   languageName: node
   linkType: hard
 
@@ -12395,6 +13473,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"nodemailer@npm:6.9.14":
+  version: 6.9.14
+  resolution: "nodemailer@npm:6.9.14"
+  checksum: 10/749d1a3ef440d6147c37ad850f5be065d55d87cd46a4470372d4e443593838a5ddd78c69623817a804ddede3212fa28ac069b5b8266e892b27e1dcff75103def
+  languageName: node
+  linkType: hard
+
 "nopt@npm:^6.0.0":
   version: 6.0.0
   resolution: "nopt@npm:6.0.0"
@@ -12551,6 +13636,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"object-hash@npm:^2.2.0":
+  version: 2.2.0
+  resolution: "object-hash@npm:2.2.0"
+  checksum: 10/dee06b6271bf5769ae5f1a7386fdd52c1f18aae9fcb0b8d4bb1232f2d743d06cb5b662be42378b60a1c11829f96f3f86834a16bbaa57a085763295fff8b93e27
+  languageName: node
+  linkType: hard
+
 "object-hash@npm:^3.0.0":
   version: 3.0.0
   resolution: "object-hash@npm:3.0.0"
@@ -12565,6 +13657,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"oidc-token-hash@npm:^5.0.3":
+  version: 5.0.3
+  resolution: "oidc-token-hash@npm:5.0.3"
+  checksum: 10/35fa19aea9ff2c509029ec569d74b778c8a215b92bd5e6e9bc4ebbd7ab035f44304ff02430a6397c3fb7c1d15ebfa467807ca0bcd31d06ba610b47798287d303
+  languageName: node
+  linkType: hard
+
 "on-finished@npm:2.4.1":
   version: 2.4.1
   resolution: "on-finished@npm:2.4.1"
@@ -12608,6 +13707,18 @@ __metadata:
   languageName: node
   linkType: hard
 
+"openid-client@npm:5.6.5":
+  version: 5.6.5
+  resolution: "openid-client@npm:5.6.5"
+  dependencies:
+    jose: "npm:^4.15.5"
+    lru-cache: "npm:^6.0.0"
+    object-hash: "npm:^2.2.0"
+    oidc-token-hash: "npm:^5.0.3"
+  checksum: 10/e843f39b3eac07d9a415d8524745d722df0298708e58ac4e13b2642a34b2211591d2c9a58d6f4e29826b678856965454bcfa5148a030c7eef53178ceff48d8ee
+  languageName: node
+  linkType: hard
+
 "ora@npm:5.4.1, ora@npm:^5.4.1":
   version: 5.4.1
   resolution: "ora@npm:5.4.1"
@@ -13182,6 +14293,16 @@ __metadata:
   languageName: node
   linkType: hard
 
+"postcss-selector-parser@npm:6.0.10":
+  version: 6.0.10
+  resolution: "postcss-selector-parser@npm:6.0.10"
+  dependencies:
+    cssesc: "npm:^3.0.0"
+    util-deprecate: "npm:^1.0.2"
+  checksum: 10/f8ad9beb764a64b51a8027650e745a44ed7198f0b968b823db9563a54990924bcf9eb6fb59fbbb7eb05a89b2b6a24b81b2b7d60ecadda15b04a0024c7663f436
+  languageName: node
+  linkType: hard
+
 "postcss-selector-parser@npm:^6.0.11, postcss-selector-parser@npm:^6.0.2, postcss-selector-parser@npm:^6.0.4":
   version: 6.0.13
   resolution: "postcss-selector-parser@npm:6.0.13"
@@ -13263,6 +14384,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"postgres@npm:3.4.4":
+  version: 3.4.4
+  resolution: "postgres@npm:3.4.4"
+  checksum: 10/2b8c511f2dd679b91264bb3033c8d18d2ad10a5fc9aca2049eda13d6b68ae96fac45d47cfaeb0b66482ff18cf7175a5562e69a1cf259e892d063fb60d2178758
+  languageName: node
+  linkType: hard
+
 "prettier@npm:^2.7.1":
   version: 2.8.8
   resolution: "prettier@npm:2.8.8"
@@ -13534,6 +14662,18 @@ __metadata:
   languageName: node
   linkType: hard
 
+"react-dom@npm:18.2.0":
+  version: 18.2.0
+  resolution: "react-dom@npm:18.2.0"
+  dependencies:
+    loose-envify: "npm:^1.1.0"
+    scheduler: "npm:^0.23.0"
+  peerDependencies:
+    react: ^18.2.0
+  checksum: 10/ca5e7762ec8c17a472a3605b6f111895c9f87ac7d43a610ab7024f68cd833d08eda0625ce02ec7178cc1f3c957cf0b9273cdc17aa2cd02da87544331c43b1d21
+  languageName: node
+  linkType: hard
+
 "react-dom@npm:18.3.1, react-dom@npm:^18.2.0":
   version: 18.3.1
   resolution: "react-dom@npm:18.3.1"
@@ -13750,6 +14890,19 @@ __metadata:
   languageName: node
   linkType: hard
 
+"react-router-dom@npm:6.25.0":
+  version: 6.25.0
+  resolution: "react-router-dom@npm:6.25.0"
+  dependencies:
+    "@remix-run/router": "npm:1.18.0"
+    react-router: "npm:6.25.0"
+  peerDependencies:
+    react: ">=16.8"
+    react-dom: ">=16.8"
+  checksum: 10/4f74050c64980a5acd37b62dad2e877b0e757a641b310cb233e8772277e45d60c2e44a75d14572079fee60d5469002b98b46c569761a0b9b347d083804cb3b7b
+  languageName: node
+  linkType: hard
+
 "react-router-dom@npm:6.26.1":
   version: 6.26.1
   resolution: "react-router-dom@npm:6.26.1"
@@ -13763,6 +14916,17 @@ __metadata:
   languageName: node
   linkType: hard
 
+"react-router@npm:6.25.0":
+  version: 6.25.0
+  resolution: "react-router@npm:6.25.0"
+  dependencies:
+    "@remix-run/router": "npm:1.18.0"
+  peerDependencies:
+    react: ">=16.8"
+  checksum: 10/7885b90e3abb574b2843b3efefe481ed87c1dba2a17ad301dc0df5e6e93c08c56ccbafc24ee2fc43650e11c1d7d0304762e7c98d43f35e756af76f2587b95b80
+  languageName: node
+  linkType: hard
+
 "react-router@npm:6.26.1":
   version: 6.26.1
   resolution: "react-router@npm:6.26.1"
@@ -13853,6 +15017,15 @@ __metadata:
   languageName: node
   linkType: hard
 
+"react@npm:18.2.0":
+  version: 18.2.0
+  resolution: "react@npm:18.2.0"
+  dependencies:
+    loose-envify: "npm:^1.1.0"
+  checksum: 10/b9214a9bd79e99d08de55f8bef2b7fc8c39630be97c4e29d7be173d14a9a10670b5325e94485f74cd8bff4966ef3c78ee53c79a7b0b9b70cba20aa8973acc694
+  languageName: node
+  linkType: hard
+
 "react@npm:18.3.1, react@npm:^18.2.0":
   version: 18.3.1
   resolution: "react@npm:18.3.1"
@@ -14109,6 +15282,22 @@ __metadata:
   languageName: node
   linkType: hard
 
+"remix-routes@npm:1.7.6":
+  version: 1.7.6
+  resolution: "remix-routes@npm:1.7.6"
+  dependencies:
+    chokidar: "npm:^3.5.2"
+    ejs: "npm:^3.1.9"
+    meow: "npm:9.0.0"
+    mkdirp: "npm:^1.0.4"
+    slash: "npm:3"
+    typescript-remix-routes-plugin: "npm:1.0.1"
+  bin:
+    remix-routes: lib/cli.js
+  checksum: 10/115203419f84a158063b09b07ae80c567b7f6a164cad7a573c3c5f50bd5151e5ce4199175a0fbd50c86a31d1847b5607d43c66e35e7ec3ad337f47f7f1bd12af
+  languageName: node
+  linkType: hard
+
 "remix-routes@npm:1.7.7":
   version: 1.7.7
   resolution: "remix-routes@npm:1.7.7"
@@ -14221,6 +15410,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"resolve-pkg-maps@npm:^1.0.0":
+  version: 1.0.0
+  resolution: "resolve-pkg-maps@npm:1.0.0"
+  checksum: 10/0763150adf303040c304009231314d1e84c6e5ebfa2d82b7d94e96a6e82bacd1dcc0b58ae257315f3c8adb89a91d8d0f12928241cba2df1680fbe6f60bf99b0e
+  languageName: node
+  linkType: hard
+
 "resolve.exports@npm:^2.0.2":
   version: 2.0.2
   resolution: "resolve.exports@npm:2.0.2"
@@ -14310,6 +15506,69 @@ __metadata:
   languageName: node
   linkType: hard
 
+"rollup@npm:^4.2.0":
+  version: 4.19.0
+  resolution: "rollup@npm:4.19.0"
+  dependencies:
+    "@rollup/rollup-android-arm-eabi": "npm:4.19.0"
+    "@rollup/rollup-android-arm64": "npm:4.19.0"
+    "@rollup/rollup-darwin-arm64": "npm:4.19.0"
+    "@rollup/rollup-darwin-x64": "npm:4.19.0"
+    "@rollup/rollup-linux-arm-gnueabihf": "npm:4.19.0"
+    "@rollup/rollup-linux-arm-musleabihf": "npm:4.19.0"
+    "@rollup/rollup-linux-arm64-gnu": "npm:4.19.0"
+    "@rollup/rollup-linux-arm64-musl": "npm:4.19.0"
+    "@rollup/rollup-linux-powerpc64le-gnu": "npm:4.19.0"
+    "@rollup/rollup-linux-riscv64-gnu": "npm:4.19.0"
+    "@rollup/rollup-linux-s390x-gnu": "npm:4.19.0"
+    "@rollup/rollup-linux-x64-gnu": "npm:4.19.0"
+    "@rollup/rollup-linux-x64-musl": "npm:4.19.0"
+    "@rollup/rollup-win32-arm64-msvc": "npm:4.19.0"
+    "@rollup/rollup-win32-ia32-msvc": "npm:4.19.0"
+    "@rollup/rollup-win32-x64-msvc": "npm:4.19.0"
+    "@types/estree": "npm:1.0.5"
+    fsevents: "npm:~2.3.2"
+  dependenciesMeta:
+    "@rollup/rollup-android-arm-eabi":
+      optional: true
+    "@rollup/rollup-android-arm64":
+      optional: true
+    "@rollup/rollup-darwin-arm64":
+      optional: true
+    "@rollup/rollup-darwin-x64":
+      optional: true
+    "@rollup/rollup-linux-arm-gnueabihf":
+      optional: true
+    "@rollup/rollup-linux-arm-musleabihf":
+      optional: true
+    "@rollup/rollup-linux-arm64-gnu":
+      optional: true
+    "@rollup/rollup-linux-arm64-musl":
+      optional: true
+    "@rollup/rollup-linux-powerpc64le-gnu":
+      optional: true
+    "@rollup/rollup-linux-riscv64-gnu":
+      optional: true
+    "@rollup/rollup-linux-s390x-gnu":
+      optional: true
+    "@rollup/rollup-linux-x64-gnu":
+      optional: true
+    "@rollup/rollup-linux-x64-musl":
+      optional: true
+    "@rollup/rollup-win32-arm64-msvc":
+      optional: true
+    "@rollup/rollup-win32-ia32-msvc":
+      optional: true
+    "@rollup/rollup-win32-x64-msvc":
+      optional: true
+    fsevents:
+      optional: true
+  bin:
+    rollup: dist/bin/rollup
+  checksum: 10/a5f56e60d160e727f372fb0b0adbab03c1e5b858df7af62e626459687e6510d5b9685e4badef50bb6ffd916eaf53c1684a8e12ae959dacb8e6930c77a00a0f19
+  languageName: node
+  linkType: hard
+
 "rollup@npm:^4.20.0":
   version: 4.21.0
   resolution: "rollup@npm:4.21.0"
@@ -14455,7 +15714,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"scheduler@npm:^0.23.2":
+"scheduler@npm:^0.23.0, scheduler@npm:^0.23.2":
   version: 0.23.2
   resolution: "scheduler@npm:0.23.2"
   dependencies:
@@ -14812,6 +16071,16 @@ __metadata:
   languageName: node
   linkType: hard
 
+"sonner@npm:1.5.0":
+  version: 1.5.0
+  resolution: "sonner@npm:1.5.0"
+  peerDependencies:
+    react: ^18.0.0
+    react-dom: ^18.0.0
+  checksum: 10/f189ec3cacf294b875eeb349faefa4be90d2ff4dcde6dc73b3b77a4a8ed6ff393a1d6a1b1b90ef86de861a4c84d988aec09558cabd39fcb1befddd384c4010b0
+  languageName: node
+  linkType: hard
+
 "source-map-js@npm:1.0.2, source-map-js@npm:>=0.6.2 <2.0.0, source-map-js@npm:^1.0.2":
   version: 1.0.2
   resolution: "source-map-js@npm:1.0.2"
@@ -15209,6 +16478,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"tailwind-merge@npm:2.4.0":
+  version: 2.4.0
+  resolution: "tailwind-merge@npm:2.4.0"
+  checksum: 10/dda331a95146c9943dd18f152156fe7a5daf450a686d0562fb521ebc027273bd0c8f9503742668b3e90d27e887aa794ea5e79c38faa51216e19e9d0f1ec55925
+  languageName: node
+  linkType: hard
+
 "tailwind-merge@npm:^1.14.0":
   version: 1.14.0
   resolution: "tailwind-merge@npm:1.14.0"
@@ -15216,6 +16492,15 @@ __metadata:
   languageName: node
   linkType: hard
 
+"tailwindcss-animate@npm:1.0.7":
+  version: 1.0.7
+  resolution: "tailwindcss-animate@npm:1.0.7"
+  peerDependencies:
+    tailwindcss: "*"
+  checksum: 10/ef176fbb0bf9dca84178b35b6a9615cd756358ea80be9c575456d12ecd7f3c431e9e571915c7df72959dc798a730959e9a4739d59eab55d8cc6db390870ff0d2
+  languageName: node
+  linkType: hard
+
 "tailwindcss@npm:3.4.0":
   version: 3.4.0
   resolution: "tailwindcss@npm:3.4.0"
@@ -15249,6 +16534,39 @@ __metadata:
   languageName: node
   linkType: hard
 
+"tailwindcss@npm:3.4.4":
+  version: 3.4.4
+  resolution: "tailwindcss@npm:3.4.4"
+  dependencies:
+    "@alloc/quick-lru": "npm:^5.2.0"
+    arg: "npm:^5.0.2"
+    chokidar: "npm:^3.5.3"
+    didyoumean: "npm:^1.2.2"
+    dlv: "npm:^1.1.3"
+    fast-glob: "npm:^3.3.0"
+    glob-parent: "npm:^6.0.2"
+    is-glob: "npm:^4.0.3"
+    jiti: "npm:^1.21.0"
+    lilconfig: "npm:^2.1.0"
+    micromatch: "npm:^4.0.5"
+    normalize-path: "npm:^3.0.0"
+    object-hash: "npm:^3.0.0"
+    picocolors: "npm:^1.0.0"
+    postcss: "npm:^8.4.23"
+    postcss-import: "npm:^15.1.0"
+    postcss-js: "npm:^4.0.1"
+    postcss-load-config: "npm:^4.0.1"
+    postcss-nested: "npm:^6.0.1"
+    postcss-selector-parser: "npm:^6.0.11"
+    resolve: "npm:^1.22.2"
+    sucrase: "npm:^3.32.0"
+  bin:
+    tailwind: lib/cli.js
+    tailwindcss: lib/cli.js
+  checksum: 10/ab120014a68517c079fbeecba06c404ac94088a959b5b5e631214af4d87b332b6e4b28d8453f65eac9d94759a030ca581b1330f7d73cbf497883c4e2de083432
+  languageName: node
+  linkType: hard
+
 "tapable@npm:^2.1.1, tapable@npm:^2.2.0":
   version: 2.2.1
   resolution: "tapable@npm:2.2.1"
@@ -15482,6 +16800,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"ts-pattern@npm:5.2.0":
+  version: 5.2.0
+  resolution: "ts-pattern@npm:5.2.0"
+  checksum: 10/d85607629a46d93a6245b3b17238648fda49d3ab01366df4d049dd5b293876de599f0a810c9a62de13adad38028ffb336539f612bc2686a1f879cd1f3f671a0c
+  languageName: node
+  linkType: hard
+
 "ts-pattern@npm:5.3.1":
   version: 5.3.1
   resolution: "ts-pattern@npm:5.3.1"
@@ -15496,6 +16821,20 @@ __metadata:
   languageName: node
   linkType: hard
 
+"tsconfck@npm:^2.1.0":
+  version: 2.1.2
+  resolution: "tsconfck@npm:2.1.2"
+  peerDependencies:
+    typescript: ^4.3.5 || ^5.0.0
+  peerDependenciesMeta:
+    typescript:
+      optional: true
+  bin:
+    tsconfck: bin/tsconfck.js
+  checksum: 10/61df3b03b334a25eabb0a52e67a0c8d85770c631f2739db7703af8fdd102a2ebd598f1c851cc5fc6d6a59f2497a26c845be71c934ea16d838a3ff95a885034fb
+  languageName: node
+  linkType: hard
+
 "tsconfck@npm:^3.0.3":
   version: 3.0.3
   resolution: "tsconfck@npm:3.0.3"
@@ -15542,6 +16881,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"turbo-stream@npm:2.2.0":
+  version: 2.2.0
+  resolution: "turbo-stream@npm:2.2.0"
+  checksum: 10/1a61be95585d6c901c7f626497765e28c38332f6562f4206e8a295dca87297d044f2413f369764365a6cbb13aae3c21fee2aace4525ff32c1e787e418916914e
+  languageName: node
+  linkType: hard
+
 "turbo-stream@npm:2.3.0":
   version: 2.3.0
   resolution: "turbo-stream@npm:2.3.0"
@@ -16243,6 +17589,22 @@ __metadata:
   languageName: node
   linkType: hard
 
+"vite-tsconfig-paths@npm:4.2.1":
+  version: 4.2.1
+  resolution: "vite-tsconfig-paths@npm:4.2.1"
+  dependencies:
+    debug: "npm:^4.1.1"
+    globrex: "npm:^0.1.2"
+    tsconfck: "npm:^2.1.0"
+  peerDependencies:
+    vite: "*"
+  peerDependenciesMeta:
+    vite:
+      optional: true
+  checksum: 10/fd21e99a9bcd4ddb995727a9cf7b87ca95308b0b67e69bee2b836024aabe962b2206fbb62e66645638a1298fd171f8a2b0b55202da7cc801a31b3efa41a3fb2f
+  languageName: node
+  linkType: hard
+
 "vite-tsconfig-paths@npm:5.0.1":
   version: 5.0.1
   resolution: "vite-tsconfig-paths@npm:5.0.1"
@@ -16259,6 +17621,46 @@ __metadata:
   languageName: node
   linkType: hard
 
+"vite@npm:5.1.0":
+  version: 5.1.0
+  resolution: "vite@npm:5.1.0"
+  dependencies:
+    esbuild: "npm:^0.19.3"
+    fsevents: "npm:~2.3.3"
+    postcss: "npm:^8.4.35"
+    rollup: "npm:^4.2.0"
+  peerDependencies:
+    "@types/node": ^18.0.0 || >=20.0.0
+    less: "*"
+    lightningcss: ^1.21.0
+    sass: "*"
+    stylus: "*"
+    sugarss: "*"
+    terser: ^5.4.0
+  dependenciesMeta:
+    fsevents:
+      optional: true
+  peerDependenciesMeta:
+    "@types/node":
+      optional: true
+    less:
+      optional: true
+    lightningcss:
+      optional: true
+    sass:
+      optional: true
+    stylus:
+      optional: true
+    sugarss:
+      optional: true
+    terser:
+      optional: true
+  bin:
+    vite: bin/vite.js
+  checksum: 10/14d136f2e71d657cb55bec2a9330951e27e572ed79c4e79e3edc24decfa87f95664b8206614bfcf6a61db933667e554a8eed389291ad8af49de0784548a83a4c
+  languageName: node
+  linkType: hard
+
 "vite@npm:5.4.2":
   version: 5.4.2
   resolution: "vite@npm:5.4.2"
@@ -16726,7 +18128,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"zod@npm:3.23.8, zod@npm:^3.17.3, zod@npm:^3.22.4, zod@npm:^3.23.8":
+"zod@npm:3.23.8, zod@npm:^3.17.3, zod@npm:^3.22.4, zod@npm:^3.23.5, zod@npm:^3.23.8":
   version: 3.23.8
   resolution: "zod@npm:3.23.8"
   checksum: 10/846fd73e1af0def79c19d510ea9e4a795544a67d5b34b7e1c4d0425bf6bfd1c719446d94cdfa1721c1987d891321d61f779e8236fde517dc0e524aa851a6eff1


Test patch: