diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..4c49bd7
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1 @@
+.env
diff --git a/.env b/.env
new file mode 100644
index 0000000..2dff607
--- /dev/null
+++ b/.env
@@ -0,0 +1 @@
+NEXT_PUBLIC_BACKEND_URL="http://localhost:4000"
diff --git a/.releaserc.json b/.releaserc.json
index 02f7401..df9c7ae 100644
--- a/.releaserc.json
+++ b/.releaserc.json
@@ -7,7 +7,10 @@
       {
         "dockerImage": "gists-app",
         "dockerProject": "milou666",
-        "dockerCacheFrom": "milou666/gists-app"
+        "dockerCacheFrom": "milou666/gists-app",
+        "dockerArgs": {
+          "NEXT_PUBLIC_API_URL": "https://api-gists.courtcircuits.xyz"
+        }
       }
     ]
   ]
diff --git a/package.json b/package.json
index 1b9afd0..bed3ffb 100644
--- a/package.json
+++ b/package.json
@@ -11,21 +11,27 @@
   "dependencies": {
     "@radix-ui/react-slot": "^1.1.0",
     "@radix-ui/react-toast": "^1.2.1",
+    "@reduxjs/toolkit": "^2.2.7",
+    "@tanstack/query-core": "5.21.4",
+    "@tanstack/react-query": "5.21.4",
     "class-variance-authority": "^0.7.0",
     "clsx": "^2.1.1",
     "gsap": "^3.12.5",
     "input-otp": "^1.2.4",
+    "ky": "^1.5.0",
     "lucide-react": "^0.419.0",
     "next": "14.2.5",
     "next-themes": "^0.3.0",
     "react": "^18",
     "react-dom": "^18",
     "react-hook-form": "^7.52.1",
+    "react-redux": "^9.1.2",
     "tailwind-merge": "^2.4.0",
     "tailwindcss-animate": "^1.0.7"
   },
   "devDependencies": {
     "@codedependant/semantic-release-docker": "^5.0.3",
+    "@iconify/react": "^5.0.2",
     "@types/node": "^20",
     "@types/react": "^18",
     "@types/react-dom": "^18",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4a466f2..7ab5a99 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -14,6 +14,15 @@ importers:
       '@radix-ui/react-toast':
         specifier: ^1.2.1
         version: 1.2.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+      '@reduxjs/toolkit':
+        specifier: ^2.2.7
+        version: 2.2.7(react-redux@9.1.2(@types/react@18.3.3)(react@18.3.1)(redux@5.0.1))(react@18.3.1)
+      '@tanstack/query-core':
+        specifier: 5.21.4
+        version: 5.21.4
+      '@tanstack/react-query':
+        specifier: 5.21.4
+        version: 5.21.4(react@18.3.1)
       class-variance-authority:
         specifier: ^0.7.0
         version: 0.7.0
@@ -26,6 +35,9 @@ importers:
       input-otp:
         specifier: ^1.2.4
         version: 1.2.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+      ky:
+        specifier: ^1.5.0
+        version: 1.6.0
       lucide-react:
         specifier: ^0.419.0
         version: 0.419.0(react@18.3.1)
@@ -43,20 +55,26 @@ importers:
         version: 18.3.1(react@18.3.1)
       react-hook-form:
         specifier: ^7.52.1
-        version: 7.52.1(react@18.3.1)
+        version: 7.52.2(react@18.3.1)
+      react-redux:
+        specifier: ^9.1.2
+        version: 9.1.2(@types/react@18.3.3)(react@18.3.1)(redux@5.0.1)
       tailwind-merge:
         specifier: ^2.4.0
-        version: 2.4.0
+        version: 2.5.2
       tailwindcss-animate:
         specifier: ^1.0.7
-        version: 1.0.7(tailwindcss@3.4.7)
+        version: 1.0.7(tailwindcss@3.4.9)
     devDependencies:
       '@codedependant/semantic-release-docker':
         specifier: ^5.0.3
         version: 5.0.3
+      '@iconify/react':
+        specifier: ^5.0.2
+        version: 5.0.2(react@18.3.1)
       '@types/node':
         specifier: ^20
-        version: 20.14.13
+        version: 20.14.15
       '@types/react':
         specifier: ^18
         version: 18.3.3
@@ -71,10 +89,10 @@ importers:
         version: 14.2.5(eslint@8.57.0)(typescript@5.5.4)
       postcss:
         specifier: ^8
-        version: 8.4.40
+        version: 8.4.41
       tailwindcss:
         specifier: ^3.4.1
-        version: 3.4.7
+        version: 3.4.9
       typescript:
         specifier: ^5
         version: 5.5.4
@@ -119,6 +137,14 @@ packages:
     resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==}
     deprecated: Use @eslint/object-schema instead
 
+  '@iconify/react@5.0.2':
+    resolution: {integrity: sha512-wtmstbYlEbo4NDxFxBJkhkf9gJBDqMGr7FaqLrAUMneRV3Z+fVHLJjOhWbkAF8xDQNFC/wcTYdrWo1lnRhmagQ==}
+    peerDependencies:
+      react: '>=16'
+
+  '@iconify/types@2.0.0':
+    resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
+
   '@isaacs/cliui@8.0.2':
     resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
     engines: {node: '>=12'}
@@ -374,6 +400,17 @@ packages:
       '@types/react-dom':
         optional: true
 
+  '@reduxjs/toolkit@2.2.7':
+    resolution: {integrity: sha512-faI3cZbSdFb8yv9dhDTmGwclW0vk0z5o1cia+kf7gCbaCwHI5e+7tP57mJUv22pNcNbeA62GSrPpfrUfdXcQ6g==}
+    peerDependencies:
+      react: ^16.9.0 || ^17.0.0 || ^18
+      react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0
+    peerDependenciesMeta:
+      react:
+        optional: true
+      react-redux:
+        optional: true
+
   '@rushstack/eslint-patch@1.10.4':
     resolution: {integrity: sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA==}
 
@@ -387,11 +424,19 @@ packages:
   '@swc/helpers@0.5.5':
     resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==}
 
+  '@tanstack/query-core@5.21.4':
+    resolution: {integrity: sha512-k3u4RcDAtcCurs8KVEIf52k4yUayc852v4ZQrtI8pkEii71riM9758A2WVGo5T/v4/X7b1RLON5g0aDvkoZYCA==}
+
+  '@tanstack/react-query@5.21.4':
+    resolution: {integrity: sha512-tzl4mGerfPKmJsPDWbfKol0eBEk8bsgtMZJOwnbUvvSRnYZzS9OfX9CD/dbQLr+JjIvSekWKaDBTo3oXeeFhoQ==}
+    peerDependencies:
+      react: ^18.0.0
+
   '@types/json5@0.0.29':
     resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
 
