From fd44aceb3113b31132ab59facc49fc601a882261 Mon Sep 17 00:00:00 2001
From: Philipp Nowinski <mail@pno.dev>
Date: Wed, 10 Jun 2026 15:55:51 +0200
Subject: [PATCH 1/5] feat: performance optimizations

---
 package.json                                  |   3 +
 pnpm-lock.yaml                                | 199 +++++++++++++++++
 src/components/OpenTask.tsx                   |  61 ++++--
 src/components/TaskCard.tsx                   |  98 ++++-----
 src/components/views/BoardView.tsx            |  39 ++--
 src/components/views/ListView.tsx             |  19 +-
 src/components/views/TaskViewsContainer.tsx   |  61 +++++-
 src/components/views/VirtualizedTaskCards.tsx |  49 +++++
 .../views/VirtualizedTaskListRows.tsx         |  44 ++++
 src/components/views/task-view-types.ts       |   3 +
 src/db/mutations/attachments.ts               |   9 +-
 src/db/mutations/comments.ts                  |   9 +-
 src/db/mutations/labels.ts                    |  18 +-
 src/db/mutations/projects.ts                  |  13 +-
 src/db/mutations/sprints.ts                   |  13 +-
 src/db/mutations/statuses.ts                  |  18 +-
 src/db/mutations/sub-tasks.ts                 |  21 +-
 src/db/mutations/sync.ts                      |  61 ++++--
 src/db/mutations/tasks.ts                     | 141 ++++++------
 src/db/queries/attachments.ts                 |  17 +-
 src/db/queries/bundles.ts                     |  24 ++-
 src/db/queries/comments.ts                    |  15 +-
 src/db/queries/dashboard.ts                   |  13 +-
 src/db/queries/hydrate-query-cache.ts         |  12 ++
 src/db/queries/labels.ts                      |  33 +--
 src/db/queries/notifications.ts               |  13 +-
 src/db/queries/projects.ts                    |  29 +--
 src/db/queries/sprints.ts                     |  29 +--
 src/db/queries/statuses.ts                    |  33 +--
 src/db/queries/tasks.ts                       |  57 +----
 src/hooks/live-sync-provider.tsx              | 200 ++++++++++++++++++
 src/hooks/use-event-source.ts                 |  35 ---
 src/hooks/use-git-task-live-sync.ts           |  25 +--
 src/lib/query-cache-fresh.ts                  |  15 ++
 src/router.tsx                                |   3 +
 src/routes/_signed-in.tsx                     |  38 +++-
 src/routes/_signed-in/dashboard.tsx           |  10 +-
 .../_signed-in/projects.$projectId.tasks.tsx  |  19 +-
 .../_signed-in/sprints.$sprintId.tasks.tsx    |  19 +-
 src/routes/_signed-in/tasks.tsx               |   8 +-
 vite.config.ts                                |   8 +
 41 files changed, 964 insertions(+), 570 deletions(-)
 create mode 100644 src/components/views/VirtualizedTaskCards.tsx
 create mode 100644 src/components/views/VirtualizedTaskListRows.tsx
 create mode 100644 src/hooks/live-sync-provider.tsx
 delete mode 100644 src/hooks/use-event-source.ts
 create mode 100644 src/lib/query-cache-fresh.ts

diff --git a/package.json b/package.json
index 726b93d..8f9ddc5 100644
--- a/package.json
+++ b/package.json
@@ -8,6 +8,7 @@
   "scripts": {
     "dev": "vite dev --port 3001",
     "build": "node scripts/compute-app-version.mjs && vite build && tsc --noEmit",
+    "build:analyze": "ANALYZE=true node scripts/compute-app-version.mjs && vite build",
     "lint": "eslint src --max-warnings 0",
     "test": "vitest run",
     "preview": "vite preview",
@@ -52,6 +53,7 @@
     "@tanstack/react-router-ssr-query": "^1.167.0",
     "@tanstack/react-start": "^1.167.0",
     "@tanstack/react-store": "^0.9.3",
+    "@tanstack/react-virtual": "^3.14.2",
     "@tiptap/core": "^2.25.0",
     "@tiptap/extension-text-style": "^2.25.0",
     "@tiptap/react": "^2.25.0",
@@ -103,6 +105,7 @@
     "eslint-plugin-react-hooks": "^7.1.1",
     "eslint-plugin-react-refresh": "^0.5.2",
     "globals": "^17.6.0",
+    "rollup-plugin-visualizer": "^7.0.1",
     "semantic-release": "^24.2.7",
     "semver": "^7.7.2",
     "tailwind-scrollbar": "^4.0.2",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4fbd6af..9e16a5b 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -110,6 +110,9 @@ importers:
       '@tanstack/react-store':
         specifier: ^0.9.3
         version: 0.9.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+      '@tanstack/react-virtual':
+        specifier: ^3.14.2
+        version: 3.14.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
       '@tiptap/core':
         specifier: ^2.25.0
         version: 2.27.2(@tiptap/pm@2.27.2)
@@ -258,6 +261,9 @@ importers:
       globals:
         specifier: ^17.6.0
         version: 17.6.0
+      rollup-plugin-visualizer:
+        specifier: ^7.0.1
+        version: 7.0.1(rolldown@1.0.1)(rollup@4.61.0)
       semantic-release:
         specifier: ^24.2.7
         version: 24.2.9(typescript@5.9.3)
@@ -3215,6 +3221,12 @@ packages:
       react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
       react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
 
+  '@tanstack/react-virtual@3.14.2':
+    resolution: {integrity: sha512-IpWnmCLvuymRfeeLNVXIzNEYBFLpd3drVIS91sqV78VTZFyldlChkOocZRCPp1B+Wnk09bcLNme8WaMU/9/9bQ==}
+    peerDependencies:
+      react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+      react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
   '@tanstack/router-core@1.171.2':
     resolution: {integrity: sha512-sUd+BhGYkBF64LVhmOHnYsc1AutPNch/huohEXiXL4IUgmk17Gy+RkUazvjQhptVdYW5QT+qtATrUr2cQZNHFA==}
     engines: {node: '>=20.19'}
@@ -3296,6 +3308,9 @@ packages:
   '@tanstack/store@0.9.3':
     resolution: {integrity: sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw==}
 
+  '@tanstack/virtual-core@3.17.0':
+    resolution: {integrity: sha512-gOxY/hFkPh/XQYhnThBHzkbkX3Ed+z/iushyz+R+JAr213aXxUDgQoTgTdrDpBSRsjFM73P/KfUyWmaF9WHMkQ==}
+
   '@tanstack/virtual-file-routes@1.162.0':
     resolution: {integrity: sha512-uhOeFyxLcU41HzvrxsGpiWdcMbScY1EDgbZ5K7DVRMYInbLYWAC0EA/kx9wXAoSM8q82bUG2hRl8+EAjE6XAbA==}
     engines: {node: '>=20.19'}
@@ -4046,6 +4061,10 @@ packages:
   buffer@6.0.3:
     resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
 
+  bundle-name@4.1.0:
+    resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==}
+    engines: {node: '>=18'}
+
   cac@6.7.14:
     resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
     engines: {node: '>=8'}
@@ -4170,6 +4189,10 @@ packages:
     resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
     engines: {node: '>=12'}
 
+  cliui@9.0.1:
+    resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==}
+    engines: {node: '>=20'}
+
   clsx@2.1.1:
     resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
     engines: {node: '>=6'}
@@ -4435,10 +4458,22 @@ packages:
     resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
     engines: {node: '>=0.10.0'}
 
+  default-browser-id@5.0.1:
+    resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==}
+    engines: {node: '>=18'}
+
+  default-browser@5.5.0:
+    resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==}
+    engines: {node: '>=18'}
+
   define-data-property@1.1.4:
     resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
     engines: {node: '>= 0.4'}
 
+  define-lazy-prop@3.0.0:
+    resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==}
+    engines: {node: '>=12'}
+
   define-properties@1.2.1:
     resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
     engines: {node: '>= 0.4'}
@@ -4675,6 +4710,9 @@ packages:
   electron-to-chromium@1.5.358:
     resolution: {integrity: sha512-EO7tKm3QxRqTs1lSuPXzl6yRAwznehp0AH9OoMOIC+4mQzTFday8FJCO5KU6J/TFSQXEOahNq4vTKpz1jmCVOA==}
 
+  emoji-regex@10.6.0:
+    resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==}
+
   emoji-regex@8.0.0:
     resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
 
@@ -5091,6 +5129,10 @@ packages:
     resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
     engines: {node: 6.* || 8.* || >= 10.*}
 
+  get-east-asian-width@1.6.0:
+    resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==}
+    engines: {node: '>=18'}
+
   get-intrinsic@1.3.0:
     resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
     engines: {node: '>= 0.4'}
@@ -5433,6 +5475,11 @@ packages:
   is-decimal@2.0.1:
     resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==}
 
+  is-docker@3.0.0:
+    resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==}
+    engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+    hasBin: true
+
   is-extglob@2.1.1:
     resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
     engines: {node: '>=0.10.0'}
@@ -5456,6 +5503,15 @@ packages:
   is-hexadecimal@2.0.1:
     resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==}
 
+  is-in-ssh@1.0.0:
+    resolution: {integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==}
+    engines: {node: '>=20'}
+
+  is-inside-container@1.0.0:
+    resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==}
+    engines: {node: '>=14.16'}
+    hasBin: true
+
   is-map@2.0.3:
     resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==}
     engines: {node: '>= 0.4'}
@@ -5548,6 +5604,10 @@ packages:
     resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==}
     engines: {node: '>= 0.4'}
 
+  is-wsl@3.1.1:
+    resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==}
+    engines: {node: '>=16'}
+
   isarray@1.0.0:
     resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
 
@@ -6366,6 +6426,10 @@ packages:
   oniguruma-to-es@4.3.6:
     resolution: {integrity: sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==}
 
+  open@11.0.0:
+    resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==}
+    engines: {node: '>=20'}
+
   optionator@0.9.4:
     resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
     engines: {node: '>= 0.8.0'}
@@ -6591,6 +6655,10 @@ packages:
     resolution: {integrity: sha512-GD3qdB0x1z9xgFI6cdRD6xu2Sp2WCOEoe3mtnyB5Ee0XrrL5Pe+e4CCnJrRMnL1zYtRDZmQQVbvOttLnKDLnaw==}
     engines: {node: '>=12'}
 
+  powershell-utils@0.1.0:
+    resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==}
+    engines: {node: '>=20'}
+
   preact-render-to-string@6.6.5:
     resolution: {integrity: sha512-O6MHzYNIKYaiSX3bOw0gGZfEbOmlIDtDfWwN1JJdc/T3ihzRT6tGGSEWE088dWrEDGa1u7101q+6fzQnO9XCPA==}
     peerDependencies:
@@ -6904,6 +6972,19 @@ packages:
     engines: {node: ^20.19.0 || >=22.12.0}
     hasBin: true
 
+  rollup-plugin-visualizer@7.0.1:
+    resolution: {integrity: sha512-UJUT4+1Ho4OcWmPYU3sYXgUqI8B8Ayfe06MX7y0qCJ1K8aGoKtR/NDd/2nZqM7ADkrzny+I99Ul7GgyoiVNAgg==}
+    engines: {node: '>=22'}
+    hasBin: true
+    peerDependencies:
+      rolldown: 1.x || ^1.0.0-beta || ^1.0.0-rc
+      rollup: 2.x || 3.x || 4.x
+    peerDependenciesMeta:
+      rolldown:
+        optional: true
+      rollup:
+        optional: true
+
   rollup@4.61.0:
     resolution: {integrity: sha512-T9mWdbWfQtp0B5lv/HX+wrhYsmXRlcWnXXmJbXqKJhlRaoS6KMhq0gpyzW4UJfclcxrEdLnTgjT2NjruLONu0g==}
     engines: {node: '>=18.0.0', npm: '>=8.0.0'}
@@ -6918,6 +6999,10 @@ packages:
   rou3@0.8.1:
     resolution: {integrity: sha512-ePa+XGk00/3HuCqrEnK3LxJW7I0SdNg6EFzKUJG73hMAdDcOUC/i/aSz7LSDwLrGr33kal/rqOGydzwl6U7zBA==}
 
+  run-applescript@7.1.0:
+    resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==}
+    engines: {node: '>=18'}
+
   run-parallel@1.2.0:
     resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
 
@@ -7140,6 +7225,10 @@ packages:
     resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
     engines: {node: '>=12'}
 
+  string-width@7.2.0:
+    resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==}
+    engines: {node: '>=18'}
+
   string.prototype.trim@1.2.10:
     resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==}
     engines: {node: '>= 0.4'}
@@ -7864,6 +7953,10 @@ packages:
     resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
     engines: {node: '>=12'}
 
+  wrap-ansi@9.0.2:
+    resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==}
+    engines: {node: '>=18'}
+
   wrappy@1.0.2:
     resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
 
@@ -7879,6 +7972,10 @@ packages:
       utf-8-validate:
         optional: true
 
+  wsl-utils@0.3.1:
+    resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==}
+    engines: {node: '>=20'}
+
   xmlbuilder2@4.0.3:
     resolution: {integrity: sha512-bx8Q1STctnNaaDymWnkfQLKofs0mGNN7rLLapJlGuV3VlvegD7Ls4ggMjE3aUSWItCCzU0PEv45lI87iSigiCA==}
     engines: {node: '>=20.0'}
@@ -7916,6 +8013,10 @@ packages:
     resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
     engines: {node: '>=12'}
 
+  yargs-parser@22.0.0:
+    resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==}
+    engines: {node: ^20.19.0 || ^22.12.0 || >=23}
+
   yargs@16.2.0:
     resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==}
     engines: {node: '>=10'}
@@ -7924,6 +8025,10 @@ packages:
     resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
     engines: {node: '>=12'}
 
+  yargs@18.0.0:
+    resolution: {integrity: sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==}
+    engines: {node: ^20.19.0 || ^22.12.0 || >=23}
+
   yauzl@2.10.0:
     resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==}
 
@@ -10680,6 +10785,12 @@ snapshots:
       react-dom: 19.2.6(react@19.2.6)
       use-sync-external-store: 1.6.0(react@19.2.6)
 
+  '@tanstack/react-virtual@3.14.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
+    dependencies:
+      '@tanstack/virtual-core': 3.17.0
+      react: 19.2.6
+      react-dom: 19.2.6(react@19.2.6)
+
   '@tanstack/router-core@1.171.2':
     dependencies:
       '@tanstack/history': 1.162.0
@@ -10809,6 +10920,8 @@ snapshots:
 
   '@tanstack/store@0.9.3': {}
 
+  '@tanstack/virtual-core@3.17.0': {}
+
   '@tanstack/virtual-file-routes@1.162.0': {}
 
   '@tiptap/core@2.27.2(@tiptap/pm@2.27.2)':
@@ -11632,6 +11745,10 @@ snapshots:
       base64-js: 1.5.1
       ieee754: 1.2.1
 
+  bundle-name@4.1.0:
+    dependencies:
+      run-applescript: 7.1.0
+
   cac@6.7.14: {}
 
   call-bind-apply-helpers@1.0.2:
@@ -11782,6 +11899,12 @@ snapshots:
       strip-ansi: 6.0.1
       wrap-ansi: 7.0.0
 
+  cliui@9.0.1:
+    dependencies:
+      string-width: 7.2.0
+      strip-ansi: 7.2.0
+      wrap-ansi: 9.0.2
+
   clsx@2.1.1: {}
 
   cmdk@1.1.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6):
@@ -12023,12 +12146,21 @@ snapshots:
 
   deepmerge@4.3.1: {}
 
+  default-browser-id@5.0.1: {}
+
+  default-browser@5.5.0:
+    dependencies:
+      bundle-name: 4.1.0
+      default-browser-id: 5.0.1
+
   define-data-property@1.1.4:
     dependencies:
       es-define-property: 1.0.1
       es-errors: 1.3.0
       gopd: 1.2.0
 
+  define-lazy-prop@3.0.0: {}
+
   define-properties@1.2.1:
     dependencies:
       define-data-property: 1.1.4
@@ -12190,6 +12322,8 @@ snapshots:
 
   electron-to-chromium@1.5.358: {}
 
+  emoji-regex@10.6.0: {}
+
   emoji-regex@8.0.0: {}
 
   emoji-regex@9.2.2: {}
@@ -12767,6 +12901,8 @@ snapshots:
 
   get-caller-file@2.0.5: {}
 
+  get-east-asian-width@1.6.0: {}
+
   get-intrinsic@1.3.0:
     dependencies:
       call-bind-apply-helpers: 1.0.2
@@ -13216,6 +13352,8 @@ snapshots:
 
   is-decimal@2.0.1: {}
 
+  is-docker@3.0.0: {}
+
   is-extglob@2.1.1: {}
 
   is-finalizationregistry@1.1.1:
@@ -13238,6 +13376,12 @@ snapshots:
 
   is-hexadecimal@2.0.1: {}
 
+  is-in-ssh@1.0.0: {}
+
+  is-inside-container@1.0.0:
+    dependencies:
+      is-docker: 3.0.0
+
   is-map@2.0.3: {}
 
   is-negative-zero@2.0.3: {}
@@ -13308,6 +13452,10 @@ snapshots:
       call-bound: 1.0.4
       get-intrinsic: 1.3.0
 
+  is-wsl@3.1.1:
+    dependencies:
+      is-inside-container: 1.0.0
+
   isarray@1.0.0: {}
 
   isarray@2.0.5: {}
@@ -14236,6 +14384,15 @@ snapshots:
       regex: 6.1.0
       regex-recursion: 6.0.2
 
+  open@11.0.0:
+    dependencies:
+      default-browser: 5.5.0
+      define-lazy-prop: 3.0.0
+      is-in-ssh: 1.0.0
+      is-inside-container: 1.0.0
+      powershell-utils: 0.1.0
+      wsl-utils: 0.3.1
+
   optionator@0.9.4:
     dependencies:
       deep-is: 0.1.4
@@ -14468,6 +14625,8 @@ snapshots:
 
   postgres@3.4.9: {}
 
+  powershell-utils@0.1.0: {}
+
   preact-render-to-string@6.6.5(preact@11.0.0-beta.0):
     dependencies:
       preact: 11.0.0-beta.0
@@ -14895,6 +15054,16 @@ snapshots:
       '@rolldown/binding-win32-arm64-msvc': 1.0.1
       '@rolldown/binding-win32-x64-msvc': 1.0.1
 
+  rollup-plugin-visualizer@7.0.1(rolldown@1.0.1)(rollup@4.61.0):
+    dependencies:
+      open: 11.0.0
+      picomatch: 4.0.4
+      source-map: 0.7.6
+      yargs: 18.0.0
+    optionalDependencies:
+      rolldown: 1.0.1
+      rollup: 4.61.0
+
   rollup@4.61.0:
     dependencies:
       '@types/estree': 1.0.9
@@ -14932,6 +15101,8 @@ snapshots:
 
   rou3@0.8.1: {}
 
+  run-applescript@7.1.0: {}
+
   run-parallel@1.2.0:
     dependencies:
       queue-microtask: 1.2.3
@@ -15225,6 +15396,12 @@ snapshots:
       emoji-regex: 9.2.2
       strip-ansi: 7.2.0
 