-  '@types/node@20.14.13':
-    resolution: {integrity: sha512-+bHoGiZb8UiQ0+WEtmph2IWQCjIqg8MDZMAV+ppRRhUZnquF5mQkP/9vpSwJClEiSM/C7fZZExPzfU0vJTyp8w==}
+  '@types/node@20.14.15':
+    resolution: {integrity: sha512-Fz1xDMCF/B00/tYSVMlmK7hVeLh7jE5f3B7X1/hmV0MJBwE27KlS7EvD/Yp+z1lm8mVhwV5w+n8jOZG8AfTlKw==}
 
   '@types/prop-types@15.7.12':
     resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==}
@@ -402,6 +447,9 @@ packages:
   '@types/react@18.3.3':
     resolution: {integrity: sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==}
 
+  '@types/use-sync-external-store@0.0.3':
+    resolution: {integrity: sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==}
+
   '@typescript-eslint/parser@7.2.0':
     resolution: {integrity: sha512-5FKsVcHTk6TafQKQbuIVkXq58Fnbkd2wDL4LB7AURN7RUOu1utVP+G8+6u3ZhEroW3DF6hyo3ZEXxgKgp4KeCg==}
     engines: {node: ^16.0.0 || >=18.0.0}
@@ -564,8 +612,8 @@ packages:
     resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
     engines: {node: '>= 6'}
 
-  caniuse-lite@1.0.30001644:
-    resolution: {integrity: sha512-YGvlOZB4QhZuiis+ETS0VXR+MExbFf4fZYYeMTEE0aTQd/RdIjkTyZjLrbYVKnHzppDvnOhritRVv+i7Go6mHw==}
+  caniuse-lite@1.0.30001651:
+    resolution: {integrity: sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==}
 
   chalk@4.1.2:
     resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
@@ -877,8 +925,8 @@ packages:
   for-each@0.3.3:
     resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==}
 
-  foreground-child@3.2.1:
-    resolution: {integrity: sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==}
+  foreground-child@3.3.0:
+    resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==}
     engines: {node: '>=14'}
 
   fs.realpath@1.0.0:
@@ -994,10 +1042,13 @@ packages:
     resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==}
     engines: {node: '>=8.12.0'}
 
-  ignore@5.3.1:
-    resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==}
+  ignore@5.3.2:
+    resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
     engines: {node: '>= 4'}
 
+  immer@10.1.1:
+    resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==}
+
   import-fresh@3.3.0:
     resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==}
     engines: {node: '>=6'}
@@ -1187,6 +1238,10 @@ packages:
   keyv@4.5.4:
     resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
 
+  ky@1.6.0:
+    resolution: {integrity: sha512-MG7hlH26oShC4Lysk5TYzXshHLfEY52IJ0ofOeCsifquqTymbXCSTx+g4rXO30XYxoM6Y1ed5pNnpULe9Rx19A==}
+    engines: {node: '>=18'}
+
   language-subtag-registry@0.3.23:
     resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==}
 
@@ -1451,8 +1506,8 @@ packages:
     peerDependencies:
       postcss: ^8.2.14
 
-  postcss-selector-parser@6.1.1:
-    resolution: {integrity: sha512-b4dlw/9V8A71rLIDsSwVmak9z2DuBUB7CA1/wSdelNEzqsjoSPeADTWNO09lpH49Diy3/JIZ2bSPB1dI3LJCHg==}
+  postcss-selector-parser@6.1.2:
+    resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==}
     engines: {node: '>=4'}
 
   postcss-value-parser@4.2.0:
@@ -1462,8 +1517,8 @@ packages:
     resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
     engines: {node: ^10 || ^12 || >=14}
 
-  postcss@8.4.40:
-    resolution: {integrity: sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==}
+  postcss@8.4.41:
+    resolution: {integrity: sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==}
     engines: {node: ^10 || ^12 || >=14}
 
   prelude-ls@1.2.1:
@@ -1488,15 +1543,27 @@ packages:
     peerDependencies:
       react: ^18.3.1
 
-  react-hook-form@7.52.1:
-    resolution: {integrity: sha512-uNKIhaoICJ5KQALYZ4TOaOLElyM+xipord+Ha3crEFhTntdLvWZqVY49Wqd/0GiVCA/f9NjemLeiNPjG7Hpurg==}
-    engines: {node: '>=12.22.0'}
+  react-hook-form@7.52.2:
+    resolution: {integrity: sha512-pqfPEbERnxxiNMPd0bzmt1tuaPcVccywFDpyk2uV5xCIBphHV5T8SVnX9/o3kplPE1zzKt77+YIoq+EMwJp56A==}
+    engines: {node: '>=18.0.0'}
     peerDependencies:
       react: ^16.8.0 || ^17 || ^18 || ^19
 
   react-is@16.13.1:
     resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
 
+  react-redux@9.1.2:
+    resolution: {integrity: sha512-0OA4dhM1W48l3uzmv6B7TXPCGmokUU4p1M44DGN2/D9a1FjVPukVjER1PcPX97jIg6aUeLq1XJo1IpfbgULn0w==}
+    peerDependencies:
+      '@types/react': ^18.2.25
+      react: ^18.0
+      redux: ^5.0.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      redux:
+        optional: true
+
   react@18.3.1:
     resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
     engines: {node: '>=0.10.0'}
@@ -1508,6 +1575,14 @@ packages:
     resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
     engines: {node: '>=8.10.0'}
 
+  redux-thunk@3.1.0:
+    resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==}
+    peerDependencies:
+      redux: ^5.0.0
+
+  redux@5.0.1:
+    resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==}
+
   reflect.getprototypeof@1.0.6:
     resolution: {integrity: sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==}
     engines: {node: '>= 0.4'}
@@ -1516,6 +1591,9 @@ packages:
     resolution: {integrity: sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==}
     engines: {node: '>= 0.4'}
 
+  reselect@5.1.1:
+    resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==}
+
   resolve-from@4.0.0:
     resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
     engines: {node: '>=4'}
@@ -1685,16 +1763,16 @@ packages:
     resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
     engines: {node: '>= 0.4'}
 
-  tailwind-merge@2.4.0:
-    resolution: {integrity: sha512-49AwoOQNKdqKPd9CViyH5wJoSKsCDjUlzL8DxuGp3P1FsGY36NJDAa18jLZcaHAUUuTj+JB8IAo8zWgBNvBF7A==}
+  tailwind-merge@2.5.2:
+    resolution: {integrity: sha512-kjEBm+pvD+6eAwzJL2Bi+02/9LFLal1Gs61+QB7HvTfQQ0aXwC5LGT8PEt1gS0CWKktKe6ysPTAy3cBC5MeiIg==}
 
   tailwindcss-animate@1.0.7:
     resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==}
     peerDependencies:
       tailwindcss: '>=3.0.0 || insiders'
 
-  tailwindcss@3.4.7:
-    resolution: {integrity: sha512-rxWZbe87YJb4OcSopb7up2Ba4U82BoiSGUdoDr3Ydrg9ckxFS/YWsvhN323GMcddgU65QRy7JndC7ahhInhvlQ==}
+  tailwindcss@3.4.9:
+    resolution: {integrity: sha512-1SEOvRr6sSdV5IDf9iC+NU4dhwdqzF4zKKq3sAbasUWHEM6lsMhX+eNN5gkPx1BvLFEnZQEUFbXnGj8Qlp83Pg==}
     engines: {node: '>=14.0.0'}
     hasBin: true
 
@@ -1760,8 +1838,8 @@ packages:
     engines: {node: '>=14.17'}
     hasBin: true
 
-  uglify-js@3.19.1:
-    resolution: {integrity: sha512-y/2wiW+ceTYR2TSSptAhfnEtpLaQ4Ups5zrjB2d3kuVxHj16j/QJwPl5PvuGy9uARb39J0+iKxcRPvtpsx4A4A==}
+  uglify-js@3.19.2:
+    resolution: {integrity: sha512-S8KA6DDI47nQXJSi2ctQ629YzwOVs+bQML6DAtvy0wgNdpi+0ySpQK0g2pxBq2xfF2z3YCscu7NNA8nXT9PlIQ==}
     engines: {node: '>=0.8.0'}
     hasBin: true
 
@@ -1774,6 +1852,11 @@ packages:
   uri-js@4.4.1:
     resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
 
+  use-sync-external-store@1.2.2:
+    resolution: {integrity: sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==}
+    peerDependencies:
+      react: ^16.8.0 || ^17.0.0 || ^18.0.0
+
   util-deprecate@1.0.2:
     resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
 
@@ -1851,7 +1934,7 @@ snapshots:
       debug: 4.3.6
       espree: 9.6.1
       globals: 13.24.0
-      ignore: 5.3.1
+      ignore: 5.3.2
       import-fresh: 3.3.0
       js-yaml: 4.1.0
       minimatch: 3.1.2
@@ -1873,6 +1956,13 @@ snapshots:
 
   '@humanwhocodes/object-schema@2.0.3': {}
 
+  '@iconify/react@5.0.2(react@18.3.1)':
+    dependencies:
+      '@iconify/types': 2.0.0
+      react: 18.3.1
+
+  '@iconify/types@2.0.0': {}
+
   '@isaacs/cliui@8.0.2':
     dependencies:
       string-width: 5.1.2
@@ -2077,6 +2167,16 @@ snapshots:
       '@types/react': 18.3.3
       '@types/react-dom': 18.3.0
 
+  '@reduxjs/toolkit@2.2.7(react-redux@9.1.2(@types/react@18.3.3)(react@18.3.1)(redux@5.0.1))(react@18.3.1)':
+    dependencies:
+      immer: 10.1.1
+      redux: 5.0.1
+      redux-thunk: 3.1.0(redux@5.0.1)
+      reselect: 5.1.1
+    optionalDependencies:
+      react: 18.3.1
+      react-redux: 9.1.2(@types/react@18.3.3)(react@18.3.1)(redux@5.0.1)
+
   '@rushstack/eslint-patch@1.10.4': {}
 
   '@semantic-release/error@3.0.0': {}
@@ -2088,9 +2188,16 @@ snapshots:
       '@swc/counter': 0.1.3
       tslib: 2.6.3
 
+  '@tanstack/query-core@5.21.4': {}
+
+  '@tanstack/react-query@5.21.4(react@18.3.1)':
+    dependencies:
+      '@tanstack/query-core': 5.21.4
+      react: 18.3.1
+
   '@types/json5@0.0.29': {}
 
-  '@types/node@20.14.13':
+  '@types/node@20.14.15':
     dependencies:
       undici-types: 5.26.5
 
@@ -2105,6 +2212,8 @@ snapshots:
       '@types/prop-types': 15.7.12
       csstype: 3.1.3
 
+  '@types/use-sync-external-store@0.0.3': {}
+
   '@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4)':
     dependencies:
       '@typescript-eslint/scope-manager': 7.2.0
@@ -2297,7 +2406,7 @@ snapshots:
 
   camelcase-css@2.0.1: {}
 
-  caniuse-lite@1.0.30001644: {}
+  caniuse-lite@1.0.30001651: {}
 
   chalk@4.1.2:
     dependencies:
@@ -2554,7 +2663,7 @@ snapshots:
       eslint: 8.57.0
       eslint-import-resolver-node: 0.3.9
       eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0)
-      eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
+      eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
       eslint-plugin-jsx-a11y: 6.9.0(eslint@8.57.0)
       eslint-plugin-react: 7.35.0(eslint@8.57.0)
       eslint-plugin-react-hooks: 4.6.2(eslint@8.57.0)
@@ -2578,7 +2687,7 @@ snapshots:
       enhanced-resolve: 5.17.1
       eslint: 8.57.0
       eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
-      eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
+      eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
       fast-glob: 3.3.2
       get-tsconfig: 4.7.6
       is-core-module: 2.15.0
@@ -2600,7 +2709,7 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0):
+  eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0):
     dependencies:
       array-includes: 3.1.8
       array.prototype.findlastindex: 1.2.5
@@ -2707,7 +2816,7 @@ snapshots:
       glob-parent: 6.0.2
       globals: 13.24.0
       graphemer: 1.4.0
-      ignore: 5.3.1
+      ignore: 5.3.2
       imurmurhash: 0.1.4
       is-glob: 4.0.3
       is-path-inside: 3.0.3
@@ -2796,7 +2905,7 @@ snapshots:
     dependencies:
       is-callable: 1.2.7
 
-  foreground-child@3.2.1:
+  foreground-child@3.3.0:
     dependencies:
       cross-spawn: 7.0.3
       signal-exit: 4.1.0
@@ -2849,7 +2958,7 @@ snapshots:
 
   glob@10.3.10:
     dependencies:
-      foreground-child: 3.2.1
+      foreground-child: 3.3.0
       jackspeak: 2.3.6
       minimatch: 9.0.5
       minipass: 7.1.2
@@ -2857,7 +2966,7 @@ snapshots:
 
   glob@10.4.5:
     dependencies:
-      foreground-child: 3.2.1
+      foreground-child: 3.3.0
       jackspeak: 3.4.3
       minimatch: 9.0.5
       minipass: 7.1.2
@@ -2887,7 +2996,7 @@ snapshots:
       array-union: 2.1.0
       dir-glob: 3.0.1
       fast-glob: 3.3.2
-      ignore: 5.3.1
+      ignore: 5.3.2
       merge2: 1.4.1
       slash: 3.0.0
 