+  string-width@7.2.0:
+    dependencies:
+      emoji-regex: 10.6.0
+      get-east-asian-width: 1.6.0
+      strip-ansi: 7.2.0
+
   string.prototype.trim@1.2.10:
     dependencies:
       call-bind: 1.0.9
@@ -15911,10 +16088,21 @@ snapshots:
       string-width: 5.1.2
       strip-ansi: 7.2.0
 
+  wrap-ansi@9.0.2:
+    dependencies:
+      ansi-styles: 6.2.3
+      string-width: 7.2.0
+      strip-ansi: 7.2.0
+
   wrappy@1.0.2: {}
 
   ws@8.20.1: {}
 
+  wsl-utils@0.3.1:
+    dependencies:
+      is-wsl: 3.1.1
+      powershell-utils: 0.1.0
+
   xmlbuilder2@4.0.3:
     dependencies:
       '@oozcitak/dom': 2.0.2
@@ -15941,6 +16129,8 @@ snapshots:
 
   yargs-parser@21.1.1: {}
 
+  yargs-parser@22.0.0: {}
+
   yargs@16.2.0:
     dependencies:
       cliui: 7.0.4
@@ -15961,6 +16151,15 @@ snapshots:
       y18n: 5.0.8
       yargs-parser: 21.1.1
 
+  yargs@18.0.0:
+    dependencies:
+      cliui: 9.0.1
+      escalade: 3.2.0
+      get-caller-file: 2.0.5
+      string-width: 7.2.0
+      y18n: 5.0.8
+      yargs-parser: 22.0.0
+
   yauzl@2.10.0:
     dependencies:
       buffer-crc32: 0.2.13
diff --git a/src/components/OpenTask.tsx b/src/components/OpenTask.tsx
index 19e7c39..af561d7 100644
--- a/src/components/OpenTask.tsx
+++ b/src/components/OpenTask.tsx
@@ -11,8 +11,7 @@ import {
   TaskWithRelations,
   UpdateTask,
 } from "~/db/schema";