@@ -2908,7 +3017,7 @@ snapshots:
       source-map: 0.6.1
       wordwrap: 1.0.0
     optionalDependencies:
-      uglify-js: 3.19.1
+      uglify-js: 3.19.2
 
   has-bigints@1.0.2: {}
 
@@ -2932,7 +3041,9 @@ snapshots:
 
   human-signals@1.1.1: {}
 
-  ignore@5.3.1: {}
+  ignore@5.3.2: {}
+
+  immer@10.1.1: {}
 
   import-fresh@3.3.0:
     dependencies:
@@ -3117,6 +3228,8 @@ snapshots:
     dependencies:
       json-buffer: 3.0.1
 
+  ky@1.6.0: {}
+
   language-subtag-registry@0.3.23: {}
 
   language-tags@1.0.9:
@@ -3203,7 +3316,7 @@ snapshots:
       '@next/env': 14.2.5
       '@swc/helpers': 0.5.5
       busboy: 1.6.0
-      caniuse-lite: 1.0.30001644
+      caniuse-lite: 1.0.30001651
       graceful-fs: 4.2.11
       postcss: 8.4.31
       react: 18.3.1
@@ -3330,31 +3443,31 @@ snapshots:
 
   possible-typed-array-names@1.0.0: {}
 
-  postcss-import@15.1.0(postcss@8.4.40):
+  postcss-import@15.1.0(postcss@8.4.41):
     dependencies:
-      postcss: 8.4.40
+      postcss: 8.4.41
       postcss-value-parser: 4.2.0
       read-cache: 1.0.0
       resolve: 1.22.8
 
-  postcss-js@4.0.1(postcss@8.4.40):
+  postcss-js@4.0.1(postcss@8.4.41):
     dependencies:
       camelcase-css: 2.0.1
-      postcss: 8.4.40
+      postcss: 8.4.41
 
-  postcss-load-config@4.0.2(postcss@8.4.40):
+  postcss-load-config@4.0.2(postcss@8.4.41):
     dependencies:
       lilconfig: 3.1.2
       yaml: 2.5.0
     optionalDependencies:
-      postcss: 8.4.40
+      postcss: 8.4.41
 
-  postcss-nested@6.2.0(postcss@8.4.40):
+  postcss-nested@6.2.0(postcss@8.4.41):
     dependencies:
-      postcss: 8.4.40
-      postcss-selector-parser: 6.1.1
+      postcss: 8.4.41
+      postcss-selector-parser: 6.1.2
 
-  postcss-selector-parser@6.1.1:
+  postcss-selector-parser@6.1.2:
     dependencies:
       cssesc: 3.0.0
       util-deprecate: 1.0.2
@@ -3367,7 +3480,7 @@ snapshots:
       picocolors: 1.0.1
       source-map-js: 1.2.0
 
-  postcss@8.4.40:
+  postcss@8.4.41:
     dependencies:
       nanoid: 3.3.7
       picocolors: 1.0.1
@@ -3396,12 +3509,21 @@ snapshots:
       react: 18.3.1
       scheduler: 0.23.2
 
-  react-hook-form@7.52.1(react@18.3.1):
+  react-hook-form@7.52.2(react@18.3.1):
     dependencies:
       react: 18.3.1
 
   react-is@16.13.1: {}
 
+  react-redux@9.1.2(@types/react@18.3.3)(react@18.3.1)(redux@5.0.1):
+    dependencies:
+      '@types/use-sync-external-store': 0.0.3
+      react: 18.3.1
+      use-sync-external-store: 1.2.2(react@18.3.1)
+    optionalDependencies:
+      '@types/react': 18.3.3
+      redux: 5.0.1
+
   react@18.3.1:
     dependencies:
       loose-envify: 1.4.0
@@ -3414,6 +3536,12 @@ snapshots:
     dependencies:
       picomatch: 2.3.1
 
+  redux-thunk@3.1.0(redux@5.0.1):
+    dependencies:
+      redux: 5.0.1
+
+  redux@5.0.1: {}
+
   reflect.getprototypeof@1.0.6:
     dependencies:
       call-bind: 1.0.7
@@ -3431,6 +3559,8 @@ snapshots:
       es-errors: 1.3.0
       set-function-name: 2.0.2
 
+  reselect@5.1.1: {}
+
   resolve-from@4.0.0: {}
 
   resolve-pkg-maps@1.0.0: {}
@@ -3614,13 +3744,13 @@ snapshots:
 
   supports-preserve-symlinks-flag@1.0.0: {}
 
-  tailwind-merge@2.4.0: {}
+  tailwind-merge@2.5.2: {}
 
-  tailwindcss-animate@1.0.7(tailwindcss@3.4.7):
+  tailwindcss-animate@1.0.7(tailwindcss@3.4.9):
     dependencies:
-      tailwindcss: 3.4.7
+      tailwindcss: 3.4.9
 
-  tailwindcss@3.4.7:
+  tailwindcss@3.4.9:
     dependencies:
       '@alloc/quick-lru': 5.2.0
       arg: 5.0.2
@@ -3636,12 +3766,12 @@ snapshots:
       normalize-path: 3.0.0
       object-hash: 3.0.0
       picocolors: 1.0.1
-      postcss: 8.4.40
-      postcss-import: 15.1.0(postcss@8.4.40)
-      postcss-js: 4.0.1(postcss@8.4.40)
-      postcss-load-config: 4.0.2(postcss@8.4.40)
-      postcss-nested: 6.2.0(postcss@8.4.40)
-      postcss-selector-parser: 6.1.1
+      postcss: 8.4.41
+      postcss-import: 15.1.0(postcss@8.4.41)
+      postcss-js: 4.0.1(postcss@8.4.41)
+      postcss-load-config: 4.0.2(postcss@8.4.41)
+      postcss-nested: 6.2.0(postcss@8.4.41)
+      postcss-selector-parser: 6.1.2
       resolve: 1.22.8
       sucrase: 3.35.0
     transitivePeerDependencies:
@@ -3718,7 +3848,7 @@ snapshots:
 
   typescript@5.5.4: {}
 
-  uglify-js@3.19.1:
+  uglify-js@3.19.2:
     optional: true
 
   unbox-primitive@1.0.2:
@@ -3734,6 +3864,10 @@ snapshots:
     dependencies:
       punycode: 2.3.1
 
+  use-sync-external-store@1.2.2(react@18.3.1):
+    dependencies:
+      react: 18.3.1
+
   util-deprecate@1.0.2: {}
 
   which-boxed-primitive@1.0.2:
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 268fc15..3e5ba0b 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,35 +1,39 @@
-import type { Metadata } from 'next'
-import { Inter as FontSans } from 'next/font/google'
-import './globals.css'
-import { cn } from '@/lib/utils'
-import BlurBackground from '@/components/ui/blur-background'
-import ThemeWrapper from '@/components/theme/theme-wrapper'
-import { Toaster } from '@/components/shadcn/toaster'
-import { Providers } from '@/components/theme/theme-provider'
+import type { Metadata } from "next";
+import { Inter as FontSans } from "next/font/google";
+import "./globals.css";
+import { cn, getBackendURL } from "@/lib/utils";
+import BlurBackground from "@/components/ui/blur-background";
+import ThemeWrapper from "@/components/theme/theme-wrapper";
+import { Toaster } from "@/components/shadcn/toaster";
+import { Providers } from "@/components/theme/theme-provider";
+import QueryProvider from "@/components/api/api-provider";
 
-const fontSans = FontSans({ subsets: ['latin'] })
+const fontSans = FontSans({ subsets: ["latin"] });
 
 export const metadata: Metadata = {
-  title: 'Gists',
-  description: 'Create and share code snippets.',
-}
+  title: "Gists",
+  description: "Create and share code snippets.",
+};
 
 export default function RootLayout({
   children,
 }: Readonly<{
-  children: React.ReactNode
+  children: React.ReactNode;
 }>) {
+  console.log(getBackendURL());
   return (
     <html lang="en" suppressHydrationWarning={true}>
       <body className={cn(fontSans.className)}>
-        <Providers>
-          <ThemeWrapper>
-            <div className="z-10 flex justify-center">{children}</div>
-            <BlurBackground />
-            <Toaster />
-          </ThemeWrapper>
-        </Providers>
+        <QueryProvider>
+          <Providers>
+            <ThemeWrapper>
+              <div className="z-10 flex justify-center">{children}</div>
+              <BlurBackground />
+              <Toaster />
+            </ThemeWrapper>
+          </Providers>
+        </QueryProvider>
       </body>
     </html>
-  )
+  );
 }