-import { RichtextEditor } from "~/components/RichtextEditor/Editor";
-import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import { Suspense, lazy, useCallback, useEffect, useMemo, useRef, useState } from "react";
 import {
   useAssignTaskMutation,
   useDeleteTaskMutation,
@@ -71,7 +70,6 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs";
 import { SubTasks } from "./SubTasks";
 import { Badge } from "./ui/badge";
 import { SprintSelect } from "./SprintSelect";
-import { TaskDevelopmentSection } from "~/components/git/TaskDevelopmentSection";
 import { TaskPullRequestReviewFeed } from "~/components/git/TaskPullRequestReviewFeed";
 import { GitProviderIcon } from "~/components/git/GitProviderIcon";
 import { useGitConnectionQuery, useTaskGitContextQuery } from "~/db/queries/git";
@@ -83,6 +81,27 @@ import {
   type TaskTab,
   type TaskTabSearch,
 } from "~/lib/task-tab-search";
+import { EndlessLoadingSpinner } from "./EndlessLoadingSpinner";
+
+const RichtextEditor = lazy(() =>
+  import("~/components/RichtextEditor/Editor").then((module) => ({
+    default: module.RichtextEditor,
+  }))
+);
+
+const TaskDevelopmentSection = lazy(() =>
+  import("~/components/git/TaskDevelopmentSection").then((module) => ({
+    default: module.TaskDevelopmentSection,
+  }))
+);
+
+function TabLoadingFallback() {
+  return (
+    <div className="flex min-h-32 items-center justify-center">
+      <EndlessLoadingSpinner isActive centered />
+    </div>
+  );
+}
 
 export function OpenTask({
   task,
@@ -518,16 +537,18 @@ export function OpenTask({
                       </DetailListItem>
                     </DetailList>
                   </div>
-                  <RichtextEditor
-                    content={task.description || ""}
-                    onUpdate={(data) => {
-                      handleUpdateTask({
-                        id: task.id,
-                        description: data.text,
-                        projectId: task.projectId,
-                      });
-                    }}
-                  />
+                  <Suspense fallback={<TabLoadingFallback />}>
+                    <RichtextEditor
+                      content={task.description || ""}
+                      onUpdate={(data) => {
+                        handleUpdateTask({
+                          id: task.id,
+                          description: data.text,
+                          projectId: task.projectId,
+                        });
+                      }}
+                    />
+                  </Suspense>
 
                   <Tabs
                     value={taskTab}
@@ -573,12 +594,14 @@ export function OpenTask({
                   value="development"
                   className="mt-0 flex min-h-0 flex-1 flex-col overflow-hidden data-[state=inactive]:hidden"
                 >
-                  <TaskDevelopmentSection
-                    layout="panel"
-                    taskId={task.id}
-                    taskName={task.name}
-                    projectId={task.projectId}
-                  />
+                  <Suspense fallback={<TabLoadingFallback />}>
+                    <TaskDevelopmentSection
+                      layout="panel"
+                      taskId={task.id}
+                      taskName={task.name}
+                      projectId={task.projectId}
+                    />
+                  </Suspense>
                 </TabsContent>
               </Tabs>
             </div>
diff --git a/src/components/TaskCard.tsx b/src/components/TaskCard.tsx
index acecc73..f4132a5 100644
--- a/src/components/TaskCard.tsx
+++ b/src/components/TaskCard.tsx
@@ -1,3 +1,5 @@
+import { memo, type CSSProperties, type ReactNode } from "react";
+import type { AppUser } from "~/db/queries/users";
 import { TaskWithRelations } from "~/db/schema";
 import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
 import { ClientOnly, Link } from "@tanstack/react-router";
@@ -29,13 +31,6 @@ import {
 } from "~/components/ui/tooltip";
 import { DateDisplay } from "./ui/date-display";
 import { Avatar, AvatarFallback, AvatarImage, AvatarList } from "./ui/avatar";
-import { useUsersQuery } from "~/db/queries/users";
-import { useEffect, useState, type CSSProperties, type ReactNode } from "react";
-import { authClient } from "~/lib/auth-client";
-import {
-  useAssignTaskMutation,
-  useUnassignTaskMutation,
-} from "~/db/mutations/tasks";
 import { EndlessLoadingSpinner } from "./EndlessLoadingSpinner";
 import { Progress } from "./ui/progress";
 import { Badge } from "./ui/badge";
@@ -55,22 +50,30 @@ import {
 type TaskCardProps = {
   task: TaskWithRelations;
   columnTaskIds: string[];
+  users: AppUser[];
+  assigningTaskId?: string | null;
   showSprint?: boolean;
   showProject?: boolean;
   taskLinkTo?: "project" | "sprint" | "all";
   gitSummary?: TaskGitSummary;
   closingStatusId?: string;
   manualSortEnabled?: boolean;
+  isSelected?: boolean;
+  isHovered?: boolean;
 };
 
-function TaskCardShell({
+const TaskCardShell = memo(function TaskCardShell({
   task,
   columnTaskIds,
+  users,
+  assigningTaskId,
   showSprint,
   showProject,
   taskLinkTo,
   gitSummary,
   closingStatusId,
+  isSelected = false,
+  isHovered = false,
   listeners,
   setNodeRef,
   isDragging,
@@ -81,14 +84,6 @@ function TaskCardShell({
   isDragging: boolean;
   style: CSSProperties | undefined;
 }) {
-  const isSelected = useStore(TaskViewStore, (state) =>
-    state.selectedTaskIds.includes(task.id)
-  );
-  const isHovered = useStore(
-    TaskViewStore,
-    (state) => state.hoveredTaskId === task.id
-  );
-
   return (
     <div
       ref={setNodeRef}
@@ -128,6 +123,8 @@ function TaskCardShell({
           task={task}
           key={task.id}
           columnTaskIds={columnTaskIds}
+          users={users}
+          assigningTaskId={assigningTaskId}
           showSprint={showSprint}
           showProject={showProject}
           taskLinkTo={taskLinkTo}
@@ -139,7 +136,7 @@ function TaskCardShell({
       </div>
     </div>
   );
-}
+});
 
 function DraggableTaskCard(props: TaskCardProps) {
   const { attributes: _attributes, ...drag } = useDraggableTaskItem(props.task.id);
@@ -151,16 +148,30 @@ function SortableTaskCard(props: TaskCardProps) {
   return <TaskCardShell {...props} {...drag} />;
 }
 
-export default function TaskCard({
+const TaskCard = memo(function TaskCard({
   manualSortEnabled = false,
   ...props
 }: TaskCardProps) {
+  const isSelected = useStore(TaskViewStore, (state) =>
+    state.selectedTaskIds.includes(props.task.id)
+  );
+  const isHovered = useStore(
+    TaskViewStore,
+    (state) => state.hoveredTaskId === props.task.id
+  );
+  const cardProps = {
+    ...props,
+    isSelected,
+    isHovered,
+  };
+
   return (
     <ClientOnly
       fallback={
         <TaskCardComponent
           task={props.task}
           key={props.task.id}
+          users={props.users}
           showSprint={props.showSprint}
           showProject={props.showProject}
           taskLinkTo={props.taskLinkTo}
@@ -169,13 +180,15 @@ export default function TaskCard({
       }
     >
       {manualSortEnabled ? (
-        <SortableTaskCard manualSortEnabled {...props} />
+        <SortableTaskCard manualSortEnabled {...cardProps} />
       ) : (
-        <DraggableTaskCard manualSortEnabled={false} {...props} />
+        <DraggableTaskCard manualSortEnabled={false} {...cardProps} />
       )}
     </ClientOnly>
   );
-}
+});
+
+export default TaskCard;
 
 const TaskCardLinkWrapper = ({
   to,
@@ -260,9 +273,11 @@ function TaskCardIndicatorTooltip({
   );
 }
 
-export const TaskCardComponent = ({
+export const TaskCardComponent = memo(function TaskCardComponent({
   task,
   columnTaskIds = [],
+  users,
+  assigningTaskId = null,
   showSprint = true,
   showProject = true,
   taskLinkTo,
@@ -273,6 +288,8 @@ export const TaskCardComponent = ({
 }: {
   task: TaskWithRelations;
   columnTaskIds?: string[];
+  users: AppUser[];
+  assigningTaskId?: string | null;
   showSprint?: boolean;
   showProject?: boolean;
   taskLinkTo?: "project" | "sprint" | "all";
@@ -280,40 +297,13 @@ export const TaskCardComponent = ({
   isHovered?: boolean;
   gitSummary?: TaskGitSummary;
   closingStatusId?: string;
-}) => {
+}) {
   const isOverdue = isTaskOverdue(task, { closingStatusId });
   const taskKeyLabel =
     task.project.taskKeyPrefix != null
       ? formatTaskKey(task.project.taskKeyPrefix, task.number)
       : null;
-  const usersQuery = useUsersQuery();
-  const { data: session } = authClient.useSession();
-  const currentUserId = session?.user.id;
-  const assignTask = useAssignTaskMutation();
-  const unassignTask = useUnassignTaskMutation();
-  const [isAssigning, setIsAssigning] = useState(false);
-
-  const [isCardHovered, setIsCardHovered] = useState(false);
-  useEffect(() => {
-    const handleKeyPress = async (event: KeyboardEvent) => {
-      if (currentUserId && isCardHovered && event.key === "m") {
-        setIsAssigning(true);
-        if (
-          task.assignees.find((assignee) => assignee.userId === currentUserId)
-        ) {
-          await unassignTask(task, [currentUserId]);
-        } else {
-          await assignTask(task, [currentUserId]);
-        }
-        setIsAssigning(false);
-      }
-    };
-
-    window.addEventListener("keydown", handleKeyPress);
-    return () => {
-      window.removeEventListener("keydown", handleKeyPress);
-    };
-  }, [isCardHovered, task, currentUserId, assignTask, unassignTask]);
+  const isAssigning = assigningTaskId === task.id;
 
   const percentageComplete = Math.round(
     (task.subTasks.filter((t) => t.done).length / task.subTasks.length) * 100
@@ -342,8 +332,6 @@ export const TaskCardComponent = ({
           isSelected && "border-l-[3px] border-l-primary bg-primary/5",
           isHovered && !isSelected && "bg-accent/30"
         )}
-        onMouseEnter={() => setIsCardHovered(true)}
-        onMouseLeave={() => setIsCardHovered(false)}
       >
         <CardHeader className="space-y-2 p-3">
           {showProject || task.labels.length > 0 ? (
@@ -443,7 +431,7 @@ export const TaskCardComponent = ({
               {!isAssigning ? (
                 <AvatarList>
                   {task.assignees.map((taskAssignee) => {
-                    const assignee = usersQuery.data.find(
+                    const assignee = users.find(
                       (u) => u.id === taskAssignee.userId
                     );
                     if (!assignee) return null;
@@ -483,4 +471,4 @@ export const TaskCardComponent = ({
       </Card>
     </TaskCardLinkWrapper>
   );
-};
+});
diff --git a/src/components/views/BoardView.tsx b/src/components/views/BoardView.tsx
index 8cdbf14..cefe0cf 100644
--- a/src/components/views/BoardView.tsx
+++ b/src/components/views/BoardView.tsx
@@ -21,12 +21,15 @@ import {
 import { TaskViewStore } from "./task-view-store";
 import type { TaskViewProps } from "./task-view-types";
 import { getClosingStatusId } from "~/lib/statuses";
+import { VirtualizedTaskCards } from "./VirtualizedTaskCards";
 
 export const BoardView = ({
   tasks,
   projectId,
   sprintId,
   statuses,
+  users,
+  assigningTaskId,
   location,
   updateTask,
   reorderTasks,
@@ -46,8 +49,9 @@ export const BoardView = ({
   const sensors = useTaskViewSensors(manualSortEnabled);
 
   const taskIds = useMemo(() => tasks.map((t) => t.id), [tasks]);
+  const taskIdsKey = useMemo(() => taskIds.join(","), [taskIds]);
   const { data: gitSummaries } = useQuery({
-    queryKey: ["git", "summaries", taskIds],
+    queryKey: ["git", "summaries", taskIdsKey],
     queryFn: () => fetchTaskGitSummaries({ data: { taskIds } }),
     enabled: taskIds.length > 0,
   });
@@ -85,19 +89,27 @@ export const BoardView = ({
   const renderColumnCards = (columnTasks: TaskViewProps["tasks"]) => {
     const sorted = sortColumnTasks(columnTasks);
     const columnTaskIds = sorted.map((t) => t.id);
-    const cards = sorted.map((task) => (
-      <TaskCard
-        key={task.id}
-        task={task}
-        columnTaskIds={columnTaskIds}
-        showSprint={showSprint}
-        showProject={showProject}
-        taskLinkTo={taskLinkTo}
-        gitSummary={gitSummaries?.[task.id]}
-        closingStatusId={closingStatusId}
-        manualSortEnabled={manualSortEnabled}
+
+    const cards = (
+      <VirtualizedTaskCards
+        items={sorted}
+        renderItem={(task) => (
+          <TaskCard
+            key={task.id}
+            task={task}
+            columnTaskIds={columnTaskIds}
+            users={users}
+            assigningTaskId={assigningTaskId}
+            showSprint={showSprint}
+            showProject={showProject}
+            taskLinkTo={taskLinkTo}
+            gitSummary={gitSummaries?.[task.id]}
+            closingStatusId={closingStatusId}
+            manualSortEnabled={manualSortEnabled}
+          />
+        )}
       />
-    ));
+    );
 
     if (!manualSortEnabled) return cards;
 
@@ -164,6 +176,7 @@ export const BoardView = ({
         {activeTask ? (
           <TaskCardComponent
             task={activeTask}
+            users={users}
             showSprint={showSprint}
             showProject={showProject}
             taskLinkTo={taskLinkTo}
diff --git a/src/components/views/ListView.tsx b/src/components/views/ListView.tsx
index 14add2b..2d06981 100644
--- a/src/components/views/ListView.tsx
+++ b/src/components/views/ListView.tsx
@@ -29,6 +29,7 @@ import {
 import { TaskViewStore } from "./task-view-store";
 import type { TaskViewProps } from "./task-view-types";
 import { getClosingStatusId } from "~/lib/statuses";
+import { VirtualizedTaskListRows } from "./VirtualizedTaskListRows";
 
 const statusDotClass = {
   red: "bg-red-500",
@@ -262,7 +263,23 @@ function ListSection({
 
       {sortedTasks.length > 0 ? (
         <div>
-          {manualSortEnabled ? (
+          {sortedTasks.length > 25 ? (
+            <VirtualizedTaskListRows
+              items={sortedTasks}
+              renderItem={(task) => (
+                <TaskListRow
+                  key={task.id}
+                  task={task}
+                  orderedTaskIds={orderedTaskIds}
+                  location={location}
+                  columnFlags={columnFlags}
+                  updateTask={updateTask}
+                  closingStatusId={closingStatusId}
+                  manualSortEnabled={manualSortEnabled}
+                />
+              )}
+            />
+          ) : manualSortEnabled ? (
             <SortableContext
               items={sortedTasks.map((task) => `task:${task.id}`)}
               strategy={verticalListSortingStrategy}
diff --git a/src/components/views/TaskViewsContainer.tsx b/src/components/views/TaskViewsContainer.tsx
index ee6f4ca..deed08e 100644
--- a/src/components/views/TaskViewsContainer.tsx
+++ b/src/components/views/TaskViewsContainer.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useMemo } from "react";
+import { useEffect, useMemo, useState } from "react";
 import { useStore } from "@tanstack/react-store";
 import { BatchTaskToolbar } from "~/components/BatchTaskToolbar";
 import { cn } from "~/lib/utils";
@@ -14,8 +14,19 @@ import { TaskViewStore } from "./task-view-store";
 import { useSyncTaskViewModeFromPreferences } from "./use-sync-task-view-mode";
 import { useSyncTaskSortFromPreferences } from "./use-sync-task-sort";
 import { registerTaskReorderKeyboard } from "./task-reorder-keyboard";
+import { useUsersQuery } from "~/db/queries/users";
+import { authClient } from "~/lib/auth-client";
+import {
+  useAssignTaskMutation,
+  useUnassignTaskMutation,
+} from "~/db/mutations/tasks";
+
+export type TaskViewsContainerProps = Omit<
+  TaskViewProps,
+  "users" | "assigningTaskId"
+>;
 
-export function TaskViewsContainer(props: TaskViewProps) {
+export function TaskViewsContainer(props: TaskViewsContainerProps) {
   useSyncTaskViewModeFromPreferences();
   useSyncTaskSortFromPreferences(
     props.location,
@@ -23,6 +34,14 @@ export function TaskViewsContainer(props: TaskViewProps) {
     props.sprintId
   );
 
+  const usersQuery = useUsersQuery();
+  const { data: session } = authClient.useSession();
+  const currentUserId = session?.user.id;
+  const assignTask = useAssignTaskMutation();
+  const unassignTask = useUnassignTaskMutation();
+  const [assigningTaskId, setAssigningTaskId] = useState<string | null>(null);
+  const hoveredTaskId = useStore(TaskViewStore, (state) => state.hoveredTaskId);
+
   const viewMode = useStore(TaskViewStore, (state) => state.viewMode);
   const sortBy = useStore(TaskViewStore, (state) => state.sortBy);
   const sortDirection = useStore(
@@ -65,14 +84,34 @@ export function TaskViewsContainer(props: TaskViewProps) {
         getStatuses: () => props.statuses,
         reorderTasks: props.reorderTasks,
       }),
-    [
-      flatTaskIds,
-      props.tasks,
-      props.statuses,
-      props.reorderTasks,
-    ]
+    [flatTaskIds, props.tasks, props.statuses, props.reorderTasks]
   );
 
+  useEffect(() => {
+    const handleKeyPress = async (event: KeyboardEvent) => {
+      if (!currentUserId || !hoveredTaskId || event.key !== "m") return;
+
+      const task = props.tasks.find((entry) => entry.id === hoveredTaskId);
+      if (!task) return;
+
+      setAssigningTaskId(task.id);
+      try {
+        if (task.assignees.some((assignee) => assignee.userId === currentUserId)) {
+          await unassignTask(task, [currentUserId]);
+        } else {
+          await assignTask(task, [currentUserId]);
+        }
+      } finally {
+        setAssigningTaskId(null);
+      }
+    };
+
+    window.addEventListener("keydown", handleKeyPress);
+    return () => {
+      window.removeEventListener("keydown", handleKeyPress);
+    };
+  }, [assignTask, currentUserId, hoveredTaskId, props.tasks, unassignTask]);
+
   const ViewComponent = TASK_VIEW_REGISTRY[viewMode];
 
   return (
@@ -91,7 +130,11 @@ export function TaskViewsContainer(props: TaskViewProps) {
           selectedTasks.length > 0 ? "pb-16" : "pb-3"
         )}
       >
-        <ViewComponent {...props} />
+        <ViewComponent
+          {...props}
+          users={usersQuery.data}
+          assigningTaskId={assigningTaskId}
+        />
       </div>
     </div>
   );
diff --git a/src/components/views/VirtualizedTaskCards.tsx b/src/components/views/VirtualizedTaskCards.tsx
new file mode 100644
index 0000000..5a7ab90
--- /dev/null
+++ b/src/components/views/VirtualizedTaskCards.tsx
@@ -0,0 +1,49 @@
+import { useVirtualizer } from "@tanstack/react-virtual";
+import { useEffect, useRef, type ReactNode } from "react";
+
+type VirtualizedTaskCardsProps<T> = {
+  items: T[];
+  estimateSize?: number;
+  renderItem: (item: T, index: number) => ReactNode;
+};
+
+export function VirtualizedTaskCards<T>({
+  items,
+  estimateSize = 132,
+  renderItem,
+}: VirtualizedTaskCardsProps<T>) {
+  const listRef = useRef<HTMLDivElement>(null);
+  const scrollElementRef = useRef<HTMLElement | null>(null);
+
+  useEffect(() => {
+    scrollElementRef.current = listRef.current?.parentElement ?? null;
+  }, [items.length]);
+
+  const virtualizer = useVirtualizer({
+    count: items.length,
+    getScrollElement: () => scrollElementRef.current,
+    estimateSize: () => estimateSize,
+    overscan: 6,
+  });
+
+  return (
+    <div ref={listRef} className="relative w-full">
+      <div
+        className="relative w-full"
+        style={{ height: `${virtualizer.getTotalSize()}px` }}
+      >
+        {virtualizer.getVirtualItems().map((virtualItem) => (
+          <div
+            key={virtualItem.key}
+            ref={virtualizer.measureElement}
+            data-index={virtualItem.index}
+            className="absolute left-0 top-0 w-full pb-1.5"
+            style={{ transform: `translateY(${virtualItem.start}px)` }}
+          >
+            {renderItem(items[virtualItem.index]!, virtualItem.index)}
+          </div>
+        ))}
+      </div>
+    </div>
+  );
+}
diff --git a/src/components/views/VirtualizedTaskListRows.tsx b/src/components/views/VirtualizedTaskListRows.tsx
new file mode 100644
index 0000000..6805c01
--- /dev/null
+++ b/src/components/views/VirtualizedTaskListRows.tsx
@@ -0,0 +1,44 @@
+import { useVirtualizer } from "@tanstack/react-virtual";
+import { useRef, type ReactNode } from "react";
+
+type VirtualizedTaskListRowsProps<T> = {
+  items: T[];
+  estimateSize?: number;
+  renderItem: (item: T, index: number) => ReactNode;
+};
+
+export function VirtualizedTaskListRows<T>({
+  items,
+  estimateSize = 44,
+  renderItem,
+}: VirtualizedTaskListRowsProps<T>) {
+  const parentRef = useRef<HTMLDivElement>(null);
+
+  const virtualizer = useVirtualizer({
+    count: items.length,
+    getScrollElement: () => parentRef.current,
+    estimateSize: () => estimateSize,
+    overscan: 8,
+  });
+
+  return (
+    <div ref={parentRef} className="max-h-[min(70vh,720px)] overflow-y-auto">
+      <div
+        className="relative w-full"
+        style={{ height: `${virtualizer.getTotalSize()}px` }}
+      >
+        {virtualizer.getVirtualItems().map((virtualItem) => (
+          <div
+            key={virtualItem.key}
+            ref={virtualizer.measureElement}
+            data-index={virtualItem.index}
+            className="absolute left-0 top-0 w-full"
+            style={{ transform: `translateY(${virtualItem.start}px)` }}
+          >
+            {renderItem(items[virtualItem.index]!, virtualItem.index)}
+          </div>
+        ))}
+      </div>
+    </div>
+  );
+}
diff --git a/src/components/views/task-view-types.ts b/src/components/views/task-view-types.ts
index 75ef5c1..9f9e526 100644
--- a/src/components/views/task-view-types.ts
+++ b/src/components/views/task-view-types.ts
@@ -1,4 +1,5 @@
 import type { ComponentType } from "react";
+import type { AppUser } from "~/db/queries/users";
 import type {
   Label,
   Status,
@@ -15,6 +16,8 @@ export type TaskViewProps = {
   sprintId?: string;
   statuses: Status[];
   labels: Label[];
+  users: AppUser[];
+  assigningTaskId: string | null;
   location: TaskViewLocation;
   updateTask: (task: UpdateTask) => Promise<void>;
   reorderTasks: (input: {
diff --git a/src/db/mutations/attachments.ts b/src/db/mutations/attachments.ts
index 4cbaa37..dfdf469 100644
--- a/src/db/mutations/attachments.ts
+++ b/src/db/mutations/attachments.ts
@@ -8,7 +8,6 @@ import { requireSessionFromRequest } from "~/lib/session";
 import { v7 as uuid } from "uuid";
 import { getOwningIdentity } from "~/lib/utils";
 import { useCallback } from "react";
-import { useRouter } from "@tanstack/react-router";
 import { useQueryClient } from "@tanstack/react-query";
 import z from "zod";
 
@@ -32,7 +31,6 @@ const createAttachment = createServerFn({ method: "POST" })
   });
 
 export function useCreateAttachmentMutation() {
-  const router = useRouter();
   const queryClient = useQueryClient();
   const _createAttachment = useServerFn(createAttachment);
 
@@ -40,7 +38,6 @@ export function useCreateAttachmentMutation() {
     async (attachmentData: CreateAttachment) => {
       const result = await _createAttachment({ data: attachmentData });
 
-      router.invalidate();
       queryClient.invalidateQueries({
         queryKey: ["attachments", attachmentData.taskId],
       });
@@ -50,7 +47,7 @@ export function useCreateAttachmentMutation() {
 
       return result;
     },
-    [router, queryClient, _createAttachment]
+    [queryClient, _createAttachment]
   );
 }
 
@@ -63,7 +60,6 @@ export const deleteAttachment = createServerFn({ method: "POST" })
   });
 
 export function useDeleteAttachmentMutation() {
-  const router = useRouter();
   const queryClient = useQueryClient();
   const _deleteAttachment = useServerFn(deleteAttachment);
 
@@ -71,7 +67,6 @@ export function useDeleteAttachmentMutation() {
     async (id: string) => {
       const result = await _deleteAttachment({ data: { id } });
 
-      router.invalidate();
       queryClient.invalidateQueries({
         queryKey: ["attachments"],
       });
@@ -86,6 +81,6 @@ export function useDeleteAttachmentMutation() {
 
       return result;
     },
-    [router, queryClient, _deleteAttachment]
+    [queryClient, _deleteAttachment]
   );
 }
diff --git a/src/db/mutations/comments.ts b/src/db/mutations/comments.ts
index c01e1ab..a34df89 100644
--- a/src/db/mutations/comments.ts
+++ b/src/db/mutations/comments.ts
@@ -20,7 +20,6 @@ import {
 import { getActorNameFromSession } from "~/lib/notifications/actor-name";
 import { stripHtml } from "~/lib/notifications/strip-html";
 import { useCallback } from "react";
-import { useRouter } from "@tanstack/react-router";
 import { useQueryClient } from "@tanstack/react-query";
 import { sync } from "./sync";
 
@@ -120,7 +119,6 @@ const updateComment = createServerFn({ method: "POST" })
   });
 
 export function useCreateCommentMutation() {
-  const router = useRouter();
   const queryClient = useQueryClient();
   const _createComment = useServerFn(createComment);
 
@@ -128,19 +126,17 @@ export function useCreateCommentMutation() {
     async (commentData: CreateComment) => {
       const result = await _createComment({ data: commentData });
 
-      router.invalidate();
       queryClient.invalidateQueries({
         queryKey: ["comments", commentData.taskId],
       });
 
       return result;
     },
-    [router, queryClient, _createComment]
+    [queryClient, _createComment]
   );
 }
 
 export function useUpdateCommentMutation() {
-  const router = useRouter();
   const queryClient = useQueryClient();
   const _updateComment = useServerFn(updateComment);
 
@@ -149,13 +145,12 @@ export function useUpdateCommentMutation() {
       const { taskId, ...updateData } = commentData;
       const result = await _updateComment({ data: updateData });
 
-      router.invalidate();
       queryClient.invalidateQueries({
         queryKey: ["comments", taskId],
       });
 
       return result;
     },
-    [router, queryClient, _updateComment]
+    [queryClient, _updateComment]
   );
 }
diff --git a/src/db/mutations/labels.ts b/src/db/mutations/labels.ts
index a69e609..d122def 100644
--- a/src/db/mutations/labels.ts
+++ b/src/db/mutations/labels.ts
@@ -11,7 +11,6 @@ import {
 } from "~/db/schema";
 import { v7 as uuid } from "uuid";
 import z from "zod";
-import { useRouter } from "@tanstack/react-router";
 import { useQueryClient } from "@tanstack/react-query";
 import { useCallback } from "react";
 import { and, eq } from "drizzle-orm";
@@ -34,7 +33,6 @@ const createLabel = createServerFn({ method: "POST" })
   });
 
 export function useCreateLabelMutation() {
-  const router = useRouter();
   const queryClient = useQueryClient();
   const _createLabel = useServerFn(createLabel);
 
@@ -42,7 +40,6 @@ export function useCreateLabelMutation() {
     async (label: CreateLabel) => {
       const result = await _createLabel({ data: label });
 
-      router.invalidate();
       queryClient.invalidateQueries({
         queryKey: ["labels"],
       });
@@ -52,7 +49,7 @@ export function useCreateLabelMutation() {
 
       return result;
     },
-    [router, queryClient, _createLabel]
+    [queryClient, _createLabel]
   );
 }
 
@@ -77,7 +74,6 @@ const updateLabel = createServerFn({ method: "POST" })
   });
 
 export function useUpdateLabelMutation() {
-  const router = useRouter();
   const queryClient = useQueryClient();
   const _updateLabel = useServerFn(updateLabel);
 
@@ -85,7 +81,6 @@ export function useUpdateLabelMutation() {
     async (label: UpdateLabel) => {
       const result = await _updateLabel({ data: label });
 
-      router.invalidate();
       queryClient.invalidateQueries({
         queryKey: ["labels"],
       });
@@ -95,7 +90,7 @@ export function useUpdateLabelMutation() {
 
       return result;
     },
-    [router, queryClient, _updateLabel]
+    [queryClient, _updateLabel]
   );
 }
 
@@ -128,7 +123,6 @@ const updateMultipleLabels = createServerFn({ method: "POST" })
   });
 
 export function useUpdateMultipleLabelsMutation() {
-  const router = useRouter();
   const queryClient = useQueryClient();
   const _updateMultipleLabels = useServerFn(updateMultipleLabels);
 
@@ -155,11 +149,9 @@ export function useUpdateMultipleLabelsMutation() {
         queryClient.invalidateQueries({ queryKey: ["labels"] });
         queryClient.invalidateQueries({ queryKey: ["labels-with-counts"] });
         throw error;
-      } finally {
-        router.invalidate();
       }
     },
-    [router, queryClient, _updateMultipleLabels]
+    [queryClient, _updateMultipleLabels]
   );
 }
 
@@ -178,7 +170,6 @@ const deleteLabel = createServerFn({ method: "POST" })
   });
 
 export function useDeleteLabelMutation() {
-  const router = useRouter();
   const queryClient = useQueryClient();
   const _deleteLabel = useServerFn(deleteLabel);
 
@@ -186,7 +177,6 @@ export function useDeleteLabelMutation() {
     async (id: string) => {
       const result = await _deleteLabel({ data: { id } });
 
-      router.invalidate();
       queryClient.invalidateQueries({
         queryKey: ["labels"],
       });
@@ -196,6 +186,6 @@ export function useDeleteLabelMutation() {
 
       return result;
     },
-    [router, queryClient, _deleteLabel]
+    [queryClient, _deleteLabel]
   );
 }
diff --git a/src/db/mutations/projects.ts b/src/db/mutations/projects.ts
index 2c9e353..ce37209 100644
--- a/src/db/mutations/projects.ts
+++ b/src/db/mutations/projects.ts
@@ -12,7 +12,6 @@ import { db } from "..";
 import { v7 as uuid } from "uuid";
 import { getOwningIdentity } from "~/lib/utils";
 import { useCallback } from "react";
-import { useRouter } from "@tanstack/react-router";
 import { useQueryClient } from "@tanstack/react-query";
 import { sync } from "./sync";
 import { and, eq } from "drizzle-orm";
@@ -40,7 +39,6 @@ const createProject = createServerFn({ method: "POST" })
   });
 
 export function useCreateProjectMutation() {
-  const router = useRouter();
   const queryClient = useQueryClient();
   const _createProject = useServerFn(createProject);
 
@@ -48,14 +46,13 @@ export function useCreateProjectMutation() {
     async (projectData: CreateProject) => {
       const result = await _createProject({ data: projectData });
 
-      router.invalidate();
       queryClient.invalidateQueries({
         queryKey: ["projects"],
       });
 
       return result;
     },
-    [router, queryClient, _createProject]
+    [queryClient, _createProject]
   );
 }
 
@@ -83,7 +80,6 @@ const updateProject = createServerFn({ method: "POST" })
   });
 
 export function useUpdateProjectMutation() {
-  const router = useRouter();
   const queryClient = useQueryClient();
   const _updateProject = useServerFn(updateProject);
 
@@ -91,14 +87,13 @@ export function useUpdateProjectMutation() {
     async (projectData: UpdateProject) => {
       const result = await _updateProject({ data: projectData });
 
-      router.invalidate();
       queryClient.invalidateQueries({
         queryKey: ["projects", projectData.id],
       });
 
       return result;
     },
-    [router, queryClient, _updateProject]
+    [queryClient, _updateProject]
   );
 }
 
@@ -127,7 +122,6 @@ const deleteProject = createServerFn({ method: "POST" })
   });
 
 export function useDeleteProjectMutation() {
-  const router = useRouter();
   const queryClient = useQueryClient();
   const _deleteProject = useServerFn(deleteProject);
 
@@ -135,7 +129,6 @@ export function useDeleteProjectMutation() {
     async (id: string) => {
       const result = await _deleteProject({ data: { id } });
 
-      router.invalidate();
       queryClient.invalidateQueries({
         queryKey: ["projects"],
       });
@@ -145,6 +138,6 @@ export function useDeleteProjectMutation() {
 
       return result;
     },
-    [router, queryClient, _deleteProject]
+    [queryClient, _deleteProject]
   );
 }
diff --git a/src/db/mutations/sprints.ts b/src/db/mutations/sprints.ts
index 9bf3c84..d741170 100644
--- a/src/db/mutations/sprints.ts
+++ b/src/db/mutations/sprints.ts
@@ -13,7 +13,6 @@ import { getOwningIdentity } from "~/lib/utils";
 import { v7 as uuid } from "uuid";
 import { sync, syncDashboard } from "./sync";
 import { useCallback } from "react";
-import { useRouter } from "@tanstack/react-router";
 import { useQueryClient } from "@tanstack/react-query";
 import { and, eq } from "drizzle-orm";
 import { z } from "zod";
@@ -36,7 +35,6 @@ const createSprint = createServerFn({ method: "POST" })
   });
 
 export function useCreateSprintMutation() {
-  const router = useRouter();
   const queryClient = useQueryClient();
   const _createSprint = useServerFn(createSprint);
 
@@ -44,14 +42,13 @@ export function useCreateSprintMutation() {
     async (sprint: CreateSprint) => {
       const result = await _createSprint({ data: sprint });
 
-      router.invalidate();
       queryClient.invalidateQueries({
         queryKey: ["sprints"],
       });
 
       return result;
     },
-    [router, queryClient, _createSprint]
+    [queryClient, _createSprint]
   );
 }
 
@@ -73,7 +70,6 @@ const updateSprint = createServerFn({ method: "POST" })
   });
 
 export function useUpdateSprintMutation() {
-  const router = useRouter();
   const queryClient = useQueryClient();
   const _updateSprint = useServerFn(updateSprint);
 
@@ -81,7 +77,6 @@ export function useUpdateSprintMutation() {
     async (sprint: UpdateSprint) => {
       const result = await _updateSprint({ data: sprint });
 
-      router.invalidate();
       queryClient.invalidateQueries({
         queryKey: ["sprints"],
       });
@@ -91,7 +86,7 @@ export function useUpdateSprintMutation() {
 
       return result;
     },
-    [router, queryClient, _updateSprint]
+    [queryClient, _updateSprint]
   );
 }
 
@@ -115,7 +110,6 @@ export const deleteSprint = createServerFn({ method: "POST" })
   });
 
 export function useDeleteSprintMutation() {
-  const router = useRouter();
   const queryClient = useQueryClient();
   const _deleteSprint = useServerFn(deleteSprint);
 
@@ -123,7 +117,6 @@ export function useDeleteSprintMutation() {
     async (sprintId: string) => {
       const result = await _deleteSprint({ data: { sprintId } });
 
-      router.invalidate();
       queryClient.invalidateQueries({
         queryKey: ["sprints"],
       });
@@ -133,6 +126,6 @@ export function useDeleteSprintMutation() {
 
       return result;
     },
-    [router, queryClient, _deleteSprint]
+    [queryClient, _deleteSprint]
   );
 }
diff --git a/src/db/mutations/statuses.ts b/src/db/mutations/statuses.ts
index 264136f..1262431 100644
--- a/src/db/mutations/statuses.ts
+++ b/src/db/mutations/statuses.ts
@@ -12,7 +12,6 @@ import {
 } from "~/db/schema";
 import { v7 as uuid } from "uuid";
 import z from "zod";
-import { useRouter } from "@tanstack/react-router";
 import { useQueryClient } from "@tanstack/react-query";
 import { useCallback } from "react";
 import { and, eq, ne } from "drizzle-orm";
@@ -61,7 +60,6 @@ const createStatus = createServerFn({ method: "POST" })
   });
 
 export function useCreateStatusMutation() {
-  const router = useRouter();
   const queryClient = useQueryClient();
   const _createStatus = useServerFn(createStatus);
 
@@ -69,7 +67,6 @@ export function useCreateStatusMutation() {
     async (status: CreateStatus) => {
       const result = await _createStatus({ data: status });
 
-      router.invalidate();
       queryClient.invalidateQueries({
         queryKey: ["statuses"],
       });
@@ -79,7 +76,7 @@ export function useCreateStatusMutation() {
 
       return result;
     },
-    [router, queryClient, _createStatus]
+    [queryClient, _createStatus]
   );
 }
 
@@ -110,7 +107,6 @@ const updateStatus = createServerFn({ method: "POST" })
   });
 
 export function useUpdateStatusMutation() {
-  const router = useRouter();
   const queryClient = useQueryClient();
   const _updateStatus = useServerFn(updateStatus);
 
@@ -118,7 +114,6 @@ export function useUpdateStatusMutation() {
     async (status: UpdateStatus) => {
       const result = await _updateStatus({ data: status });
 
-      router.invalidate();
       queryClient.invalidateQueries({
         queryKey: ["statuses"],
       });
@@ -128,7 +123,7 @@ export function useUpdateStatusMutation() {
 
       return result;
     },
-    [router, queryClient, _updateStatus]
+    [queryClient, _updateStatus]
   );
 }
 
@@ -161,7 +156,6 @@ const updateMultipleStatuses = createServerFn({ method: "POST" })
   });
 
 export function useUpdateMultipleStatusesMutation() {
-  const router = useRouter();
   const queryClient = useQueryClient();
   const _updateMultipleStatuses = useServerFn(updateMultipleStatuses);
 
@@ -188,11 +182,9 @@ export function useUpdateMultipleStatusesMutation() {
         queryClient.invalidateQueries({ queryKey: ["statuses"] });
         queryClient.invalidateQueries({ queryKey: ["statuses-with-counts"] });
         throw error;
-      } finally {
-        router.invalidate();
       }
     },
-    [router, queryClient, _updateMultipleStatuses]
+    [queryClient, _updateMultipleStatuses]
   );
 }
 
@@ -222,7 +214,6 @@ const deleteStatus = createServerFn({ method: "POST" })
   });
 
 export function useDeleteStatusMutation() {
-  const router = useRouter();
   const queryClient = useQueryClient();
   const _deleteStatus = useServerFn(deleteStatus);
 
@@ -230,7 +221,6 @@ export function useDeleteStatusMutation() {
     async (id: string) => {
       const result = await _deleteStatus({ data: { id } });
 
-      router.invalidate();
       queryClient.invalidateQueries({
         queryKey: ["statuses"],
       });
@@ -240,6 +230,6 @@ export function useDeleteStatusMutation() {
 
       return result;
     },
-    [router, queryClient, _deleteStatus]
+    [queryClient, _deleteStatus]
   );
 }
diff --git a/src/db/mutations/sub-tasks.ts b/src/db/mutations/sub-tasks.ts
index e6f9b51..55a15d0 100644
--- a/src/db/mutations/sub-tasks.ts
+++ b/src/db/mutations/sub-tasks.ts
@@ -15,7 +15,6 @@ import { v7 as uuid } from "uuid";
 import { getOwningIdentity } from "~/lib/utils";
 import { getActorNameFromSession } from "~/lib/notifications/actor-name";
 import { sync } from "./sync";
-import { useRouter } from "@tanstack/react-router";
 import { useQueryClient } from "@tanstack/react-query";
 import { useCallback } from "react";
 import { and, eq, inArray } from "drizzle-orm";
@@ -37,7 +36,6 @@ const createSubTask = createServerFn({ method: "POST" })
   });
 
 export function useCreateSubTaskMutation() {
-  const router = useRouter();
   const queryClient = useQueryClient();
   const _createSubTask = useServerFn(createSubTask);
 
@@ -45,14 +43,13 @@ export function useCreateSubTaskMutation() {
     async (subTask: CreateSubTask) => {
       const result = await _createSubTask({ data: subTask });
 
-      router.invalidate();
       queryClient.invalidateQueries({
         queryKey: ["tasks", subTask.taskId],
       });
 
       return result;
     },
-    [router, queryClient, _createSubTask]
+    [queryClient, _createSubTask]
   );
 }
 
@@ -77,7 +74,6 @@ const updateSubTask = createServerFn({ method: "POST" })
   });
 
 export function useUpdateSubTaskMutation() {
-  const router = useRouter();
   const queryClient = useQueryClient();
   const _updateSubTask = useServerFn(updateSubTask);
 
@@ -104,7 +100,6 @@ export function useUpdateSubTaskMutation() {
         const result = await _updateSubTask({ data: subTask });
 
         // On success, invalidate queries to ensure consistency
-        router.invalidate();
         queryClient.invalidateQueries({
           queryKey: ["tasks", subTask.taskId],
         });
@@ -119,7 +114,7 @@ export function useUpdateSubTaskMutation() {
         throw error;
       }
     },
-    [router, queryClient, _updateSubTask]
+    [queryClient, _updateSubTask]
   );
 }
 
@@ -152,7 +147,6 @@ const deleteSubTask = createServerFn({ method: "POST" })
   });
 
 export function useDeleteSubTaskMutation() {
-  const router = useRouter();
   const queryClient = useQueryClient();
   const _deleteSubTask = useServerFn(deleteSubTask);
 
@@ -161,7 +155,6 @@ export function useDeleteSubTaskMutation() {
       const result = await _deleteSubTask({ data: { id: subTaskId } });
 
       if (!result) return;
-      router.invalidate();
       queryClient.invalidateQueries({
         queryKey: ["tasks", result.projectId],
       });
@@ -171,7 +164,7 @@ export function useDeleteSubTaskMutation() {
 
       return result;
     },
-    [router, queryClient, _deleteSubTask]
+    [queryClient, _deleteSubTask]
   );
 }
 
@@ -205,7 +198,6 @@ const unassignSubTask = createServerFn({ method: "POST" })
   });
 
 export function useUnassignSubTaskMutation() {
-  const router = useRouter();
   const queryClient = useQueryClient();
   const _unassignSubTask = useServerFn(unassignSubTask);
 
@@ -213,7 +205,6 @@ export function useUnassignSubTaskMutation() {
     async (subTask: SubTask, userIds: string[]) => {
       await _unassignSubTask({ data: { subTaskId: subTask.id, userIds } });
 
-      router.invalidate();
       queryClient.invalidateQueries({
         queryKey: ["tasks", subTask.taskId],
       });
@@ -221,7 +212,7 @@ export function useUnassignSubTaskMutation() {
         queryKey: ["tasks", subTask.projectId],
       });
     },
-    [router, queryClient, _unassignSubTask]
+    [queryClient, _unassignSubTask]
   );
 }
 
@@ -288,7 +279,6 @@ const assignSubTask = createServerFn({ method: "POST" })
   });
 
 export function useAssignSubTaskMutation() {
-  const router = useRouter();
   const queryClient = useQueryClient();
   const _assignSubTask = useServerFn(assignSubTask);
 
@@ -296,7 +286,6 @@ export function useAssignSubTaskMutation() {
     async (subTask: SubTask, userIds: string[]) => {
       await _assignSubTask({ data: { subTaskId: subTask.id, userIds } });
 
-      router.invalidate();
       queryClient.invalidateQueries({
         queryKey: ["tasks", subTask.taskId],
       });
@@ -304,6 +293,6 @@ export function useAssignSubTaskMutation() {
         queryKey: ["tasks", subTask.projectId],
       });
     },
-    [router, queryClient, _assignSubTask]
+    [queryClient, _assignSubTask]
   );
 }
diff --git a/src/db/mutations/sync.ts b/src/db/mutations/sync.ts
index 8fba4c2..bb2c3f9 100644
--- a/src/db/mutations/sync.ts
+++ b/src/db/mutations/sync.ts
@@ -25,30 +25,59 @@ export async function syncDashboard(owner: string, payload: unknown = {}) {
   await sync(ownerDashboardTopic(owner), payload);
 }
 
+const COARSE_TOPIC_PREFIXES: Record<string, string> = {
+  "task-update-": "task-update",
+  "project-update-": "project-update",
+  "sprint-update-": "sprint-update",
+  "status-update-": "status-update",
+  "label-update-": "label-update",
+  "comment-update-": "comment-update",
+  "attachment-update-": "attachment-update",
+};
+
+async function publishSyncTopic(
+  baseUrl: string,
+  topic: string,
+  payload: unknown
+) {
+  const response = await fetch(
+    `${baseUrl}/stream/${env.SYNC_APP_ID}?key=${env.SYNC_PUBLISH_KEY}&topic=${topic}`,
+    {
+      method: "POST",
+      headers: {
+        "Content-Type": "application/json",
+      },
+      body: JSON.stringify({ topic, payload }),
+    }
+  );
+
+  if (!response.ok) {
+    const body = await response.text();
+    console.error(
+      `Sync failed for topic "${topic}" (app ${env.SYNC_APP_ID}, ${baseUrl}): ${response.status} ${body}`
+    );
+  }
+}
+
 export async function sync(topic: string, payload: unknown) {
   const baseUrl = resolveSyncEngineUrl();
   try {
-    const response = await fetch(
-      `${baseUrl}/stream/${env.SYNC_APP_ID}?key=${env.SYNC_PUBLISH_KEY}&topic=${topic}`,
-      {
-        method: "POST",
-        headers: {
-          "Content-Type": "application/json",
-        },
-        body: JSON.stringify({ payload }),
-      },
-    );
+    await publishSyncTopic(baseUrl, topic, payload);
 
-    if (!response.ok) {
-      const body = await response.text();
-      console.error(
-        `Sync failed for topic "${topic}" (app ${env.SYNC_APP_ID}, ${baseUrl}): ${response.status} ${body}`,
-      );
+    for (const [prefix, coarseTopic] of Object.entries(COARSE_TOPIC_PREFIXES)) {
+      if (!topic.startsWith(prefix)) continue;
+      const entityId = topic.slice(prefix.length);
+      await publishSyncTopic(baseUrl, coarseTopic, {
+        topic: coarseTopic,
+        entityId,
+        payload,
+      });
+      break;
     }
   } catch (error) {
     console.error(
       `Sync error for topic "${topic}" (app ${env.SYNC_APP_ID}, ${baseUrl}):`,
-      error,
+      error
     );
   }
 }
diff --git a/src/db/mutations/tasks.ts b/src/db/mutations/tasks.ts
index dc54ebe..97286ff 100644
--- a/src/db/mutations/tasks.ts
+++ b/src/db/mutations/tasks.ts
@@ -18,7 +18,6 @@ import {
   updateTaskValidator,
 } from "~/db/schema";
 import { v7 as uuid } from "uuid";
-import { useRouter } from "@tanstack/react-router";
 import { useQueryClient } from "@tanstack/react-query";
 import { useCallback } from "react";
 import { and, eq, inArray, isNull, max } from "drizzle-orm";
@@ -277,7 +276,6 @@ const createTask = createServerFn({ method: "POST" })
   });
 
 export function useCreateTaskMutation() {
-  const router = useRouter();
   const queryClient = useQueryClient();
   const _createTask = useServerFn(createTask);
 
@@ -287,7 +285,6 @@ export function useCreateTaskMutation() {
         data: { ...task, updatedAt: new Date() },
       });
 
-      router.invalidate();
       queryClient.invalidateQueries({
         queryKey: ["tasks", task.projectId],
       });
@@ -300,7 +297,7 @@ export function useCreateTaskMutation() {
 
       return result;
     },
-    [router, queryClient, _createTask]
+    [queryClient, _createTask]
   );
 }
 
@@ -395,7 +392,6 @@ const unassignTask = createServerFn({ method: "POST" })
 
 // Update the hook interfaces
 export function useAssignTaskMutation() {
-  const router = useRouter();
   const queryClient = useQueryClient();
   const _assignTask = useServerFn(assignTask);
 
@@ -403,7 +399,6 @@ export function useAssignTaskMutation() {
     async (task: Task, userIds: string[]) => {
       await _assignTask({ data: { taskId: task.id, userIds } });
 
-      router.invalidate();
       queryClient.invalidateQueries({
         queryKey: ["tasks", task.projectId],
       });
@@ -416,12 +411,11 @@ export function useAssignTaskMutation() {
         });
       }
     },
-    [router, queryClient, _assignTask]
+    [queryClient, _assignTask]
   );
 }
 
 export function useUnassignTaskMutation() {
-  const router = useRouter();
   const queryClient = useQueryClient();
   const _unassignTask = useServerFn(unassignTask);
 
@@ -429,7 +423,6 @@ export function useUnassignTaskMutation() {
     async (task: Task, userIds: string[]) => {
       await _unassignTask({ data: { taskId: task.id, userIds } });
 
-      router.invalidate();
       queryClient.invalidateQueries({
         queryKey: ["tasks", task.projectId],
       });
@@ -442,7 +435,7 @@ export function useUnassignTaskMutation() {
         });
       }
     },
-    [router, queryClient, _unassignTask]
+    [queryClient, _unassignTask]
   );
 }
 
@@ -474,7 +467,6 @@ const updateTask = createServerFn({ method: "POST" })
   });
 
 export function useUpdateTaskMutation() {
-  const router = useRouter();
   const queryClient = useQueryClient();
   const _updateTask = useServerFn(updateTask);
 
@@ -511,17 +503,9 @@ export function useUpdateTaskMutation() {
       );
 
       try {
-        const result = await _updateTask({
+        return await _updateTask({
           data: task,
         });
-
-        for (const queryKey of listQueryKeys) {
-          queryClient.invalidateQueries({ queryKey });
-        }
-        queryClient.invalidateQueries({ queryKey: detailQueryKey });
-        router.invalidate();
-
-        return result;
       } catch (error) {
         for (const { queryKey, data } of snapshots) {
           queryClient.setQueryData(queryKey, data);
@@ -535,7 +519,7 @@ export function useUpdateTaskMutation() {
         throw error;
       }
     },
-    [router, queryClient, _updateTask]
+    [queryClient, _updateTask]
   );
 }
 
@@ -576,7 +560,6 @@ const deleteTask = createServerFn({ method: "POST" })
   });
 
 export function useDeleteTaskMutation() {
-  const router = useRouter();
   const queryClient = useQueryClient();
   const _deleteTask = useServerFn(deleteTask);
 
@@ -584,7 +567,6 @@ export function useDeleteTaskMutation() {
     async (id: string) => {
       const result = await _deleteTask({ data: { id } });
 
-      router.invalidate();
       queryClient.invalidateQueries({
         queryKey: ["tasks"],
       });
@@ -594,7 +576,7 @@ export function useDeleteTaskMutation() {
 
       return result;
     },
-    [router, queryClient, _deleteTask]
+    [queryClient, _deleteTask]
   );
 }
 
@@ -640,7 +622,6 @@ const setLabelsForTask = createServerFn({ method: "POST" })
   });
 
 export function useSetLabelsForTaskMutation() {
-  const router = useRouter();
   const queryClient = useQueryClient();
   const _setLabelsForTask = useServerFn(setLabelsForTask);
 
@@ -661,12 +642,25 @@ export function useSetLabelsForTaskMutation() {
 
       await _setLabelsForTask({ data: { taskId: task.id, labelIds } });
 
-      router.invalidate();
-      queryClient.invalidateQueries({ queryKey: ["tasks"] });
-      queryClient.invalidateQueries({ queryKey: ["tasks", task.projectId] });
-      queryClient.invalidateQueries({ queryKey: ["tasks", task.id] });
+      const listQueryKeys = getTaskListQueryKeys(queryClient, task);
+      const patchList = (oldData: TaskWithRelations[] | undefined) => {
+        if (!oldData) return oldData;
+        return oldData.map((entry) =>
+          entry.id === task.id
+            ? {
+                ...entry,
+                labels: labelIds.map(
+                  (id) => labels.find((label) => label.id === id)!
+                ),
+              }
+            : entry
+        );
+      };
+      for (const queryKey of listQueryKeys) {
+        queryClient.setQueryData(queryKey, patchList);
+      }
     },
-    [router, queryClient, _setLabelsForTask]
+    [queryClient, _setLabelsForTask]
   );
 }
 
@@ -875,7 +869,6 @@ function snapshotTaskQueries(
 }
 
 export function useBatchUpdateTasksMutation() {
-  const router = useRouter();
   const queryClient = useQueryClient();
   const _batchUpdateTasks = useServerFn(batchUpdateTasks);
 
@@ -910,13 +903,6 @@ export function useBatchUpdateTasksMutation() {
 
       try {
         await _batchUpdateTasks({ data: { taskIds, ...patch } });
-        for (const queryKey of listQueryKeys) {
-          queryClient.invalidateQueries({ queryKey });
-        }
-        for (const id of taskIds) {
-          queryClient.invalidateQueries({ queryKey: ["tasks", id] });
-        }
-        router.invalidate();
       } catch (error) {
         for (const { queryKey, data } of snapshots) {
           queryClient.setQueryData(queryKey, data);
@@ -925,12 +911,11 @@ export function useBatchUpdateTasksMutation() {
         throw error;
       }
     },
-    [router, queryClient, _batchUpdateTasks]
+    [queryClient, _batchUpdateTasks]
   );
 }
 
 export function useBatchDeleteTasksMutation() {
-  const router = useRouter();
   const queryClient = useQueryClient();
   const _batchDeleteTasks = useServerFn(batchDeleteTasks);
 
@@ -954,8 +939,6 @@ export function useBatchDeleteTasksMutation() {
 
       try {
         await _batchDeleteTasks({ data: { taskIds } });
-        queryClient.invalidateQueries({ queryKey: ["tasks"] });
-        router.invalidate();
       } catch (error) {
         for (const { queryKey, data } of snapshots) {
           queryClient.setQueryData(queryKey, data);
@@ -964,12 +947,11 @@ export function useBatchDeleteTasksMutation() {
         throw error;
       }
     },
-    [router, queryClient, _batchDeleteTasks]
+    [queryClient, _batchDeleteTasks]
   );
 }
 
 export function useBatchAssignTasksMutation() {
-  const router = useRouter();
   const queryClient = useQueryClient();
   const _batchAssignTasks = useServerFn(batchAssignTasks);
 
@@ -1035,13 +1017,6 @@ export function useBatchAssignTasksMutation() {
 
       try {
         await _batchAssignTasks({ data: { taskIds, userIds } });
-        for (const queryKey of listQueryKeys) {
-          queryClient.invalidateQueries({ queryKey });
-        }
-        for (const id of taskIds) {
-          queryClient.invalidateQueries({ queryKey: ["tasks", id] });
-        }
-        router.invalidate();
       } catch (error) {
         for (const { queryKey, data } of snapshots) {
           queryClient.setQueryData(queryKey, data);
@@ -1050,12 +1025,11 @@ export function useBatchAssignTasksMutation() {
         throw error;
       }
     },
-    [router, queryClient, _batchAssignTasks]
+    [queryClient, _batchAssignTasks]
   );
 }
 
 export function useBatchSetLabelsForTasksMutation() {
-  const router = useRouter();
   const queryClient = useQueryClient();
   const _batchSetLabelsForTasks = useServerFn(batchSetLabelsForTasks);
 
@@ -1097,13 +1071,6 @@ export function useBatchSetLabelsForTasksMutation() {
 
       try {
         await _batchSetLabelsForTasks({ data: { taskIds, labelIds } });
-        for (const queryKey of listQueryKeys) {
-          queryClient.invalidateQueries({ queryKey });
-        }
-        for (const id of taskIds) {
-          queryClient.invalidateQueries({ queryKey: ["tasks", id] });
-        }
-        router.invalidate();
       } catch (error) {
         for (const { queryKey, data } of snapshots) {
           queryClient.setQueryData(queryKey, data);
@@ -1112,7 +1079,7 @@ export function useBatchSetLabelsForTasksMutation() {
         throw error;
       }
     },
-    [router, queryClient, _batchSetLabelsForTasks]
+    [queryClient, _batchSetLabelsForTasks]
   );
 }
 
@@ -1294,7 +1261,6 @@ const moveTaskInBucket = createServerFn({ method: "POST" })
   });
 
 export function useReorderTasksMutation() {
-  const router = useRouter();
   const queryClient = useQueryClient();
   const _reorderTasks = useServerFn(reorderTasks);
 
@@ -1325,11 +1291,6 @@ export function useReorderTasksMutation() {
 
       try {
         await _reorderTasks({ data: input });
-        for (const queryKey of listQueryKeys) {
-          queryClient.invalidateQueries({ queryKey });
-        }
-        queryClient.invalidateQueries({ queryKey: allTasksListQueryKey });
-        router.invalidate();
       } catch (error) {
         for (const { queryKey, data } of snapshots) {
           queryClient.setQueryData(queryKey, data);
@@ -1338,12 +1299,11 @@ export function useReorderTasksMutation() {
         throw error;
       }
     },
-    [router, queryClient, _reorderTasks]
+    [queryClient, _reorderTasks]
   );
 }
 
 export function useMoveTaskInBucketMutation() {
-  const router = useRouter();
   const queryClient = useQueryClient();
   const _moveTaskInBucket = useServerFn(moveTaskInBucket);
 
@@ -1360,14 +1320,43 @@ export function useMoveTaskInBucketMutation() {
         listQueryKeys
       );
 
+      const patchMovedTask = (oldData: TaskWithRelations[] | undefined) => {
+        if (!oldData) return oldData;
+        const taskIndex = oldData.findIndex((task) => task.id === input.taskId);
+        if (taskIndex === -1) return oldData;
+
+        const movedTask = {
+          ...oldData[taskIndex]!,
+          statusId: input.statusId,
+          updatedAt: new Date(),
+        };
+        const withoutTask = oldData.filter((task) => task.id !== input.taskId);
+        const next = [...withoutTask];
+        next.splice(
+          Math.min(input.targetIndex, next.length),
+          0,
+          movedTask
+        );
+        return next;
+      };
+
+      for (const queryKey of listQueryKeys) {
+        queryClient.setQueryData(queryKey, patchMovedTask);
+      }
+      queryClient.setQueryData(
+        ["tasks", input.taskId],
+        (oldData: TaskWithRelations | undefined) => {
+          if (!oldData) return oldData;
+          return {
+            ...oldData,
+            statusId: input.statusId,
+            updatedAt: new Date(),
+          };
+        }
+      );
+
       try {
         await _moveTaskInBucket({ data: input });
-        for (const queryKey of listQueryKeys) {
-          queryClient.invalidateQueries({ queryKey });
-        }
-        queryClient.invalidateQueries({ queryKey: ["tasks", input.taskId] });
-        queryClient.invalidateQueries({ queryKey: allTasksListQueryKey });
-        router.invalidate();
       } catch (error) {
         for (const { queryKey, data } of snapshots) {
           queryClient.setQueryData(queryKey, data);
@@ -1376,6 +1365,6 @@ export function useMoveTaskInBucketMutation() {
         throw error;
       }
     },
-    [router, queryClient, _moveTaskInBucket]
+    [queryClient, _moveTaskInBucket]
   );
 }
diff --git a/src/db/queries/attachments.ts b/src/db/queries/attachments.ts
index 89a4adf..1119fd2 100644
--- a/src/db/queries/attachments.ts
+++ b/src/db/queries/attachments.ts
@@ -3,7 +3,6 @@ import { createServerFn } from "@tanstack/react-start";
 import { db } from "..";
 import { getOwningIdentity } from "~/lib/utils";
 import { queryOptions, useSuspenseQuery } from "@tanstack/react-query";
-import { useEventSource } from "~/hooks/use-event-source";
 
 const fetchAttachmentsForTask = createServerFn({ method: "GET" })
   .inputValidator((data?: string) => data)
@@ -11,7 +10,6 @@ const fetchAttachmentsForTask = createServerFn({ method: "GET" })
     if (!data) {
       return [];
     }
-    console.info(`Fetching attachments for... ${data}`);
     const session = await requireSessionFromRequest();
     return await db.query.attachments.findMany({
       where: (model, { eq, and }) =>
@@ -27,18 +25,5 @@ export const attachmentsQueryOptions = (taskId?: string) =>
   });
 
 export const useAttachmentsQuery = (taskId: string) => {
-  const queryData = useSuspenseQuery(attachmentsQueryOptions(taskId));
-
-  useEventSource({
-    topics: [
-      "attachment-create",
-      "attachment-delete",
-      ...queryData.data.map((t) => `attachment-update-${t.id}`),
-    ],
-    callback: () => {
-      queryData.refetch();
-    },
-  });
-
-  return { ...queryData };
+  return useSuspenseQuery(attachmentsQueryOptions(taskId));
 };
diff --git a/src/db/queries/bundles.ts b/src/db/queries/bundles.ts
index 27e1266..3806ffb 100644
--- a/src/db/queries/bundles.ts
+++ b/src/db/queries/bundles.ts
@@ -16,6 +16,7 @@ export type ProjectBoardBundle = {
   project?: Project;
   tasks: TaskWithRelations[];
   statuses: Status[];
+  labels: Label[];
   users: AppUser[];
 };
 
@@ -23,12 +24,14 @@ export type SprintBoardBundle = {
   sprint: Sprint | null | undefined;
   tasks: TaskWithRelations[];
   statuses: Status[];
+  labels: Label[];
   users: AppUser[];
 };
 
 export type AllTasksBoardBundle = {
   tasks: TaskWithRelations[];
   statuses: Status[];
+  labels: Label[];
   users: AppUser[];
 };
 
@@ -61,7 +64,7 @@ export const fetchProjectBoardBundle = createServerFn({ method: "GET" })
       const owner = getOwningIdentity(session);
       const { getUsersForSession } = await import("~/db/queries/users.server");
 
-      const [project, rawTasks, statuses, users] = await Promise.all([
+      const [project, rawTasks, statuses, labels, users] = await Promise.all([
         db.query.projects.findFirst({
           where: (model, { eq, and }) =>
             and(eq(model.id, projectId), eq(model.owner, owner)),
@@ -75,6 +78,10 @@ export const fetchProjectBoardBundle = createServerFn({ method: "GET" })
           where: (model, { eq }) => eq(model.owner, owner),
           orderBy: (fields, { asc: ascFn }) => [ascFn(fields.order)],
         }),
+        db.query.labels.findMany({
+          where: (model, { eq }) => eq(model.owner, owner),
+          orderBy: (fields, { asc: ascFn }) => [ascFn(fields.order)],
+        }),
         getUsersForSession(session),
       ]);
 
@@ -84,6 +91,7 @@ export const fetchProjectBoardBundle = createServerFn({ method: "GET" })
           includeProject: false,
         }),
         statuses: statuses as Status[],
+        labels: labels as Label[],
         users,
       };
     }
@@ -99,7 +107,7 @@ export const fetchSprintBoardBundle = createServerFn({ method: "GET" })
       const owner = getOwningIdentity(session);
       const { getUsersForSession } = await import("~/db/queries/users.server");
 
-      const [sprint, rawTasks, statuses, users] = await Promise.all([
+      const [sprint, rawTasks, statuses, labels, users] = await Promise.all([
         db.query.sprints.findFirst({
           where: (model, { eq, and }) =>
             and(eq(model.id, sprintId), eq(model.owner, owner)),
@@ -113,6 +121,10 @@ export const fetchSprintBoardBundle = createServerFn({ method: "GET" })
           where: (model, { eq }) => eq(model.owner, owner),
           orderBy: (fields, { asc: ascFn }) => [ascFn(fields.order)],
         }),
+        db.query.labels.findMany({
+          where: (model, { eq }) => eq(model.owner, owner),
+          orderBy: (fields, { asc: ascFn }) => [ascFn(fields.order)],
+        }),
         getUsersForSession(session),
       ]);
 
@@ -120,6 +132,7 @@ export const fetchSprintBoardBundle = createServerFn({ method: "GET" })
         sprint,
         tasks: mapBoardTasks(rawTasks, { includeProject: true }),
         statuses: statuses as Status[],
+        labels: labels as Label[],
         users,
       };
     }
@@ -131,7 +144,7 @@ export const fetchAllTasksBoardBundle = createServerFn({ method: "GET" }).handle
     const owner = getOwningIdentity(session);
     const { getUsersForSession } = await import("~/db/queries/users.server");
 
-    const [rawTasks, statuses, users] = await Promise.all([
+    const [rawTasks, statuses, labels, users] = await Promise.all([
       db.query.tasks.findMany({
         with: boardTaskRelationsForSprint,
         where: (model, { eq }) => eq(model.owner, owner),
@@ -140,12 +153,17 @@ export const fetchAllTasksBoardBundle = createServerFn({ method: "GET" }).handle
         where: (model, { eq }) => eq(model.owner, owner),
         orderBy: (fields, { asc: ascFn }) => [ascFn(fields.order)],
       }),
+      db.query.labels.findMany({
+        where: (model, { eq }) => eq(model.owner, owner),
+        orderBy: (fields, { asc: ascFn }) => [ascFn(fields.order)],
+      }),
       getUsersForSession(session),
     ]);
 
     return {
       tasks: mapBoardTasks(rawTasks, { includeProject: true }),
       statuses: statuses as Status[],
+      labels: labels as Label[],
       users,
     };
   }
diff --git a/src/db/queries/comments.ts b/src/db/queries/comments.ts
index c738b56..0a93ab9 100644
--- a/src/db/queries/comments.ts
+++ b/src/db/queries/comments.ts
@@ -2,7 +2,6 @@ import { requireSessionFromRequest } from "~/lib/session";
 import { createServerFn } from "@tanstack/react-start";
 import { getOwningIdentity } from "~/lib/utils";
 import { queryOptions, useSuspenseQuery } from "@tanstack/react-query";
-import { useEventSource } from "~/hooks/use-event-source";
 
 export type { CommentWithAuthor } from "~/db/queries/comments.server";
 
@@ -25,17 +24,5 @@ export const commentsQueryOptions = (taskId?: string) =>
   });
 
 export const useCommentsQuery = (taskId: string) => {
-  const queryData = useSuspenseQuery(commentsQueryOptions(taskId));
-
-  useEventSource({
-    topics: [
-      "comment-create",
-      ...queryData.data.map((t) => `comment-update-${t.id}`),
-    ],
-    callback: () => {
-      queryData.refetch();
-    },
-  });
-
-  return { ...queryData };
+  return useSuspenseQuery(commentsQueryOptions(taskId));
 };
diff --git a/src/db/queries/dashboard.ts b/src/db/queries/dashboard.ts
index 88f4c30..2db36ac 100644
--- a/src/db/queries/dashboard.ts
+++ b/src/db/queries/dashboard.ts
@@ -13,8 +13,6 @@ import {
 } from "drizzle-orm";
 import { db } from "~/db";
 import { Sprint, statuses, tasks } from "~/db/schema";
-import { ownerDashboardTopic } from "~/lib/owner-dashboard-topic";
-import { useEventSource } from "~/hooks/use-event-source";
 import { requireSessionFromRequest } from "~/lib/session";
 import { getOwningIdentity } from "~/lib/utils";
 
@@ -98,14 +96,5 @@ export const dashboardStatsQueryOptions = () =>
   });
 
 export const useDashboardStatsQuery = () => {
-  const queryData = useSuspenseQuery(dashboardStatsQueryOptions());
-
-  useEventSource({
-    topics: [ownerDashboardTopic(queryData.data.owner)],
-    callback: () => {
-      queryData.refetch();
-    },
-  });
-
-  return { ...queryData };
+  return useSuspenseQuery(dashboardStatsQueryOptions());
 };
diff --git a/src/db/queries/hydrate-query-cache.ts b/src/db/queries/hydrate-query-cache.ts
index c2cbcf0..bcc1e4b 100644
--- a/src/db/queries/hydrate-query-cache.ts
+++ b/src/db/queries/hydrate-query-cache.ts
@@ -54,6 +54,10 @@ export function hydrateProjectBoardCache(
     statusesQueryOptions().queryKey,
     data.statuses as StatusesCache
   );
+  queryClient.setQueryData(
+    labelsQueryOptions().queryKey,
+    data.labels as LabelsCache
+  );
   queryClient.setQueryData(usersQueryOptions().queryKey, data.users);
 }
 
@@ -66,6 +70,10 @@ export function hydrateAllTasksBoardCache(
     statusesQueryOptions().queryKey,
     data.statuses as StatusesCache
   );
+  queryClient.setQueryData(
+    labelsQueryOptions().queryKey,
+    data.labels as LabelsCache
+  );
   queryClient.setQueryData(usersQueryOptions().queryKey, data.users);
 }
 
@@ -85,6 +93,10 @@ export function hydrateSprintBoardCache(
     statusesQueryOptions().queryKey,
     data.statuses as StatusesCache
   );
+  queryClient.setQueryData(
+    labelsQueryOptions().queryKey,
+    data.labels as LabelsCache
+  );
   queryClient.setQueryData(usersQueryOptions().queryKey, data.users);
 }
 
diff --git a/src/db/queries/labels.ts b/src/db/queries/labels.ts
index 0194168..cbacea7 100644
--- a/src/db/queries/labels.ts
+++ b/src/db/queries/labels.ts
@@ -5,11 +5,9 @@ import { labels, labelsToTasks } from "~/db/schema";
 import { asc, eq, sql } from "drizzle-orm";
 import { requireSessionFromRequest } from "~/lib/session";
 import { getOwningIdentity } from "~/lib/utils";
-import { useEventSource } from "~/hooks/use-event-source";
 
 export const fetchLabels = createServerFn({ method: "GET" }).handler(
   async () => {
-    console.info("Fetching labels...");
     const session = await requireSessionFromRequest();
     return await db.query.labels.findMany({
       where: (model, { eq }) => eq(model.owner, getOwningIdentity(session)),
@@ -26,26 +24,12 @@ export const labelsQueryOptions = () =>
   });
 
 export const useLabelsQuery = () => {
-  const queryData = useSuspenseQuery(labelsQueryOptions());
-
-  useEventSource({
-    topics: [
-      "label-create",
-      "label-delete",
-      ...queryData.data.map((t) => `label-update-${t.id}`),
-    ],
-    callback: () => {
-      queryData.refetch();
-    },
-  });
-
-  return { ...queryData };
+  return useSuspenseQuery(labelsQueryOptions());
 };
 
 export const fetchLabelsWithTaskCounts = createServerFn({
   method: "GET",
 }).handler(async () => {
-  console.info("Fetching labels with task counts...");
   const session = await requireSessionFromRequest();
   const labelsWithCounts = await db
     .select({
@@ -73,18 +57,5 @@ export const labelsWithCountsQueryOptions = () =>
   });
 
 export const useLabelsWithCountsQuery = () => {
-  const queryData = useSuspenseQuery(labelsWithCountsQueryOptions());
-
-  useEventSource({
-    topics: [
-      "label-create",
-      "label-delete",
-      ...queryData.data.map((t) => `label-update-${t.id}`),
-    ],
-    callback: () => {
-      queryData.refetch();
-    },
-  });
-
-  return { ...queryData };
+  return useSuspenseQuery(labelsWithCountsQueryOptions());
 };
diff --git a/src/db/queries/notifications.ts b/src/db/queries/notifications.ts
index 1c57f11..8d4088d 100644
--- a/src/db/queries/notifications.ts
+++ b/src/db/queries/notifications.ts
@@ -3,9 +3,7 @@ import { createServerFn } from "@tanstack/react-start";
 import { and, count, desc, eq, isNull } from "drizzle-orm";
 import { db } from "~/db";
 import { inAppNotifications } from "~/db/schema";
-import { useEventSource } from "~/hooks/use-event-source";
 import { authClient } from "~/lib/auth-client";
-import { userNotificationsTopic } from "~/lib/user-notifications-topic";
 import { requireSessionFromRequest } from "~/lib/session";
 
 export type InAppNotificationFeed = {
@@ -69,17 +67,8 @@ export const inAppNotificationFeedQueryOptions = () =>
 
 export function useInAppNotificationFeedQuery() {
   const { data: session } = authClient.useSession();
-  const query = useQuery({
+  return useQuery({
     ...inAppNotificationFeedQueryOptions(),
     enabled: Boolean(session),
   });
-
-  useEventSource({
-    topics: session ? [userNotificationsTopic(session.user.id)] : [],
-    callback: () => {
-      void query.refetch();
-    },
-  });
-
-  return query;
 }
diff --git a/src/db/queries/projects.ts b/src/db/queries/projects.ts
index 8dd41c2..aa30f0f 100644
--- a/src/db/queries/projects.ts
+++ b/src/db/queries/projects.ts
@@ -8,10 +8,8 @@ import {
 } from "~/lib/project-order";
 import { loadUserPreferences } from "~/lib/user-preferences.server";
 import { getOwningIdentity } from "~/lib/utils";
-import { useEventSource } from "~/hooks/use-event-source";
 
 const fetchProjects = createServerFn({ method: "GET" }).handler(async () => {
-  console.info("Fetching projects...");
   const session = await requireSessionFromRequest();
   const owner = getOwningIdentity(session);
   const [projects, preferences] = await Promise.all([
@@ -34,26 +32,12 @@ export const projectsQueryOptions = () =>
   });
 
 export const useProjectsQuery = () => {
-  const queryData = useSuspenseQuery(projectsQueryOptions());
-
-  useEventSource({
-    topics: [
-      "project-create",
-      "project-delete",
-      ...queryData.data.map((t) => `project-update-${t.id}`),
-    ],
-    callback: () => {
-      queryData.refetch();
-    },
-  });
-
-  return { ...queryData };
+  return useSuspenseQuery(projectsQueryOptions());
 };
 
 const fetchProject = createServerFn({ method: "GET" })
   .inputValidator((d: { projectId: string }) => d)
   .handler(async ({ data: { projectId } }) => {
-    console.info("Fetching project...");
     const session = await requireSessionFromRequest();
     return await db.query.projects.findFirst({
       where: (model, { eq, and }) =>
@@ -68,14 +52,5 @@ export const projectQueryOptions = (projectId: string) =>
   });
 
 export const useProjectQuery = (projectId: string) => {
-  const queryData = useSuspenseQuery(projectQueryOptions(projectId));
-
-  useEventSource({
-    topics: [`project-update-${projectId}`],
-    callback: () => {
-      queryData.refetch();
-    },
-  });
-
-  return { ...queryData };
+  return useSuspenseQuery(projectQueryOptions(projectId));
 };
diff --git a/src/db/queries/sprints.ts b/src/db/queries/sprints.ts
index e057a73..f714dc6 100644
--- a/src/db/queries/sprints.ts
+++ b/src/db/queries/sprints.ts
@@ -3,10 +3,8 @@ import { createServerFn } from "@tanstack/react-start";
 import { db } from "~/db";
 import { requireSessionFromRequest } from "~/lib/session";
 import { getOwningIdentity } from "~/lib/utils";
-import { useEventSource } from "~/hooks/use-event-source";
 
 const fetchSprints = createServerFn({ method: "GET" }).handler(async () => {
-  console.info("Fetching sprints...");
   const session = await requireSessionFromRequest();
 
   return await db.query.sprints.findMany({
@@ -22,26 +20,12 @@ export const sprintsQueryOptions = () =>
   });
 
 export const useSprintsQuery = () => {
-  const queryData = useSuspenseQuery(sprintsQueryOptions());
-
-  useEventSource({
-    topics: [
-      "sprint-create",
-      "sprint-delete",
-      ...queryData.data.map((t) => `sprint-update-${t.id}`),
-    ],
-    callback: () => {
-      queryData.refetch();
-    },
-  });
-
-  return { ...queryData };
+  return useSuspenseQuery(sprintsQueryOptions());
 };
 
 const fetchSprint = createServerFn({ method: "GET" })
   .inputValidator((d: { sprintId: string }) => d)
   .handler(async ({ data: { sprintId } }) => {
-    console.info("Fetching sprint...");
     const session = await requireSessionFromRequest();
 
     return await db.query.sprints.findFirst({
@@ -58,14 +42,5 @@ export const sprintQueryOptions = (sprintId: string) =>
   });
 
 export const useSprintQuery = (sprintId: string) => {
-  const queryData = useSuspenseQuery(sprintQueryOptions(sprintId));
-
-  useEventSource({
-    topics: [`sprint-update-${sprintId}`],
-    callback: () => {
-      queryData.refetch();
-    },
-  });
-
-  return { ...queryData };
+  return useSuspenseQuery(sprintQueryOptions(sprintId));
 };
diff --git a/src/db/queries/statuses.ts b/src/db/queries/statuses.ts
index bd56084..5fadc15 100644
--- a/src/db/queries/statuses.ts
+++ b/src/db/queries/statuses.ts
@@ -5,11 +5,9 @@ import { statuses, tasks } from "~/db/schema";
 import { asc, eq, sql } from "drizzle-orm";
 import { requireSessionFromRequest } from "~/lib/session";
 import { getOwningIdentity } from "~/lib/utils";
-import { useEventSource } from "~/hooks/use-event-source";
 
 export const fetchStatuses = createServerFn({ method: "GET" }).handler(
   async () => {
-    console.info("Fetching statuses...");
     const session = await requireSessionFromRequest();
 
     return await db.query.statuses.findMany({
@@ -27,26 +25,12 @@ export const statusesQueryOptions = () =>
   });
 
 export const useStatusesQuery = () => {
-  const queryData = useSuspenseQuery(statusesQueryOptions());
-
-  useEventSource({
-    topics: [
-      "status-create",
-      "status-delete",
-      ...queryData.data.map((t) => `status-update-${t.id}`),
-    ],
-    callback: () => {
-      queryData.refetch();
-    },
-  });
-
-  return { ...queryData };
+  return useSuspenseQuery(statusesQueryOptions());
 };
 
 export const fetchStatusesWithTaskCounts = createServerFn({
   method: "GET",
 }).handler(async () => {
-  console.info("Fetching statuses with task counts...");
   const session = await requireSessionFromRequest();
 
   const statusesWithCounts = await db
@@ -76,18 +60,5 @@ export const statusesWithCountsQueryOptions = () =>
   });
 
 export const useStatusesWithCountsQuery = () => {
-  const queryData = useSuspenseQuery(statusesWithCountsQueryOptions());
-
-  useEventSource({
-    topics: [
-      "status-create",
-      "status-delete",
-      ...queryData.data.map((t) => `status-update-${t.id}`),
-    ],
-    callback: () => {
-      queryData.refetch();
-    },
-  });
-
-  return { ...queryData };
+  return useSuspenseQuery(statusesWithCountsQueryOptions());
 };
diff --git a/src/db/queries/tasks.ts b/src/db/queries/tasks.ts
index c809daa..b67d8b6 100644
--- a/src/db/queries/tasks.ts
+++ b/src/db/queries/tasks.ts
@@ -4,7 +4,6 @@ import { db } from "~/db";
 import { Project, TaskWithRelations } from "../schema";
 import { requireSessionFromRequest } from "~/lib/session";
 import { getOwningIdentity } from "~/lib/utils";
-import { useEventSource } from "~/hooks/use-event-source";
 
 /** Board cards only need lightweight relations (not full attachments / subtask bodies). */
 export const boardTaskRelationsForProject = {
@@ -135,20 +134,7 @@ export const tasksQueryOptions = (projectId: string) =>
   });
 
 export const useTasksQuery = (projectId: string) => {
-  const queryData = useSuspenseQuery(tasksQueryOptions(projectId));
-
-  useEventSource({
-    topics: [
-      "task-create",
-      "task-delete",
-      ...queryData.data.map((t) => `task-update-${t.id}`),
-    ],
-    callback: () => {
-      queryData.refetch();
-    },
-  });
-
-  return { ...queryData };
+  return useSuspenseQuery(tasksQueryOptions(projectId));
 };
 
 const fetchTasksForSprint = createServerFn({ method: "GET" })
@@ -179,20 +165,7 @@ export const tasksForSprintQueryOptions = (sprintId: string) =>
   });
 
 export const useTasksForSprintQuery = (sprintId: string) => {
-  const queryData = useSuspenseQuery(tasksForSprintQueryOptions(sprintId));
-
-  useEventSource({
-    topics: [
-      "task-create",
-      "task-delete",
-      ...queryData.data.map((t) => `task-update-${t.id}`),
-    ],
-    callback: () => {
-      queryData.refetch();
-    },
-  });
-
-  return { ...queryData };
+  return useSuspenseQuery(tasksForSprintQueryOptions(sprintId));
 };
 
 export const ALL_TASKS_SCOPE = "all" as const;
@@ -222,20 +195,7 @@ export const allTasksQueryOptions = () =>
   });
 
 export const useAllTasksQuery = () => {
-  const queryData = useSuspenseQuery(allTasksQueryOptions());
-
-  useEventSource({
-    topics: [
-      "task-create",
-      "task-delete",
-      ...queryData.data.map((t) => `task-update-${t.id}`),
-    ],
-    callback: () => {
-      queryData.refetch();
-    },
-  });
-
-  return { ...queryData };
+  return useSuspenseQuery(allTasksQueryOptions());
 };
 
 export const fetchTask = createServerFn({ method: "GET" })
@@ -280,14 +240,5 @@ export const taskQueryOptions = (id: string) =>
   });
 
 export const useTaskQuery = (taskId: string) => {
-  const queryData = useSuspenseQuery(taskQueryOptions(taskId));
-
-  useEventSource({
-    topics: [`task-update-${taskId}`],
-    callback: () => {
-      queryData.refetch();
-    },
-  });
-
-  return { ...queryData };
+  return useSuspenseQuery(taskQueryOptions(taskId));
 };
diff --git a/src/hooks/live-sync-provider.tsx b/src/hooks/live-sync-provider.tsx
new file mode 100644
index 0000000..d999d0c
--- /dev/null
+++ b/src/hooks/live-sync-provider.tsx
@@ -0,0 +1,200 @@
+import type { QueryClient } from "@tanstack/react-query";
+import { useQueryClient } from "@tanstack/react-query";
+import { useEffect, useEffectEvent, type ReactNode } from "react";
+import { authClient } from "~/lib/auth-client";
+import { invalidateGitTaskQueries } from "~/hooks/invalidate-git-task-queries";
+import { parseTaskGitSyncPayload } from "~/lib/git/sync-task-update";
+import { ownerDashboardTopic } from "~/lib/owner-dashboard-topic";
+import { userNotificationsTopic } from "~/lib/user-notifications-topic";
+import { getOwningIdentity } from "~/lib/utils";
+
+const SYNC_APP_ID = "676bb0d1-942d-465a-a706-4ee451177507";
+const SYNC_STREAM_BASE = "https://sync-connect.pno.dev";
+
+const COARSE_TOPICS = [
+  "task-create",
+  "task-delete",
+  "task-update",
+  "project-create",
+  "project-delete",
+  "project-update",
+  "sprint-create",
+  "sprint-delete",
+  "sprint-update",
+  "status-create",
+  "status-delete",
+  "status-update",
+  "label-create",
+  "label-delete",
+  "label-update",
+  "comment-create",
+  "comment-update",
+  "attachment-create",
+  "attachment-delete",
+  "attachment-update",
+] as const;
+
+function parseEventPayload(data: unknown): {
+  topic?: string;
+  entityId?: string;
+} {
+  if (typeof data !== "string" || !data) return {};
+  try {
+    const parsed = JSON.parse(data) as { topic?: string; entityId?: string };
+    return {
+      topic: typeof parsed.topic === "string" ? parsed.topic : undefined,
+      entityId:
+        typeof parsed.entityId === "string" ? parsed.entityId : undefined,
+    };
+  } catch {
+    return {};
+  }
+}
+
+function invalidateTaskQueries(queryClient: QueryClient, taskId?: string) {
+  if (taskId) {
+    void queryClient.invalidateQueries({ queryKey: ["tasks", taskId] });
+  }
+  void queryClient.invalidateQueries({ queryKey: ["tasks"] });
+}
+
+function handleCoarseSyncEvent(
+  queryClient: QueryClient,
+  topic: string,
+  data: unknown,
+  entityId: string | undefined,
+  ownerTopic: string | null,
+  notificationsTopic: string | null
+) {
+  if (notificationsTopic && topic === notificationsTopic) {
+    void queryClient.invalidateQueries({ queryKey: ["in-app-notifications"] });
+    return;
+  }
+
+  if (ownerTopic && topic === ownerTopic) {
+    void queryClient.invalidateQueries({ queryKey: ["dashboard-stats"] });
+    return;
+  }
+
+  switch (topic) {
+    case "task-create":
+    case "task-delete":
+      invalidateTaskQueries(queryClient);
+      if (ownerTopic) {
+        void queryClient.invalidateQueries({ queryKey: ["dashboard-stats"] });
+      }
+      return;
+    case "task-update": {
+      const gitPayload = parseTaskGitSyncPayload(data);
+      const taskId = gitPayload?.id ?? entityId;
+      if (taskId) {
+        void invalidateGitTaskQueries(
+          queryClient,
+          taskId,
+          gitPayload?.gitInvalidate,
+          gitPayload?.pullRequestId
+        );
+      }
+      invalidateTaskQueries(queryClient, taskId);
+      if (ownerTopic) {
+        void queryClient.invalidateQueries({ queryKey: ["dashboard-stats"] });
+      }
+      return;
+    }
+    case "project-create":
+    case "project-delete":
+    case "project-update":
+      void queryClient.invalidateQueries({ queryKey: ["projects"] });
+      return;
+    case "sprint-create":
+    case "sprint-delete":
+    case "sprint-update":
+      void queryClient.invalidateQueries({ queryKey: ["sprints"] });
+      return;
+    case "status-create":
+    case "status-delete":
+    case "status-update":
+      void queryClient.invalidateQueries({ queryKey: ["statuses"] });
+      void queryClient.invalidateQueries({ queryKey: ["statuses-with-counts"] });
+      return;
+    case "label-create":
+    case "label-delete":
+    case "label-update":
+      void queryClient.invalidateQueries({ queryKey: ["labels"] });
+      void queryClient.invalidateQueries({ queryKey: ["labels-with-counts"] });
+      return;
+    case "comment-create":
+    case "comment-update":
+      void queryClient.invalidateQueries({ queryKey: ["comments"] });
+      return;
+    case "attachment-create":
+    case "attachment-delete":
+    case "attachment-update":
+      void queryClient.invalidateQueries({ queryKey: ["attachments"] });
+      return;
+    default:
+      return;
+  }
+}
+
+export function LiveSyncProvider({ children }: { children: ReactNode }) {
+  const queryClient = useQueryClient();
+  const { data: session } = authClient.useSession();
+  const ownerTopic = session
+    ? ownerDashboardTopic(getOwningIdentity(session))
+    : null;
+  const notificationsTopic = session?.user.id
+    ? userNotificationsTopic(session.user.id)
+    : null;
+
+  const onUpdate = useEffectEvent((data: unknown) => {
+    const { topic, entityId } = parseEventPayload(data);
+    if (!topic) return;
+
+    if (notificationsTopic && topic === notificationsTopic) {
+      void queryClient.invalidateQueries({ queryKey: ["in-app-notifications"] });
+      return;
+    }
+
+    if (ownerTopic && topic === ownerTopic) {
+      void queryClient.invalidateQueries({ queryKey: ["dashboard-stats"] });
+      return;
+    }
+
+    handleCoarseSyncEvent(
+      queryClient,
+      topic,
+      data,
+      entityId,
+      ownerTopic,
+      notificationsTopic
+    );
+  });
+
+  useEffect(() => {
+    const topics = [
+      ...COARSE_TOPICS,
+      ...(ownerTopic ? [ownerTopic] : []),
+      ...(notificationsTopic ? [notificationsTopic] : []),
+    ];
+    let topicsString = "?";
+    for (const topic of topics) {
+      if (topicsString !== "?") topicsString += "&";
+      topicsString += `topic[]=${topic}`;
+    }
+
+    const sse = new EventSource(
+      `${SYNC_STREAM_BASE}/stream/${SYNC_APP_ID}${topicsString}`
+    );
+
+    sse.addEventListener("update", (event) => {
+      onUpdate(event.data);
+    });
+
+    return () => {
+      sse.close();
+    };
+  }, [ownerTopic, notificationsTopic]);
+
+  return children;
+}
diff --git a/src/hooks/use-event-source.ts b/src/hooks/use-event-source.ts
deleted file mode 100644
index f24f583..0000000
--- a/src/hooks/use-event-source.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import { useEffect, useEffectEvent } from "react";
-
-export function useEventSource({
-  topics,
-  callback,
-}: {
-  topics: string[];
-  callback: (data: unknown) => void;
-}) {
-  const topicsKey = [...topics].sort().join("\0");
-
-  const onUpdate = useEffectEvent((data: unknown) => {
-    callback(data);
-  });
-
-  useEffect(() => {
-    const sortedTopics = topicsKey ? topicsKey.split("\0") : [];
-    const appId = "676bb0d1-942d-465a-a706-4ee451177507";
-    let topicsString = "?";
-    sortedTopics.forEach((topic) => {
-      if (topicsString !== "?") topicsString += "&";
-      topicsString += `topic[]=${topic}`;
-    });
-    const sse = new EventSource(
-      `https://sync-connect.pno.dev/stream/${appId}${topicsString}`,
-    );
-    sse.addEventListener("update", (event) => {
-      onUpdate(event.data);
-    });
-
-    return () => {
-      sse.close();
-    };
-  }, [topicsKey]);
-}
diff --git a/src/hooks/use-git-task-live-sync.ts b/src/hooks/use-git-task-live-sync.ts
index 9f74374..57c6996 100644
--- a/src/hooks/use-git-task-live-sync.ts
+++ b/src/hooks/use-git-task-live-sync.ts
@@ -1,23 +1,2 @@
-import { useQueryClient } from "@tanstack/react-query";
-import { useEventSource } from "~/hooks/use-event-source";
-import { invalidateGitTaskQueries } from "~/hooks/invalidate-git-task-queries";
-import { parseTaskGitSyncPayload } from "~/lib/git/sync-task-update";
-
-/** Refetch git queries when webhooks or mutations sync this task. */
-export function useGitTaskLiveSync(taskId: string | undefined) {
-  const queryClient = useQueryClient();
-
-  useEventSource({
-    topics: taskId ? [`task-update-${taskId}`] : [],
-    callback: (data) => {
-      if (!taskId) return;
-      const payload = parseTaskGitSyncPayload(data);
-      void invalidateGitTaskQueries(
-        queryClient,
-        taskId,
-        payload?.gitInvalidate,
-        payload?.pullRequestId
-      );
-    },
-  });
-}
+/** Git task live sync is handled by LiveSyncProvider via coarse task-update events. */
+export function useGitTaskLiveSync(_taskId: string | undefined) {}
diff --git a/src/lib/query-cache-fresh.ts b/src/lib/query-cache-fresh.ts
new file mode 100644
index 0000000..1dd6fe5
--- /dev/null
+++ b/src/lib/query-cache-fresh.ts
@@ -0,0 +1,15 @@
+import type { QueryClient, QueryKey } from "@tanstack/react-query";
+
+const DEFAULT_STALE_TIME_MS = 60_000;
+
+export function isQueryCacheFresh(
+  queryClient: QueryClient,
+  queryKey: QueryKey,
+  staleTimeMs = DEFAULT_STALE_TIME_MS
+): boolean {
+  const state = queryClient.getQueryState(queryKey);
+  if (state?.data === undefined || state.dataUpdatedAt === undefined) {
+    return false;
+  }
+  return Date.now() - state.dataUpdatedAt < staleTimeMs;
+}
diff --git a/src/router.tsx b/src/router.tsx
index deab56c..fc333df 100644
--- a/src/router.tsx
+++ b/src/router.tsx
@@ -20,6 +20,9 @@ export function getRouter() {
     routeTree,
     context: { queryClient },
     defaultPreload: "intent",
+    defaultStaleTime: 60_000,
+    defaultGcTime: 5 * 60_000,
+    defaultStaleReloadMode: "background",
     defaultErrorComponent: DefaultCatchBoundary,
     defaultNotFoundComponent: () => <NotFound />,
     defaultViewTransition: true,
diff --git a/src/routes/_signed-in.tsx b/src/routes/_signed-in.tsx
index 72ccf80..149e44e 100644
--- a/src/routes/_signed-in.tsx
+++ b/src/routes/_signed-in.tsx
@@ -13,7 +13,12 @@ import {
 } from "~/db/queries/hydrate-query-cache";
 import { useRouterState } from "@tanstack/react-router";
 import { TopLoadingState } from "~/components/TopLoadingState";
+import { LiveSyncProvider } from "~/hooks/live-sync-provider";
 import { ensureSignedInBeforeLoad } from "~/lib/signed-in-before-load";
+import { isQueryCacheFresh } from "~/lib/query-cache-fresh";
+import { userPreferencesQueryOptions } from "~/db/queries/user-preferences";
+import { projectsQueryOptions } from "~/db/queries/projects";
+import { sprintsQueryOptions } from "~/db/queries/sprints";
 
 export const Route = createFileRoute("/_signed-in")({
   validateSearch: signedInSearchSchema,
@@ -21,12 +26,25 @@ export const Route = createFileRoute("/_signed-in")({
     await ensureSignedInBeforeLoad({ data: location.href });
   },
   loader: async ({ context }) => {
+    const needsSidebar =
+      !isQueryCacheFresh(context.queryClient, projectsQueryOptions().queryKey) ||
+      !isQueryCacheFresh(context.queryClient, sprintsQueryOptions().queryKey);
+    const needsPreferences = !isQueryCacheFresh(
+      context.queryClient,
+      userPreferencesQueryOptions().queryKey
+    );
+
     const [bundle, preferences] = await Promise.all([
-      fetchSidebarBundle(),
-      fetchUserPreferences(),
+      needsSidebar ? fetchSidebarBundle() : Promise.resolve(null),
+      needsPreferences ? fetchUserPreferences() : Promise.resolve(null),
     ]);
-    hydrateSidebarCache(context.queryClient, bundle);
-    hydrateUserPreferencesCache(context.queryClient, preferences);
+
+    if (bundle) {
+      hydrateSidebarCache(context.queryClient, bundle);
+    }
+    if (preferences) {
+      hydrateUserPreferencesCache(context.queryClient, preferences);
+    }
   },
   component: PathlessLayoutComponent,
 });
@@ -36,9 +54,10 @@ function PathlessLayoutComponent() {
   const routerState = useRouterState();
 
   return (
-    <div className="[--header-height:calc(--spacing(14))]">
-      <TopLoadingState isActive={routerState.isLoading} />
-      <SidebarProvider className="flex min-h-svh overflow-x-hidden">
+    <LiveSyncProvider>
+      <div className="[--header-height:calc(--spacing(14))]">
+        <TopLoadingState isActive={routerState.isLoading} />
+        <SidebarProvider className="flex min-h-svh overflow-x-hidden">
         <div className="flex min-w-0 flex-1">
           <AppSidebar />
           <div className="flex min-w-0 flex-1 flex-col">
@@ -49,7 +68,8 @@ function PathlessLayoutComponent() {
             </SidebarInset>
           </div>
         </div>
-      </SidebarProvider>
-    </div>
+        </SidebarProvider>
+      </div>
+    </LiveSyncProvider>
   );
 }
diff --git a/src/routes/_signed-in/dashboard.tsx b/src/routes/_signed-in/dashboard.tsx
index db7472a..84721b9 100644
--- a/src/routes/_signed-in/dashboard.tsx
+++ b/src/routes/_signed-in/dashboard.tsx
@@ -15,6 +15,8 @@ import {
   useDashboardStatsQuery,
 } from "~/db/queries/dashboard";
 import { hydrateDashboardCache } from "~/db/queries/hydrate-query-cache";
+import { isQueryCacheFresh } from "~/lib/query-cache-fresh";
+import { dashboardStatsQueryOptions } from "~/db/queries/dashboard";
 import { useProjectsQuery } from "~/db/queries/projects";
 import { useSprintsQuery } from "~/db/queries/sprints";
 import { authClient } from "~/lib/auth-client";
@@ -24,8 +26,12 @@ import { pageMeta } from "~/utils/seo";
 
 export const Route = createFileRoute("/_signed-in/dashboard")({
   loader: async ({ context }) => {
-    const stats = await fetchDashboardStats();
-    hydrateDashboardCache(context.queryClient, stats);
+    if (
+      !isQueryCacheFresh(context.queryClient, dashboardStatsQueryOptions().queryKey)
+    ) {
+      const stats = await fetchDashboardStats();
+      hydrateDashboardCache(context.queryClient, stats);
+    }
   },
   head: () => ({
     meta: [...pageMeta("Dashboard")],
diff --git a/src/routes/_signed-in/projects.$projectId.tasks.tsx b/src/routes/_signed-in/projects.$projectId.tasks.tsx
index bd923c4..e4dc972 100644
--- a/src/routes/_signed-in/projects.$projectId.tasks.tsx
+++ b/src/routes/_signed-in/projects.$projectId.tasks.tsx
@@ -21,6 +21,9 @@ import { EndlessLoadingSpinner } from "~/components/EndlessLoadingSpinner";
 import { useTasksQuery } from "~/db/queries/tasks";
 import { fetchProjectBoardBundle } from "~/db/queries/bundles";
 import { hydrateProjectBoardCache } from "~/db/queries/hydrate-query-cache";
+import { isQueryCacheFresh } from "~/lib/query-cache-fresh";
+import { tasksQueryOptions } from "~/db/queries/tasks";
+import { projectQueryOptions } from "~/db/queries/projects";
 import { Button } from "~/components/ui/button";
 import { TaskViewSortControls } from "~/components/views/TaskViewSortControls";
 import { TaskViewSwitcher } from "~/components/views/TaskViewSwitcher";
@@ -31,9 +34,19 @@ import { pageMeta } from "~/utils/seo";
 export const Route = createFileRoute("/_signed-in/projects/$projectId/tasks")({
   loader: async ({ context, params }) => {
     const { projectId } = params;
-    const bundle = await fetchProjectBoardBundle({ data: { projectId } });
-    hydrateProjectBoardCache(context.queryClient, projectId, bundle);
-    const project = bundle.project;
+    const needsBundle = !isQueryCacheFresh(
+      context.queryClient,
+      tasksQueryOptions(projectId).queryKey
+    );
+
+    if (needsBundle) {
+      const bundle = await fetchProjectBoardBundle({ data: { projectId } });
+      hydrateProjectBoardCache(context.queryClient, projectId, bundle);
+    }
+
+    const project = context.queryClient.getQueryData(
+      projectQueryOptions(projectId).queryKey
+    );
     return {
       pageTitle: project ? `${project.name} - Tasks` : "Tasks",
     };
diff --git a/src/routes/_signed-in/sprints.$sprintId.tasks.tsx b/src/routes/_signed-in/sprints.$sprintId.tasks.tsx
index 1cdb5e9..631f19e 100644
--- a/src/routes/_signed-in/sprints.$sprintId.tasks.tsx
+++ b/src/routes/_signed-in/sprints.$sprintId.tasks.tsx
@@ -22,6 +22,9 @@ import { useLabelsQuery } from "~/db/queries/labels";
 import { useTasksForSprintQuery } from "~/db/queries/tasks";
 import { fetchSprintBoardBundle } from "~/db/queries/bundles";
 import { hydrateSprintBoardCache } from "~/db/queries/hydrate-query-cache";
+import { isQueryCacheFresh } from "~/lib/query-cache-fresh";
+import { tasksForSprintQueryOptions } from "~/db/queries/tasks";
+import { sprintQueryOptions } from "~/db/queries/sprints";
 import { UpdateTask } from "~/db/schema";
 import { sprintEditSheetSearch } from "~/lib/form-sheet-search";
 import { pageMeta } from "~/utils/seo";
@@ -29,9 +32,19 @@ import { pageMeta } from "~/utils/seo";
 export const Route = createFileRoute("/_signed-in/sprints/$sprintId/tasks")({
   loader: async ({ context, params }) => {
     const { sprintId } = params;
-    const bundle = await fetchSprintBoardBundle({ data: { sprintId } });
-    hydrateSprintBoardCache(context.queryClient, sprintId, bundle);
-    const sprint = bundle.sprint;
+    const needsBundle = !isQueryCacheFresh(
+      context.queryClient,
+      tasksForSprintQueryOptions(sprintId).queryKey
+    );
+
+    if (needsBundle) {
+      const bundle = await fetchSprintBoardBundle({ data: { sprintId } });
+      hydrateSprintBoardCache(context.queryClient, sprintId, bundle);
+    }
+
+    const sprint = context.queryClient.getQueryData(
+      sprintQueryOptions(sprintId).queryKey
+    );
     return {
       pageTitle: sprint ? `${sprint.name} - Tasks` : "Tasks",
     };
diff --git a/src/routes/_signed-in/tasks.tsx b/src/routes/_signed-in/tasks.tsx
index 8bd2ee4..854d127 100644
--- a/src/routes/_signed-in/tasks.tsx
+++ b/src/routes/_signed-in/tasks.tsx
@@ -14,6 +14,8 @@ import { EndlessLoadingSpinner } from "~/components/EndlessLoadingSpinner";
 import { useAllTasksQuery } from "~/db/queries/tasks";
 import { fetchAllTasksBoardBundle } from "~/db/queries/bundles";
 import { hydrateAllTasksBoardCache } from "~/db/queries/hydrate-query-cache";
+import { isQueryCacheFresh } from "~/lib/query-cache-fresh";
+import { allTasksQueryOptions } from "~/db/queries/tasks";
 import { TaskViewSortControls } from "~/components/views/TaskViewSortControls";
 import { TaskViewSwitcher } from "~/components/views/TaskViewSwitcher";
 import { TaskViewsContainer } from "~/components/views/TaskViewsContainer";
@@ -21,8 +23,10 @@ import { pageMeta } from "~/utils/seo";
 
 export const Route = createFileRoute("/_signed-in/tasks")({
   loader: async ({ context }) => {
-    const bundle = await fetchAllTasksBoardBundle();
-    hydrateAllTasksBoardCache(context.queryClient, bundle);
+    if (!isQueryCacheFresh(context.queryClient, allTasksQueryOptions().queryKey)) {
+      const bundle = await fetchAllTasksBoardBundle();
+      hydrateAllTasksBoardCache(context.queryClient, bundle);
+    }
   },
   head: () => ({
     meta: [...pageMeta("All Projects - Tasks")],
diff --git a/vite.config.ts b/vite.config.ts
index e01196b..f594ecf 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -7,6 +7,7 @@ import netlify from "@netlify/vite-plugin-tanstack-start";
 import { tanstackStart } from "@tanstack/react-start/plugin/vite";
 import viteReact from "@vitejs/plugin-react";
 import tailwindcss from "@tailwindcss/vite";
+import { visualizer } from "rollup-plugin-visualizer";
 
 const packageJson = JSON.parse(
   readFileSync(new URL("./package.json", import.meta.url), "utf8"),
@@ -45,6 +46,13 @@ export default defineConfig(({ mode }) => {
       tanstackStart(),
       netlify(),
       viteReact(),
+      process.env.ANALYZE === "true"
+        ? visualizer({
+            filename: "dist/bundle-stats.html",
+            gzipSize: true,
+            open: false,
+          })
+        : undefined,
     ],
     define: {
       "import.meta.env.VITE_APP_VERSION": JSON.stringify(appVersion),

From 320d3dc6c5ee8a798b3d9e4e350b430180eae6cc Mon Sep 17 00:00:00 2001
From: Philipp Nowinski <mail@pno.dev>
Date: Thu, 11 Jun 2026 08:41:33 +0200
Subject: [PATCH 2/5] fix: address coderabbit findings

---
 src/components/views/BoardView.tsx            |  1 +
 src/components/views/ListView.tsx             |  2 +-
 src/components/views/TaskViewsContainer.tsx   | 32 +++++-
 src/components/views/VirtualizedTaskCards.tsx | 10 +-
 src/db/mutations/sync.ts                      | 24 ++---
 src/db/mutations/tasks.ts                     | 99 +++++++++++++++----
 src/hooks/live-sync-provider.tsx              | 61 ++++--------
 src/lib/query-cache-fresh.ts                  |  3 +-
 src/router.tsx                                | 15 ++-
 src/routes/_signed-in.tsx                     |  7 +-
 10 files changed, 160 insertions(+), 94 deletions(-)

diff --git a/src/components/views/BoardView.tsx b/src/components/views/BoardView.tsx
index cefe0cf..9301691 100644
--- a/src/components/views/BoardView.tsx
+++ b/src/components/views/BoardView.tsx
@@ -177,6 +177,7 @@ export const BoardView = ({
           <TaskCardComponent
             task={activeTask}
             users={users}
+            assigningTaskId={assigningTaskId}
             showSprint={showSprint}
             showProject={showProject}
             taskLinkTo={taskLinkTo}
diff --git a/src/components/views/ListView.tsx b/src/components/views/ListView.tsx
index 2d06981..256b7a9 100644
--- a/src/components/views/ListView.tsx
+++ b/src/components/views/ListView.tsx
@@ -263,7 +263,7 @@ function ListSection({
 
       {sortedTasks.length > 0 ? (
         <div>
-          {sortedTasks.length > 25 ? (
+          {!manualSortEnabled && sortedTasks.length > 25 ? (
             <VirtualizedTaskListRows
               items={sortedTasks}
               renderItem={(task) => (
diff --git a/src/components/views/TaskViewsContainer.tsx b/src/components/views/TaskViewsContainer.tsx
index deed08e..94f1aba 100644
--- a/src/components/views/TaskViewsContainer.tsx
+++ b/src/components/views/TaskViewsContainer.tsx
@@ -89,7 +89,27 @@ export function TaskViewsContainer(props: TaskViewsContainerProps) {
 
   useEffect(() => {
     const handleKeyPress = async (event: KeyboardEvent) => {
-      if (!currentUserId || !hoveredTaskId || event.key !== "m") return;
+      if (
+        !currentUserId ||
+        !hoveredTaskId ||
+        event.key !== "m" ||
+        event.repeat ||
+        assigningTaskId != null
+      ) {
+        return;
+      }
+
+      const target = event.target;
+      if (target instanceof HTMLElement) {
+        if (
+          target instanceof HTMLInputElement ||
+          target instanceof HTMLTextAreaElement ||
+          target.isContentEditable ||
+          target.closest('[contenteditable="true"]')
+        ) {
+          return;
+        }
+      }
 
       const task = props.tasks.find((entry) => entry.id === hoveredTaskId);
       if (!task) return;
@@ -110,7 +130,15 @@ export function TaskViewsContainer(props: TaskViewsContainerProps) {
     return () => {
       window.removeEventListener("keydown", handleKeyPress);
     };
-  }, [assignTask, currentUserId, hoveredTaskId, props.tasks, unassignTask]);
+  }, [
+    assignTask,
+    assigningTaskId,
+    currentUserId,
+    hoveredTaskId,
+    props.tasks,
+    setAssigningTaskId,
+    unassignTask,
+  ]);
 
   const ViewComponent = TASK_VIEW_REGISTRY[viewMode];
 
diff --git a/src/components/views/VirtualizedTaskCards.tsx b/src/components/views/VirtualizedTaskCards.tsx
index 5a7ab90..e17f7c3 100644
--- a/src/components/views/VirtualizedTaskCards.tsx
+++ b/src/components/views/VirtualizedTaskCards.tsx
@@ -1,5 +1,5 @@
 import { useVirtualizer } from "@tanstack/react-virtual";
-import { useEffect, useRef, type ReactNode } from "react";
+import { useCallback, useRef, type ReactNode } from "react";
 
 type VirtualizedTaskCardsProps<T> = {
   items: T[];
@@ -12,12 +12,10 @@ export function VirtualizedTaskCards<T>({
   estimateSize = 132,
   renderItem,
 }: VirtualizedTaskCardsProps<T>) {
-  const listRef = useRef<HTMLDivElement>(null);
   const scrollElementRef = useRef<HTMLElement | null>(null);
-
-  useEffect(() => {
-    scrollElementRef.current = listRef.current?.parentElement ?? null;
-  }, [items.length]);
+  const listRef = useCallback((node: HTMLDivElement | null) => {
+    scrollElementRef.current = node?.parentElement ?? null;
+  }, []);
 
   const virtualizer = useVirtualizer({
     count: items.length,
diff --git a/src/db/mutations/sync.ts b/src/db/mutations/sync.ts
index bb2c3f9..1539805 100644
--- a/src/db/mutations/sync.ts
+++ b/src/db/mutations/sync.ts
@@ -2,6 +2,7 @@ import "@tanstack/react-start/server-only";
 
 import { env } from "~/env";
 import { ownerDashboardTopic } from "~/lib/owner-dashboard-topic";
+import { createSyncEnvelope, type SyncEnvelope } from "~/lib/sync-envelope";
 
 const DEFAULT_SYNC_ENGINE_URL = "https://sync-connect.pno.dev";
 
@@ -35,26 +36,22 @@ const COARSE_TOPIC_PREFIXES: Record<string, string> = {
   "attachment-update-": "attachment-update",
 };
 
-async function publishSyncTopic(
-  baseUrl: string,
-  topic: string,
-  payload: unknown
-) {
+async function publishSyncTopic(baseUrl: string, envelope: SyncEnvelope) {
   const response = await fetch(
-    `${baseUrl}/stream/${env.SYNC_APP_ID}?key=${env.SYNC_PUBLISH_KEY}&topic=${topic}`,
+    `${baseUrl}/stream/${env.SYNC_APP_ID}?key=${env.SYNC_PUBLISH_KEY}&topic=${envelope.topic}`,
     {
       method: "POST",
       headers: {
         "Content-Type": "application/json",
       },
-      body: JSON.stringify({ topic, payload }),
+      body: JSON.stringify(envelope),
     }
   );
 
   if (!response.ok) {
     const body = await response.text();
     console.error(
-      `Sync failed for topic "${topic}" (app ${env.SYNC_APP_ID}, ${baseUrl}): ${response.status} ${body}`
+      `Sync failed for topic "${envelope.topic}" (app ${env.SYNC_APP_ID}, ${baseUrl}): ${response.status} ${body}`
     );
   }
 }
@@ -62,16 +59,15 @@ async function publishSyncTopic(
 export async function sync(topic: string, payload: unknown) {
   const baseUrl = resolveSyncEngineUrl();
   try {
-    await publishSyncTopic(baseUrl, topic, payload);
+    await publishSyncTopic(baseUrl, createSyncEnvelope(topic, payload));
 
     for (const [prefix, coarseTopic] of Object.entries(COARSE_TOPIC_PREFIXES)) {
       if (!topic.startsWith(prefix)) continue;
       const entityId = topic.slice(prefix.length);
-      await publishSyncTopic(baseUrl, coarseTopic, {
-        topic: coarseTopic,
-        entityId,
-        payload,
-      });
+      await publishSyncTopic(
+        baseUrl,
+        createSyncEnvelope(coarseTopic, payload, entityId)
+      );
       break;
     }
   } catch (error) {
diff --git a/src/db/mutations/tasks.ts b/src/db/mutations/tasks.ts
index 97286ff..cde9b53 100644
--- a/src/db/mutations/tasks.ts
+++ b/src/db/mutations/tasks.ts
@@ -36,6 +36,27 @@ import {
 
 const allTasksListQueryKey: QueryKey = ["tasks", ALL_TASKS_SCOPE];
 
+function resolveLabels(labelIds: string[], labels: Label[]): Label[] {
+  return labelIds
+    .map((id) => labels.find((label) => label.id === id))
+    .filter((label): label is Label => label != null);
+}
+
+function computeBucketSortOrder(
+  tasks: TaskWithRelations[],
+  taskId: string,
+  statusId: string | null,
+  targetIndex: number
+): number | undefined {
+  const bucketTaskIds = tasks
+    .filter(
+      (task) => (task.statusId ?? null) === statusId && task.id !== taskId
+    )
+    .map((task) => task.id);
+  bucketTaskIds.splice(Math.min(targetIndex, bucketTaskIds.length), 0, taskId);
+  return sortOrdersFromOrderedIds(bucketTaskIds).get(taskId);
+}
+
 function maybeIncludeAllTasksListKey(
   queryClient: QueryClient,
   keys: QueryKey[]
@@ -628,37 +649,53 @@ export function useSetLabelsForTaskMutation() {
   return useCallback(
     async (task: Task, labelIds: string[]) => {
       const labels: Label[] = queryClient.getQueryData(["labels"]) || [];
-
-      queryClient.setQueryData(
-        ["tasks", task.id],
-        (oldData: TaskWithRelations | undefined) => {
-          if (!oldData) return oldData;
-          return {
-            ...oldData,
-            labels: [...labelIds.map((id) => labels.find((l) => l.id === id))],
-          };
-        }
-      );
-
-      await _setLabelsForTask({ data: { taskId: task.id, labelIds } });
-
+      const resolvedLabels = resolveLabels(labelIds, labels);
       const listQueryKeys = getTaskListQueryKeys(queryClient, task);
+      const detailQueryKey: QueryKey = ["tasks", task.id];
+      const snapshots = [
+        ...listQueryKeys.map((queryKey) => ({
+          queryKey,
+          data: queryClient.getQueryData(queryKey),
+        })),
+        {
+          queryKey: detailQueryKey,
+          data: queryClient.getQueryData(detailQueryKey),
+        },
+      ];
+
       const patchList = (oldData: TaskWithRelations[] | undefined) => {
         if (!oldData) return oldData;
         return oldData.map((entry) =>
           entry.id === task.id
-            ? {
-                ...entry,
-                labels: labelIds.map(
-                  (id) => labels.find((label) => label.id === id)!
-                ),
-              }
+            ? { ...entry, labels: resolvedLabels, updatedAt: new Date() }
             : entry
         );
       };
+
       for (const queryKey of listQueryKeys) {
         queryClient.setQueryData(queryKey, patchList);
       }
+      queryClient.setQueryData(
+        detailQueryKey,
+        (oldData: TaskWithRelations | undefined) => {
+          if (!oldData) return oldData;
+          return {
+            ...oldData,
+            labels: resolvedLabels,
+            updatedAt: new Date(),
+          };
+        }
+      );
+
+      try {
+        await _setLabelsForTask({ data: { taskId: task.id, labelIds } });
+      } catch (error) {
+        for (const { queryKey, data } of snapshots) {
+          queryClient.setQueryData(queryKey, data);
+        }
+        toast.error("Failed to update labels");
+        throw error;
+      }
     },
     [queryClient, _setLabelsForTask]
   );
@@ -1325,9 +1362,16 @@ export function useMoveTaskInBucketMutation() {
         const taskIndex = oldData.findIndex((task) => task.id === input.taskId);
         if (taskIndex === -1) return oldData;
 
+        const sortOrder = computeBucketSortOrder(
+          oldData,
+          input.taskId,
+          input.statusId,
+          input.targetIndex
+        );
         const movedTask = {
           ...oldData[taskIndex]!,
           statusId: input.statusId,
+          ...(sortOrder != null ? { sortOrder } : {}),
           updatedAt: new Date(),
         };
         const withoutTask = oldData.filter((task) => task.id !== input.taskId);
@@ -1340,6 +1384,20 @@ export function useMoveTaskInBucketMutation() {
         return next;
       };
 
+      const listData = listQueryKeys
+        .map((queryKey) =>
+          queryClient.getQueryData<TaskWithRelations[]>(queryKey)
+        )
+        .find((data) => data != null);
+      const detailSortOrder = listData
+        ? computeBucketSortOrder(
+            listData,
+            input.taskId,
+            input.statusId,
+            input.targetIndex
+          )
+        : undefined;
+
       for (const queryKey of listQueryKeys) {
         queryClient.setQueryData(queryKey, patchMovedTask);
       }
@@ -1350,6 +1408,7 @@ export function useMoveTaskInBucketMutation() {
           return {
             ...oldData,
             statusId: input.statusId,
+            ...(detailSortOrder != null ? { sortOrder: detailSortOrder } : {}),
             updatedAt: new Date(),
           };
         }
diff --git a/src/hooks/live-sync-provider.tsx b/src/hooks/live-sync-provider.tsx
index d999d0c..fd36f77 100644
--- a/src/hooks/live-sync-provider.tsx
+++ b/src/hooks/live-sync-provider.tsx
@@ -5,6 +5,7 @@ import { authClient } from "~/lib/auth-client";
 import { invalidateGitTaskQueries } from "~/hooks/invalidate-git-task-queries";
 import { parseTaskGitSyncPayload } from "~/lib/git/sync-task-update";
 import { ownerDashboardTopic } from "~/lib/owner-dashboard-topic";
+import { parseSyncEnvelope } from "~/lib/sync-envelope";
 import { userNotificationsTopic } from "~/lib/user-notifications-topic";
 import { getOwningIdentity } from "~/lib/utils";
 
@@ -34,23 +35,6 @@ const COARSE_TOPICS = [
   "attachment-update",
 ] as const;
 
-function parseEventPayload(data: unknown): {
-  topic?: string;
-  entityId?: string;
-} {
-  if (typeof data !== "string" || !data) return {};
-  try {
-    const parsed = JSON.parse(data) as { topic?: string; entityId?: string };
-    return {
-      topic: typeof parsed.topic === "string" ? parsed.topic : undefined,
-      entityId:
-        typeof parsed.entityId === "string" ? parsed.entityId : undefined,
-    };
-  } catch {
-    return {};
-  }
-}
-
 function invalidateTaskQueries(queryClient: QueryClient, taskId?: string) {
   if (taskId) {
     void queryClient.invalidateQueries({ queryKey: ["tasks", taskId] });
@@ -63,19 +47,8 @@ function handleCoarseSyncEvent(
   topic: string,
   data: unknown,
   entityId: string | undefined,
-  ownerTopic: string | null,
-  notificationsTopic: string | null
+  ownerTopic: string | null
 ) {
-  if (notificationsTopic && topic === notificationsTopic) {
-    void queryClient.invalidateQueries({ queryKey: ["in-app-notifications"] });
-    return;
-  }
-
-  if (ownerTopic && topic === ownerTopic) {
-    void queryClient.invalidateQueries({ queryKey: ["dashboard-stats"] });
-    return;
-  }
-
   switch (topic) {
     case "task-create":
     case "task-delete":
@@ -148,26 +121,25 @@ export function LiveSyncProvider({ children }: { children: ReactNode }) {
     : null;
 
   const onUpdate = useEffectEvent((data: unknown) => {
-    const { topic, entityId } = parseEventPayload(data);
-    if (!topic) return;
+    const envelope = parseSyncEnvelope(data);
+    if (!envelope) return;
 
-    if (notificationsTopic && topic === notificationsTopic) {
+    if (notificationsTopic && envelope.topic === notificationsTopic) {
       void queryClient.invalidateQueries({ queryKey: ["in-app-notifications"] });
       return;
     }
 
-    if (ownerTopic && topic === ownerTopic) {
+    if (ownerTopic && envelope.topic === ownerTopic) {
       void queryClient.invalidateQueries({ queryKey: ["dashboard-stats"] });
       return;
     }
 
     handleCoarseSyncEvent(
       queryClient,
-      topic,
+      envelope.topic,
       data,
-      entityId,
-      ownerTopic,
-      notificationsTopic
+      envelope.entityId,
+      ownerTopic
     );
   });
 
@@ -177,16 +149,23 @@ export function LiveSyncProvider({ children }: { children: ReactNode }) {
       ...(ownerTopic ? [ownerTopic] : []),
       ...(notificationsTopic ? [notificationsTopic] : []),
     ];
-    let topicsString = "?";
+    const params = new URLSearchParams();
     for (const topic of topics) {
-      if (topicsString !== "?") topicsString += "&";
-      topicsString += `topic[]=${topic}`;
+      params.append("topic[]", topic);
     }
 
     const sse = new EventSource(
-      `${SYNC_STREAM_BASE}/stream/${SYNC_APP_ID}${topicsString}`
+      `${SYNC_STREAM_BASE}/stream/${SYNC_APP_ID}?${params.toString()}`
     );
 
+    sse.addEventListener("open", () => {
+      console.info("[live-sync] EventSource connected");
+    });
+
+    sse.addEventListener("error", (event) => {
+      console.error("[live-sync] EventSource error", event);
+    });
+
     sse.addEventListener("update", (event) => {
       onUpdate(event.data);
     });
diff --git a/src/lib/query-cache-fresh.ts b/src/lib/query-cache-fresh.ts
index 1dd6fe5..cc24d50 100644
--- a/src/lib/query-cache-fresh.ts
+++ b/src/lib/query-cache-fresh.ts
@@ -1,6 +1,5 @@
 import type { QueryClient, QueryKey } from "@tanstack/react-query";
-
-const DEFAULT_STALE_TIME_MS = 60_000;
+import { DEFAULT_STALE_TIME_MS } from "~/lib/query-cache-defaults";
 
 export function isQueryCacheFresh(
   queryClient: QueryClient,
diff --git a/src/router.tsx b/src/router.tsx
index fc333df..e67430c 100644
--- a/src/router.tsx
+++ b/src/router.tsx
@@ -4,13 +4,18 @@ import { setupRouterSsrQueryIntegration } from "@tanstack/react-router-ssr-query
 import { routeTree } from "./routeTree.gen";
 import { DefaultCatchBoundary } from "./components/DefaultCatchBoundary";
 import { NotFound } from "./components/NotFound";
+import {
+  DEFAULT_GC_TIME_MS,
+  DEFAULT_STALE_RELOAD_MODE,
+  DEFAULT_STALE_TIME_MS,
+} from "~/lib/query-cache-defaults";
 
 export function getRouter() {
   const queryClient = new QueryClient({
     defaultOptions: {
       queries: {
-        staleTime: 60_000,
-        gcTime: 5 * 60_000,
+        staleTime: DEFAULT_STALE_TIME_MS,
+        gcTime: DEFAULT_GC_TIME_MS,
         refetchOnWindowFocus: false,
       },
     },
@@ -20,9 +25,9 @@ export function getRouter() {
     routeTree,
     context: { queryClient },
     defaultPreload: "intent",
-    defaultStaleTime: 60_000,
-    defaultGcTime: 5 * 60_000,
-    defaultStaleReloadMode: "background",
+    defaultStaleTime: DEFAULT_STALE_TIME_MS,
+    defaultGcTime: DEFAULT_GC_TIME_MS,
+    defaultStaleReloadMode: DEFAULT_STALE_RELOAD_MODE,
     defaultErrorComponent: DefaultCatchBoundary,
     defaultNotFoundComponent: () => <NotFound />,
     defaultViewTransition: true,
diff --git a/src/routes/_signed-in.tsx b/src/routes/_signed-in.tsx
index 149e44e..30bc3dc 100644
--- a/src/routes/_signed-in.tsx
+++ b/src/routes/_signed-in.tsx
@@ -26,13 +26,14 @@ export const Route = createFileRoute("/_signed-in")({
     await ensureSignedInBeforeLoad({ data: location.href });
   },
   loader: async ({ context }) => {
-    const needsSidebar =
-      !isQueryCacheFresh(context.queryClient, projectsQueryOptions().queryKey) ||
-      !isQueryCacheFresh(context.queryClient, sprintsQueryOptions().queryKey);
     const needsPreferences = !isQueryCacheFresh(
       context.queryClient,
       userPreferencesQueryOptions().queryKey
     );
+    const needsSidebar =
+      needsPreferences ||
+      !isQueryCacheFresh(context.queryClient, projectsQueryOptions().queryKey) ||
+      !isQueryCacheFresh(context.queryClient, sprintsQueryOptions().queryKey);
 
     const [bundle, preferences] = await Promise.all([
       needsSidebar ? fetchSidebarBundle() : Promise.resolve(null),

From 23d6e983ed40df79cc316d61cbed4f1d41721b12 Mon Sep 17 00:00:00 2001
From: Philipp Nowinski <mail@pno.dev>
Date: Thu, 11 Jun 2026 08:41:51 +0200
Subject: [PATCH 3/5] fix: add missing files

---
 src/lib/query-cache-defaults.ts |  3 +++
 src/lib/sync-envelope.test.ts   | 22 ++++++++++++++++++++++
 src/lib/sync-envelope.ts        | 33 +++++++++++++++++++++++++++++++++
 3 files changed, 58 insertions(+)
 create mode 100644 src/lib/query-cache-defaults.ts
 create mode 100644 src/lib/sync-envelope.test.ts
 create mode 100644 src/lib/sync-envelope.ts

diff --git a/src/lib/query-cache-defaults.ts b/src/lib/query-cache-defaults.ts
new file mode 100644
index 0000000..3989de3
--- /dev/null
+++ b/src/lib/query-cache-defaults.ts
@@ -0,0 +1,3 @@
+export const DEFAULT_STALE_TIME_MS = 60_000;
+export const DEFAULT_GC_TIME_MS = 5 * 60_000;
+export const DEFAULT_STALE_RELOAD_MODE = "background" as const;
diff --git a/src/lib/sync-envelope.test.ts b/src/lib/sync-envelope.test.ts
new file mode 100644
index 0000000..f9a8cc9
--- /dev/null
+++ b/src/lib/sync-envelope.test.ts
@@ -0,0 +1,22 @@
+import { describe, expect, it } from "vitest";
+import { createSyncEnvelope, parseSyncEnvelope } from "./sync-envelope";
+
+describe("sync envelope", () => {
+  it("round-trips fine-grained topics", () => {
+    const envelope = createSyncEnvelope("task-create", { data: { id: "1" } });
+    const serialized = JSON.stringify(envelope);
+
+    expect(parseSyncEnvelope(serialized)).toEqual(envelope);
+  });
+
+  it("round-trips coarse topics with entityId", () => {
+    const envelope = createSyncEnvelope(
+      "task-update",
+      { id: "task-1", statusId: "status-1" },
+      "task-1"
+    );
+    const serialized = JSON.stringify(envelope);
+
+    expect(parseSyncEnvelope(serialized)).toEqual(envelope);
+  });
+});
diff --git a/src/lib/sync-envelope.ts b/src/lib/sync-envelope.ts
new file mode 100644
index 0000000..6fc78af
--- /dev/null
+++ b/src/lib/sync-envelope.ts
@@ -0,0 +1,33 @@
+export type SyncEnvelope = {
+  topic: string;
+  entityId?: string;
+  payload: unknown;
+};
+
+export function createSyncEnvelope(
+  topic: string,
+  payload: unknown,
+  entityId?: string
+): SyncEnvelope {
+  return entityId ? { topic, entityId, payload } : { topic, payload };
+}
+
+export function parseSyncEnvelope(data: unknown): SyncEnvelope | null {
+  if (typeof data !== "string" || !data) return null;
+  try {
+    const parsed = JSON.parse(data) as {
+      topic?: string;
+      entityId?: string;
+      payload?: unknown;
+    };
+    if (typeof parsed.topic !== "string") return null;
+    return {
+      topic: parsed.topic,
+      entityId:
+        typeof parsed.entityId === "string" ? parsed.entityId : undefined,
+      payload: parsed.payload,
+    };
+  } catch {
+    return null;
+  }
+}

From c9c8942a9195f57a13734f1437256d300501767d Mon Sep 17 00:00:00 2001
From: Philipp Nowinski <mail@pno.dev>
Date: Thu, 11 Jun 2026 09:18:12 +0200
Subject: [PATCH 4/5] fic: fix hydration error

---
 src/routes/_signed-in/dashboard.tsx | 34 +++++++++++++++++++++--------
 1 file changed, 25 insertions(+), 9 deletions(-)

diff --git a/src/routes/_signed-in/dashboard.tsx b/src/routes/_signed-in/dashboard.tsx
index 84721b9..f3dc030 100644
--- a/src/routes/_signed-in/dashboard.tsx
+++ b/src/routes/_signed-in/dashboard.tsx
@@ -19,11 +19,24 @@ import { isQueryCacheFresh } from "~/lib/query-cache-fresh";
 import { dashboardStatsQueryOptions } from "~/db/queries/dashboard";
 import { useProjectsQuery } from "~/db/queries/projects";
 import { useSprintsQuery } from "~/db/queries/sprints";
-import { authClient } from "~/lib/auth-client";
 import { FORM_SHEET_CREATE_LINKS } from "~/lib/form-sheet-search";
 import { cn, formatUserName } from "~/lib/utils";
 import { pageMeta } from "~/utils/seo";
 
+function sessionDisplayName(
+  user:
+    | {
+        name?: string | null;
+        firstname?: string;
+        lastname?: string;
+      }
+    | undefined
+) {
+  return (
+    formatUserName(user?.firstname, user?.lastname) || user?.name || "there"
+  );
+}
+
 export const Route = createFileRoute("/_signed-in/dashboard")({
   loader: async ({ context }) => {
     if (
@@ -32,6 +45,16 @@ export const Route = createFileRoute("/_signed-in/dashboard")({
       const stats = await fetchDashboardStats();
       hydrateDashboardCache(context.queryClient, stats);
     }
+
+    const { getSessionFromRequest } = await import("~/lib/session");
+    const session = await getSessionFromRequest();
+    return {
+      displayName: sessionDisplayName(
+        session?.user as
+          | { name?: string | null; firstname?: string; lastname?: string }
+          | undefined
+      ),
+    };
   },
   head: () => ({
     meta: [...pageMeta("Dashboard")],
@@ -40,18 +63,11 @@ export const Route = createFileRoute("/_signed-in/dashboard")({
 });
 
 function RouteComponent() {
-  const { data: session } = authClient.useSession();
+  const { displayName } = Route.useLoaderData();
   const statsQuery = useDashboardStatsQuery();
   const projectsQuery = useProjectsQuery();
   const sprintsQuery = useSprintsQuery();
 
-  const user = session?.user;
-  const authUser = user as { firstname?: string; lastname?: string } | undefined;
-  const displayName =
-    formatUserName(authUser?.firstname, authUser?.lastname) ||
-    user?.name ||
-    "there";
-
   const stats = statsQuery.data;
   const now = new Date();
   const isSprintActive = (start: Date, end: Date) => start < now && end > now;

From 846512deb6970fc964ef6249befc35309a129512 Mon Sep 17 00:00:00 2001
From: Philipp Nowinski <mail@pno.dev>
Date: Thu, 11 Jun 2026 09:24:56 +0200
Subject: [PATCH 5/5] fix: rix build error

---
 src/routes/_signed-in/dashboard.tsx | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/routes/_signed-in/dashboard.tsx b/src/routes/_signed-in/dashboard.tsx
index f3dc030..7e8f4ed 100644
--- a/src/routes/_signed-in/dashboard.tsx
+++ b/src/routes/_signed-in/dashboard.tsx
@@ -19,6 +19,7 @@ import { isQueryCacheFresh } from "~/lib/query-cache-fresh";
 import { dashboardStatsQueryOptions } from "~/db/queries/dashboard";
 import { useProjectsQuery } from "~/db/queries/projects";
 import { useSprintsQuery } from "~/db/queries/sprints";
+import { getSession } from "~/lib/auth-functions";
 import { FORM_SHEET_CREATE_LINKS } from "~/lib/form-sheet-search";
 import { cn, formatUserName } from "~/lib/utils";
 import { pageMeta } from "~/utils/seo";
@@ -46,8 +47,7 @@ export const Route = createFileRoute("/_signed-in/dashboard")({
       hydrateDashboardCache(context.queryClient, stats);
     }
 
-    const { getSessionFromRequest } = await import("~/lib/session");
-    const session = await getSessionFromRequest();
+    const session = await getSession();
     return {
       displayName: sessionDisplayName(
         session?.user as