diff --git a/src/app/login/login-feature.tsx b/src/app/login/login-feature.tsx
new file mode 100644
index 0000000..bdb00bd
--- /dev/null
+++ b/src/app/login/login-feature.tsx
@@ -0,0 +1,121 @@
+"use client";
+
+import { useEffect, useState, useCallback } from "react";
+import { useForm, UseFormRegisterReturn } from "react-hook-form";
+import { useToast } from "@/components/shadcn/use-toast";
+import { getBackendURL } from "@/lib/utils";
+import { useLocalAuth } from "@/lib/queries/auth.queries";
+import Login from "./login-ui";
+
+interface FormData {
+  email: string;
+}
+
+export default function LoginFeature() {
+  const [step, setStep] = useState<"initial" | "emailInput" | "otpInput">(
+    "initial",
+  );
+  const [otpValue, setOtpValue] = useState("");
+  const { toast } = useToast();
+
+  const { mutate: sendEmail } = useLocalAuth();
+  // const { mutate: verifyEmail, data: verified } = useLocalAuthVerify();
+
+  const {
+    register,
+    handleSubmit,
+    formState: { errors, isValid },
+  } = useForm<FormData>({
+    mode: "onChange",
+  });
+
+  const emailRegister = register("email", {
+    required: "Email is required",
+    pattern: {
+      value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
+      message: "Invalid email address",
+    },
+  });
+
+  const handleEmailClick = useCallback(() => {
+    if (step === "initial") {
+      setStep("emailInput");
+    } else if (step === "emailInput" && isValid) {
+      handleSubmit(onSubmit)();
+    }
+  }, [step, isValid, handleSubmit]);
+
+  const handleGitHubClick = useCallback(() => {
+    console.log("GitHub");
+    window.location.href = getBackendURL() + "/auth/github";
+  }, []);
+
+  const handleGoogleClick = useCallback(() => {
+    console.log("CGoogle");
+
+    window.location.href = getBackendURL() + "/auth/google";
+  }, []);
+
+  const handleOtpChange = useCallback((value: string) => {
+    setOtpValue(value);
+  }, []);
+
+  const handleContinueClick = useCallback(() => {
+    console.log("OTP:", otpValue);
+    const email = localStorage.getItem("email");
+    if (!email) {
+      console.error("Email not found in local storage.");
+      return;
+    }
+    localStorage.removeItem("email");
+    // verifyEmail({ email: email, token: otpValue });
+  }, [otpValue]);
+
+  const handleTryAgainClick = useCallback(() => {
+    console.log("A new OTP has been sent.");
+    toast({
+      title: "A new one time password has been sent.",
+      description: "Please check your email.",
+    });
+  }, [toast]);
+
+  const handleBackToLoginClick = useCallback(() => {
+    setStep("initial");
+    setOtpValue("");
+  }, []);
+
+  const onSubmit = (data: FormData) => {
+    console.log(data);
+    sendEmail(data.email);
+    localStorage.setItem("email", data.email);
+    setStep("otpInput");
+  };
+
+  // useEffect(() => {
+  //   if (verified) {
+  //     console.log("Verified:", verified);
+  //     toast({
+  //       title: "You have been verified.",
+  //     });
+  //
+  //     redirect("/dashboard");
+  //   }
+  // }, [verified, toast]);
+
+  return (
+    <Login
+      step={step}
+      email={emailRegister as UseFormRegisterReturn<"email">}
+      otpValue={otpValue}
+      onEmailClick={handleEmailClick}
+      onGitHubClick={handleGitHubClick}
+      onGoogleClick={handleGoogleClick}
+      onOtpChange={handleOtpChange}
+      onContinueClick={handleContinueClick}
+      onTryAgainClick={handleTryAgainClick}
+      onBackToLoginClick={handleBackToLoginClick}
+      isEmailValid={isValid}
+      emailError={errors.email?.message}
+    />
+  );
+}
diff --git a/src/app/login/login-ui.tsx b/src/app/login/login-ui.tsx
new file mode 100644
index 0000000..db31ef9
--- /dev/null
+++ b/src/app/login/login-ui.tsx
@@ -0,0 +1,210 @@
+import React, { useRef, useEffect, useState } from "react";
+import { gsap } from "gsap";
+import { Button } from "@/components/shadcn/button";
+import { Input } from "@/components/shadcn/input";
+import {
+  InputOTP,
+  InputOTPGroup,
+  InputOTPSlot,
+  InputOTPSeparator,
+} from "@/components/shadcn/input-otp";
+import { UseFormRegisterReturn } from "react-hook-form";
+import ThemeSwitch from "@/components/theme/theme-switch";
+import { cn } from "@/lib/utils";
+import { Icon } from "@iconify/react";
+
+interface LoginProps {
+  step: "initial" | "emailInput" | "otpInput";
+  email: UseFormRegisterReturn<"email">;
+  otpValue: string;
+  onEmailClick: () => void;
+  onGitHubClick: () => void;
+  onGoogleClick: () => void;
+  onOtpChange: (value: string) => void;
+  onContinueClick: () => void;
+  onTryAgainClick: () => void;
+  onBackToLoginClick: () => void;
+  isEmailValid: boolean;
+  emailError?: string;
+}
+
+export default function Login({
+  step,
+  email,
+  otpValue,
+  onEmailClick,
+  onGitHubClick,
+  onGoogleClick,
+  onOtpChange,
+  onContinueClick,
+  onTryAgainClick,
+  onBackToLoginClick,
+  isEmailValid,
+  emailError,
+}: LoginProps) {
+  const inputRef = useRef(null);
+  const otpContainerRef = useRef(null);
+  const loginContainerRef = useRef(null);
+  const [isAnimating, setIsAnimating] = useState(false);
+  const [shouldAnimate, setShouldAnimate] = useState(false);
+
+  useEffect(() => {
+    setShouldAnimate(true);
+  }, []);
+
+  useEffect(() => {
+    if (!shouldAnimate) return;
+
+    if (step === "emailInput" && inputRef.current) {
+      gsap.fromTo(
+        inputRef.current,
+        { opacity: 0, y: -20 },
+        { opacity: 1, y: 0, duration: 0.5, ease: "power2.out" },
+      );
+    }
+
+    if (step === "otpInput" && otpContainerRef.current) {
+      gsap.fromTo(
+        otpContainerRef.current,
+        { opacity: 0, y: 20 },
+        { opacity: 1, y: 0, duration: 0.5, ease: "power2.out" },
+      );
+    }
+
+    if (step === "initial" && loginContainerRef.current) {
+      gsap.fromTo(
+        loginContainerRef.current,
+        { opacity: 0, y: 20 },
+        { opacity: 1, y: 0, duration: 0.5, ease: "power2.out" },
+      );
+    }
+  }, [step, shouldAnimate]);
+
+  const handleBackToLogin = () => {
+    setIsAnimating(true);
+    gsap.to(otpContainerRef.current, {
+      opacity: 0,
+      y: 20,
+      duration: 0.5,
+      ease: "power2.in",
+      onComplete: () => {
+        onBackToLoginClick();
+        setIsAnimating(false);
+      },
+    });
+  };
+
+  const renderContent = () => {
+    if (step === "otpInput") {
+      return (
+        <div
+          ref={otpContainerRef}
+          className="flex flex-col justify-center items-center gap-8 p-2"
+        >
+          <h2>Check your email</h2>
+          <div className="flex flex-col text-center gap-2">
+            <span>We&apos;ve sent a temporary login code.</span>
+            <span>
+              Please check your inbox at <b>{email.name}</b>
+            </span>
+          </div>
+          <div className="flex flex-col gap-8">
+            <InputOTP maxLength={6} value={otpValue} onChange={onOtpChange}>
+              <InputOTPGroup>
+                <InputOTPSlot index={0} />
+                <InputOTPSlot index={1} />
+              </InputOTPGroup>
+              <InputOTPSeparator className="hidden md:block" />
+              <InputOTPGroup>
+                <InputOTPSlot index={2} />
+                <InputOTPSlot index={3} />
+              </InputOTPGroup>
+              <InputOTPSeparator className="hidden md:block" />
+              <InputOTPGroup>
+                <InputOTPSlot index={4} />
+                <InputOTPSlot index={5} />
+              </InputOTPGroup>
+            </InputOTP>
+            <Button onClick={onContinueClick} className="w-full">
+              Continue
+            </Button>
+          </div>
+          <span>
+            Nothing received ?{" "}
+            <Button
+              onClick={onTryAgainClick}
+              variant={"link"}
+              size={"no-padding"}
+              className="text-primary underline"
+            >
+              Try again !
+            </Button>
+          </span>
+          <Button
+            onClick={handleBackToLogin}
+            variant={"link"}
+            size={"no-padding"}
+            className="text-slate-500 font-bold hover:no-underline"
+            disabled={isAnimating}
+          >
+            Back to login
+          </Button>
+        </div>
+      );
+    }
+
+    return (
+      <div
+        ref={loginContainerRef}
+        className="flex flex-col justify-center items-center gap-8 w-96 p-2"
+      >
+        <h2>Log in to Gists</h2>
+        <div className="flex flex-col gap-4 w-full">
+          {step === "emailInput" && (
+            <div ref={inputRef} style={{ opacity: shouldAnimate ? 0 : 1 }}>
+              <Input placeholder="Enter your email address..." {...email} />
+              {emailError && (
+                <p className="text-red-500 text-sm mt-1">{emailError}</p>
+              )}
+            </div>
+          )}
+          <Button
+            onClick={onEmailClick}
+            disabled={step === "emailInput" && !isEmailValid}
+          >
+            Continue with email
+          </Button>
+          {step === "emailInput" && (
+            <div className="h-[1px] w-full bg-slate-800"></div>
+          )}
+
+          <Button
+            variant={"secondary"}
+            onClick={onGitHubClick}
+            className="gap-1"
+          >
+            <Icon icon="mdi:github" width={22} height={22} />
+            Continue with GitHub
+          </Button>
+          <Button
+            variant={"secondary"}
+            onClick={onGoogleClick}
+            className="gap-1"
+          >
+            <Icon icon="mdi:google" width={18} height={18} />
+            Continue with Google
+          </Button>
+        </div>
+      </div>
+    );
+  };
+
+  return (
+    <div className="relative h-screen w-full flex justify-center items-center">
+      <div className="absolute top-4 right-4 cursor-pointer">
+        <ThemeSwitch />
+      </div>
+      {renderContent()}
+    </div>
+  );
+}
diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx
index f2983b6..2efa74a 100644
--- a/src/app/login/page.tsx
+++ b/src/app/login/page.tsx
@@ -1,5 +1,7 @@
-import LoginFeature from '@/pages/feature/login-feature'
+"use client";
+
+import LoginFeature from "./login-feature";
 
 export default function LoginRoute() {
-  return <LoginFeature />
+  return <LoginFeature />;
 }
diff --git a/src/components/api/api-provider.tsx b/src/components/api/api-provider.tsx
new file mode 100644
index 0000000..6134fe1
--- /dev/null
+++ b/src/components/api/api-provider.tsx
@@ -0,0 +1,15 @@
+"use client";
+
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { useState } from "react";
+
+export default function QueryProvider({
+  children,
+}: {
+  children: React.ReactNode;
+}) {
+  const [queryClient] = useState<QueryClient>(() => new QueryClient());
+  return (
+    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
+  );
+}
diff --git a/src/components/theme/theme-provider.tsx b/src/components/theme/theme-provider.tsx
index ebabea3..1107645 100644
--- a/src/components/theme/theme-provider.tsx
+++ b/src/components/theme/theme-provider.tsx
@@ -1,10 +1,10 @@
-'use client'
-import { ThemeProvider } from 'next-themes'
+"use client";
+import { ThemeProvider } from "next-themes";
 
 export function Providers({ children }: { children: React.ReactNode }) {
   return (
     <ThemeProvider attribute="class" defaultTheme="dark" enableSystem>
-      {children}{' '}
+      {children}{" "}
     </ThemeProvider>
-  )
+  );
 }
diff --git a/src/lib/queries/auth.queries.tsx b/src/lib/queries/auth.queries.tsx
new file mode 100644
index 0000000..bde3853
--- /dev/null
+++ b/src/lib/queries/auth.queries.tsx
@@ -0,0 +1,76 @@
+"use client";
+import ky from "ky";
+import { getBackendURL } from "../utils";
+import { useMutation } from "@tanstack/react-query";
+import getQueryClient from "./queries";
+
+const fetchLocalAuth = async ({ email }: { email: string }) => {
+  const json = await ky
+    .post(`${getBackendURL()}/auth/local/begin`, {
+      json: { email },
+    })
+    .json();
+
+  return json; //no need to assert type since the most important is the status code
+};
+
+const fetchLocalAuthVerify = async ({
+  email,
+  token,
+}: {
+  email: string;
+  token: string;
+}) => {
+  const json = await ky
+    .post(`${getBackendURL()}/auth/local/verify`, {
+      json: { email, token },
+      credentials: "include",
+    })
+    .json();
+  return json;
+};
+
+/**
+ * Start the local authentication process by sending an email to the user
+ * @param email, the user email
+ * @returns the mutation object, and error if any and a boolean indicating the loading state
+ */
+export const useLocalAuth = () => {
+  const { mutate, error, isPending } = useMutation({
+    mutationFn: (email: string) => {
+      return fetchLocalAuth({ email });
+    },
+  });
+  return { mutate, error, isPending };
+};
+
+export const prefetchLocalAuth = async (email: string) => {
+  const queryClient = getQueryClient();
+  return await queryClient.prefetchQuery({
+    queryKey: ["localAuth", email],
+    queryFn: () => fetchLocalAuth({ email }),
+  });
+};
+
+export const useLocalAuthVerify = () => {
+  const { mutate, error, isPending, data } = useMutation({
+    mutationFn: ({ email, token }: { email: string; token: string }) => {
+      return fetchLocalAuthVerify({ email, token });
+    },
+  });
+  return { mutate, error, isPending, data };
+};
+
+export const prefetchLocalAuthVerify = async ({
+  email,
+  token,
+}: {
+  email: string;
+  token: string;
+}) => {
+  const queryClient = getQueryClient();
+  return await queryClient.prefetchQuery({
+    queryKey: ["localAuthVerify", email, token],
+    queryFn: () => fetchLocalAuthVerify({ email, token }),
+  });
+};
diff --git a/src/lib/queries/queries.tsx b/src/lib/queries/queries.tsx
new file mode 100644
index 0000000..0a5442f
--- /dev/null
+++ b/src/lib/queries/queries.tsx
@@ -0,0 +1,8 @@
+"use client";
+import { QueryClient } from "@tanstack/react-query";
+import { cache } from "react";
+
+export const getQueryClient = () => {
+  return new QueryClient();
+};
+export default getQueryClient;
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index d084cca..8c5ea97 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -1,6 +1,12 @@
-import { type ClassValue, clsx } from "clsx"
-import { twMerge } from "tailwind-merge"
+import { type ClassValue, clsx } from "clsx";
+import { twMerge } from "tailwind-merge";
 
 export function cn(...inputs: ClassValue[]) {
-  return twMerge(clsx(inputs))
+  return twMerge(clsx(inputs));
+}
+
+export function getBackendURL() {
+  return (
+    process.env.NEXT_PUBLIC_BACKEND_URL || "https://api-gists.courtcircuits.xyz"
+  );
 }
diff --git a/src/middleware.ts b/src/middleware.ts
new file mode 100644
index 0000000..df21206
--- /dev/null
+++ b/src/middleware.ts
@@ -0,0 +1,19 @@
+import type { NextRequest } from "next/server";
+
+export function middleware(request: NextRequest) {
+  const accessToken = request.cookies.get("gists.access_token")?.value;
+
+  console.log("accessToken", accessToken);
+
+  if (accessToken && !request.nextUrl.pathname.startsWith("/dashboard")) {
+    return Response.redirect(new URL("/dashboard", request.url));
+  }
+
+  if (!accessToken && !request.nextUrl.pathname.startsWith("/login")) {
+    return Response.redirect(new URL("/login", request.url));
+  }
+}
+
+export const config = {
+  matcher: ["/((?!api|_next/static|_next/image|.*\\.png$).*)"],
+};
diff --git a/src/pages/feature/login-feature.tsx b/src/pages/feature/login-feature.tsx
deleted file mode 100644
index e3d14b6..0000000
--- a/src/pages/feature/login-feature.tsx
+++ /dev/null
@@ -1,88 +0,0 @@
-'use client'
-
-import { useState, useCallback } from 'react'
-import { useForm, UseFormRegisterReturn } from 'react-hook-form'
-import Login from '../ui/login'
-import { useToast } from '@/components/shadcn/use-toast'
-
-interface FormData {
-  email: string
-}
-
-export default function LoginFeature() {
-  const [step, setStep] = useState<'initial' | 'emailInput' | 'otpInput'>('initial')
-  const [otpValue, setOtpValue] = useState('')
-  const { toast } = useToast()
-
-  const {
-    register,
-    handleSubmit,
-    formState: { errors, isValid },
-  } = useForm<FormData>({
-    mode: 'onChange',
-  })
-
-  const emailRegister = register('email', {
-    required: 'Email is required',
-    pattern: {
-      value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
-      message: 'Invalid email address',
-    },
-  })
-
-  const handleEmailClick = useCallback(() => {
-    if (step === 'initial') {
-      setStep('emailInput')
-    } else if (step === 'emailInput' && isValid) {
-      handleSubmit(onSubmit)()
-    }
-  }, [step, isValid, handleSubmit])
-
-  const handleGitHubClick = useCallback(() => {
-    console.log('GitHub')
-  }, [])
-
-  const handleGoogleClick = useCallback(() => {
-    console.log('CGoogle')
-  }, [])
-
-  const handleOtpChange = useCallback((value: string) => {
-    setOtpValue(value)
-  }, [])
-
-  const handleContinueClick = useCallback(() => {
-    console.log('OTP:', otpValue)
-  }, [otpValue])
-
-  const handleTryAgainClick = useCallback(() => {
-    console.log('A new OTP has been sent.')
-    toast({ title: 'A new one time password has been sent.', description: 'Please check your email.' })
-  }, [toast])
-
-  const handleBackToLoginClick = useCallback(() => {
-    setStep('initial')
-    setOtpValue('')
-  }, [])
-
-  const onSubmit = (data: FormData) => {
-    console.log(data)
-    setStep('otpInput')
-  }
-
-  return (
-    <Login
-      step={step}
-      email={emailRegister as UseFormRegisterReturn<'email'>}
-      otpValue={otpValue}
-      onEmailClick={handleEmailClick}
-      onGitHubClick={handleGitHubClick}
-      onGoogleClick={handleGoogleClick}
-      onOtpChange={handleOtpChange}
-      onContinueClick={handleContinueClick}
-      onTryAgainClick={handleTryAgainClick}
-      onBackToLoginClick={handleBackToLoginClick}
-      isEmailValid={isValid}
-      emailError={errors.email?.message}
-    />
-  )
-}
diff --git a/src/pages/ui/login.tsx b/src/pages/ui/login.tsx
deleted file mode 100644
index 8dbf593..0000000
--- a/src/pages/ui/login.tsx
+++ /dev/null
@@ -1,158 +0,0 @@
-import React, { useRef, useEffect, useState } from 'react'
-import { gsap } from 'gsap'
-import { Button } from '@/components/shadcn/button'
-import { Input } from '@/components/shadcn/input'
-import { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator } from '@/components/shadcn/input-otp'
-import { UseFormRegisterReturn } from 'react-hook-form'
-import ThemeSwitch from '@/components/theme/theme-switch'
-import { cn } from '@/lib/utils'
-
-interface LoginProps {
-  step: 'initial' | 'emailInput' | 'otpInput'
-  email: UseFormRegisterReturn<'email'>
-  otpValue: string
-  onEmailClick: () => void
-  onGitHubClick: () => void
-  onGoogleClick: () => void
-  onOtpChange: (value: string) => void
-  onContinueClick: () => void
-  onTryAgainClick: () => void
-  onBackToLoginClick: () => void
-  isEmailValid: boolean
-  emailError?: string
-}
-
-export default function Login({
-  step,
-  email,
-  otpValue,
-  onEmailClick,
-  onGitHubClick,
-  onGoogleClick,
-  onOtpChange,
-  onContinueClick,
-  onTryAgainClick,
-  onBackToLoginClick,
-  isEmailValid,
-  emailError,
-}: LoginProps) {
-  const inputRef = useRef(null)
-  const otpContainerRef = useRef(null)
-  const loginContainerRef = useRef(null)
-  const [isAnimating, setIsAnimating] = useState(false)
-  const [shouldAnimate, setShouldAnimate] = useState(false)
-
-  useEffect(() => {
-    setShouldAnimate(true)
-  }, [])
-
-  useEffect(() => {
-    if (!shouldAnimate) return
-
-    if (step === 'emailInput' && inputRef.current) {
-      gsap.fromTo(inputRef.current, { opacity: 0, y: -20 }, { opacity: 1, y: 0, duration: 0.5, ease: 'power2.out' })
-    }
-
-    if (step === 'otpInput' && otpContainerRef.current) {
-      gsap.fromTo(otpContainerRef.current, { opacity: 0, y: 20 }, { opacity: 1, y: 0, duration: 0.5, ease: 'power2.out' })
-    }
-
-    if (step === 'initial' && loginContainerRef.current) {
-      gsap.fromTo(loginContainerRef.current, { opacity: 0, y: 20 }, { opacity: 1, y: 0, duration: 0.5, ease: 'power2.out' })
-    }
-  }, [step, shouldAnimate])
-
-  const handleBackToLogin = () => {
-    setIsAnimating(true)
-    gsap.to(otpContainerRef.current, {
-      opacity: 0,
-      y: 20,
-      duration: 0.5,
-      ease: 'power2.in',
-      onComplete: () => {
-        onBackToLoginClick()
-        setIsAnimating(false)
-      },
-    })
-  }
-
-  const renderContent = () => {
-    if (step === 'otpInput') {
-      return (
-        <div ref={otpContainerRef} className="flex flex-col justify-center items-center gap-8 p-2">
-          <h2>Check your email</h2>
-          <div className="flex flex-col text-center gap-2">
-            <span>We&apos;ve sent a temporary login code.</span>
-            <span>
-              Please check your inbox at <b>{email.name}</b>
-            </span>
-          </div>
-          <div className="flex flex-col gap-8">
-            <InputOTP maxLength={6} value={otpValue} onChange={onOtpChange}>
-              <InputOTPGroup>
-                <InputOTPSlot index={0} />
-                <InputOTPSlot index={1} />
-              </InputOTPGroup>
-              <InputOTPSeparator className="hidden md:block" />
-              <InputOTPGroup>
-                <InputOTPSlot index={2} />
-                <InputOTPSlot index={3} />
-              </InputOTPGroup>
-              <InputOTPSeparator className="hidden md:block" />
-              <InputOTPGroup>
-                <InputOTPSlot index={4} />
-                <InputOTPSlot index={5} />
-              </InputOTPGroup>
-            </InputOTP>
-            <Button onClick={onContinueClick} className="w-full">
-              Continue
-            </Button>
-          </div>
-          <span>
-            Nothing received ?{' '}
-            <Button onClick={onTryAgainClick} variant={'link'} size={'no-padding'} className="text-primary underline">
-              Try again !
-            </Button>
-          </span>
-          <Button onClick={handleBackToLogin} variant={'link'} size={'no-padding'} className="text-slate-500 font-bold hover:no-underline" disabled={isAnimating}>
-            Back to login
-          </Button>
-        </div>
-      )
-    }
-
-    return (
-      <div ref={loginContainerRef} className="flex flex-col justify-center items-center gap-8 w-96 p-2">
-        <h2>Log in to Gists</h2>
-        <div className="flex flex-col gap-4 w-full">
-          {step === 'emailInput' && (
-            <div ref={inputRef} style={{ opacity: shouldAnimate ? 0 : 1 }}>
-              <Input placeholder="Enter your email address..." {...email} />
-              {emailError && <p className="text-red-500 text-sm mt-1">{emailError}</p>}
-            </div>
-          )}
-          <Button onClick={onEmailClick} disabled={step === 'emailInput' && !isEmailValid}>
-            {step === 'emailInput' ? 'Continue with Email' : 'Log in with Email'}
-          </Button>
-          {step === 'emailInput' && <div className="h-[1px] w-full bg-slate-800"></div>}
-
-          <Button variant={'secondary'} onClick={onGitHubClick}>
-            Log in with GitHub
-          </Button>
-          <Button variant={'secondary'} onClick={onGoogleClick}>
-            Log in with Google
-          </Button>
-        </div>
-      </div>
-    )
-  }
-
-  return (
-    <div className="relative h-screen w-full flex justify-center items-center">
-      <div className="absolute top-4 right-4 cursor-pointer">
-        <ThemeSwitch />
-      </div>
-      {renderContent()}
-    </div>
-  )
-}