From afad7cd6afebdd5f38e5b988c67e0670c8825f64 Mon Sep 17 00:00:00 2001
From: Producdevity <y.gherbi.dev@gmail.com>
Date: Tue, 26 May 2026 18:03:05 +0200
Subject: [PATCH 01/35] refactor: simplify pagination logic and improve type
 safety

---
 src/server/api/routers/socs.ts |  1 +
 src/server/utils/pagination.ts | 54 ++++++----------------------------
 2 files changed, 10 insertions(+), 45 deletions(-)

diff --git a/src/server/api/routers/socs.ts b/src/server/api/routers/socs.ts
index 749f840a6..c88df5642 100644
--- a/src/server/api/routers/socs.ts
+++ b/src/server/api/routers/socs.ts
@@ -18,6 +18,7 @@ import { paginate } from '@/server/utils/pagination'
 
 export const socsRouter = createTRPCRouter({
   get: publicProcedure.input(GetSoCsSchema).query(async ({ ctx, input }) => {
+    // TODO: use paginate helpers
     const repository = new SoCsRepository(ctx.prisma)
     const { limit = 20, offset = 0, page } = input ?? {}
 
diff --git a/src/server/utils/pagination.ts b/src/server/utils/pagination.ts
index c57d9a483..d75aa666d 100644
--- a/src/server/utils/pagination.ts
+++ b/src/server/utils/pagination.ts
@@ -1,4 +1,5 @@
 import { toArray } from '@/utils/array'
+import type { SortDirection } from '@/types/api'
 
 export interface PaginationInput {
   limit?: number
@@ -46,18 +47,17 @@ interface PaginateParams {
  * @returns Complete pagination metadata with calculated offset
  */
 export function paginate(params: PaginateParams): PaginationResult {
-  const { total, page, limit } = params
-  const offset = (page - 1) * limit
-  const pages = Math.ceil(total / limit)
+  const offset = (params.page - 1) * params.limit
+  const pages = Math.ceil(params.total / params.limit)
 
   return {
-    total,
-    page,
-    limit,
+    total: params.total,
+    page: params.page,
+    limit: params.limit,
     pages,
     offset,
-    hasNextPage: page < pages,
-    hasPreviousPage: page > 1,
+    hasNextPage: params.page < pages,
+    hasPreviousPage: params.page > 1,
   }
 }
 
@@ -115,8 +115,7 @@ export async function paginatedQuery<T>(
     }),
   ])
 
-  const { page } = paginationInput
-  const actualPage = page ?? Math.floor(actualOffset / limit) + 1
+  const actualPage = paginationInput.page ?? Math.floor(actualOffset / limit) + 1
   const pagination = paginate({ total, page: actualPage, limit })
 
   return {
@@ -125,41 +124,6 @@ export async function paginatedQuery<T>(
   }
 }
 
-/**
- * Type-safe wrapper for paginated queries with Prisma
- * Ensures proper typing for the model and return type
- */
-export function createPaginatedQueryWrapper<
-  Model extends {
-    count: (args?: { where?: unknown }) => Promise<number>
-    findMany: (args?: unknown) => Promise<unknown[]>
-  },
->() {
-  return async function <T>(
-    model: Model,
-    args: Parameters<Model['findMany']>[0],
-    paginationInput: PaginationInput,
-    defaultLimit = 20,
-  ): Promise<PaginatedResponse<T>> {
-    return paginatedQuery<T>(
-      model as {
-        count: (args?: { where?: unknown }) => Promise<number>
-        findMany: (args?: unknown) => Promise<T[]>
-      },
-      args as {
-        where?: unknown
-        orderBy?: unknown
-        include?: unknown
-        select?: unknown
-      },
-      paginationInput,
-      defaultLimit,
-    )
-  }
-}
-
-export type SortDirection = 'asc' | 'desc'
-
 /**
  * Build orderBy clause from sort field and direction
  * The generic type T represents the shape of orderBy objects

From 49393ea96e230fb9289fa4a06708618f2572dffc Mon Sep 17 00:00:00 2001
From: Producdevity <y.gherbi.dev@gmail.com>
Date: Tue, 26 May 2026 21:51:26 +0200
Subject: [PATCH 02/35] refactor: rename helper function for clarity and ensure
 unique element selection

---
 prisma/seeders/listingsSeeder.ts | 12 ++++--------
 1 file changed, 4 insertions(+), 8 deletions(-)

diff --git a/prisma/seeders/listingsSeeder.ts b/prisma/seeders/listingsSeeder.ts
index 64309a5ec..f8079c7e8 100644
--- a/prisma/seeders/listingsSeeder.ts
+++ b/prisma/seeders/listingsSeeder.ts
@@ -1,17 +1,14 @@
 import { Role, ApprovalStatus, type PrismaClient } from '@orm/client'
 
-// Helper function to get random element from array
 function getRandomElement<T>(array: T[]): T {
   return array[Math.floor(Math.random() * array.length)]
 }
 
-// Helper function to get random elements from array (without duplicates)
-function getRandomElements<T>(array: T[], count: number): T[] {
+function getRandomUniqueElements<T>(array: T[], count: number): T[] {
   const shuffled = [...array].sort(() => 0.5 - Math.random())
   return shuffled.slice(0, Math.min(count, array.length))
 }
 
-// Sample notes for different performance levels
 const sampleNotes = {
   Perfect: [
     'Runs flawlessly at full speed with no issues.',
@@ -71,7 +68,6 @@ const sampleNotes = {
   ],
 }
 
-// Sample comment content
 const sampleComments = [
   'Thanks for this report! Very helpful.',
   'I can confirm this works on my device too.',
@@ -97,7 +93,7 @@ async function createVotesAndComments(
 ) {
   // Add some random votes (60% upvotes, 40% downvotes)
   const votersCount = Math.floor(Math.random() * 8) + 2 // 2-9 voters
-  const voters = getRandomElements(users, votersCount)
+  const voters = getRandomUniqueElements(users, votersCount)
 
   for (const voter of voters) {
     const isUpvote = Math.random() > 0.4 // 60% chance of upvote
@@ -114,7 +110,7 @@ async function createVotesAndComments(
   // Add some random comments (30% chance per listing)
   if (Math.random() < 0.3) {
     const commenterCount = Math.floor(Math.random() * 3) + 1 // 1-3 comments
-    const commenters = getRandomElements(users, commenterCount)
+    const commenters = getRandomUniqueElements(users, commenterCount)
 
     for (const commenter of commenters) {
       const content = getRandomElement(sampleComments)
@@ -184,7 +180,7 @@ async function listingsSeeder(prisma: PrismaClient) {
     }
 
     // Pick 2 different games for this device
-    const selectedGames = getRandomElements(compatibleGames, 2)
+    const selectedGames = getRandomUniqueElements(compatibleGames, 2)
 
     for (let i = 0; i < selectedGames.length; i++) {
       const game = selectedGames[i]

From cc0200945d54fedfed7f3147690505130010ad65 Mon Sep 17 00:00:00 2001
From: Producdevity <y.gherbi.dev@gmail.com>
Date: Tue, 26 May 2026 21:52:07 +0200
Subject: [PATCH 03/35] refactor: update cookie consent logic to use storage
 keys and improve readability

---
 .github/workflows/e2e-tests.yml |  2 +-
 tests/helpers/cookie-consent.ts | 27 +++++++++++----------------
 2 files changed, 12 insertions(+), 17 deletions(-)

diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml
index d5c4e949b..9d170853b 100644
--- a/.github/workflows/e2e-tests.yml
+++ b/.github/workflows/e2e-tests.yml
@@ -59,7 +59,7 @@ jobs:
       NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${{ secrets.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY }}
       NEXT_PUBLIC_GA_ID: ${{ secrets.NEXT_PUBLIC_GA_ID }}
       NEXT_PUBLIC_IGDB_CLIENT_ID: ${{ secrets.NEXT_PUBLIC_IGDB_CLIENT_ID }}
-      NEXT_PUBLIC_LOCAL_STORAGE_PREFIX: ${{ secrets.NEXT_PUBLIC_LOCAL_STORAGE_PREFIX }}
+      NEXT_PUBLIC_LOCAL_STORAGE_PREFIX: "@TestEmuReady_"
       RAWG_API_KEY: ${{ secrets.RAWG_API_KEY }}
       THE_GAMES_DB_API_KEY: ${{ secrets.THE_GAMES_DB_API_KEY }}
       # Test configuration
diff --git a/tests/helpers/cookie-consent.ts b/tests/helpers/cookie-consent.ts
index f3d86ffe8..b04ab6532 100644
--- a/tests/helpers/cookie-consent.ts
+++ b/tests/helpers/cookie-consent.ts
@@ -1,22 +1,15 @@
+import storageKeys from '@/data/storageKeys'
 import type { BrowserContext, Page } from '@playwright/test'
 
-const STORAGE_PREFIX = process.env.NEXT_PUBLIC_LOCAL_STORAGE_PREFIX
-if (!STORAGE_PREFIX) {
-  throw new Error(
-    'NEXT_PUBLIC_LOCAL_STORAGE_PREFIX is not set in the test environment. ' +
-      'playwright.config.ts loads .env.local and .env.test.local — ensure one of them defines it.',
-  )
-}
-
-function applyConsent(prefix: string) {
-  localStorage.setItem(`${prefix}cookie_consent`, JSON.stringify(true))
+function applyConsent(keys: typeof storageKeys.cookies) {
+  localStorage.setItem(keys.consent, JSON.stringify(true))
   localStorage.setItem(
-    `${prefix}cookie_preferences`,
+    keys.preferences,
     JSON.stringify({ necessary: true, analytics: false, performance: false }),
   )
-  localStorage.setItem(`${prefix}cookie_consent_date`, JSON.stringify(new Date().toISOString()))
-  localStorage.setItem(`${prefix}analytics_enabled`, JSON.stringify(false))
-  localStorage.setItem(`${prefix}performance_enabled`, JSON.stringify(false))
+  localStorage.setItem(keys.consentDate, JSON.stringify(new Date().toISOString()))
+  localStorage.setItem(keys.analyticsEnabled, JSON.stringify(false))
+  localStorage.setItem(keys.performanceEnabled, JSON.stringify(false))
 
   const styleId = '__e2e_cookie_consent_hidden__'
   const css = '[data-testid="cookie-consent"]{display:none !important;}'
@@ -39,10 +32,12 @@ function applyConsent(prefix: string) {
     document.addEventListener('readystatechange', injectStyle, { once: true })
     document.addEventListener('DOMContentLoaded', injectStyle, { once: true })
     observer = new MutationObserver(injectStyle)
-    observer.observe(document.documentElement, { childList: true, subtree: true })
+    if (document.documentElement) {
+      observer.observe(document.documentElement, { childList: true, subtree: true })
+    }
   }
 }
 
 export async function registerCookieConsent(target: BrowserContext | Page) {
-  await target.addInitScript(applyConsent, STORAGE_PREFIX)
+  await target.addInitScript(applyConsent, storageKeys.cookies)
 }

From 17d15b3345c0598fc63a75ef47c6cf3b023c34c9 Mon Sep 17 00:00:00 2001
From: Producdevity <y.gherbi.dev@gmail.com>
Date: Tue, 26 May 2026 21:52:29 +0200
Subject: [PATCH 04/35] refactor: add native thin scrollbar styles for improved
 aesthetics

---
 src/app/globals.css | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/src/app/globals.css b/src/app/globals.css
index 1dee1278a..4fd2437c9 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -266,6 +266,11 @@
     display: none;
   }
 
+  .scrollbar-native-thin {
+    scrollbar-width: thin;
+    scrollbar-color: auto;
+  }
+
   .animate-fade-in-up {
     animation: var(--animate-fade-in-up);
     opacity: 0;

From 2fbc2ea2ea4d979d9b965d7344cc7521312b3a3a Mon Sep 17 00:00:00 2001
From: Producdevity <y.gherbi.dev@gmail.com>
Date: Tue, 26 May 2026 21:52:49 +0200
Subject: [PATCH 05/35] refactor: mock external service scripts

---
 tests/helpers/external-services.ts | 27 +++++++++++++++++++++++++++
 1 file changed, 27 insertions(+)

diff --git a/tests/helpers/external-services.ts b/tests/helpers/external-services.ts
index ec42861e2..a373396c7 100644
--- a/tests/helpers/external-services.ts
+++ b/tests/helpers/external-services.ts
@@ -6,6 +6,33 @@ const transparentPng = Buffer.from(
 )
 
 export async function registerExternalServiceMocks(page: Page) {
+  await page.route('**/_vercel/speed-insights/script.js', async (route) => {
+    await route.fulfill({
+      status: 200,
+      contentType: 'application/javascript',
+      body: '',
+    })
+  })
+
+  await page.route(
+    'https://storage.ko-fi.com/cdn/scripts/floating-chat-wrapper.css',
+    async (route) => {
+      await route.fulfill({
+        status: 200,
+        contentType: 'text/css',
+        body: '',
+      })
+    },
+  )
+
+  await page.route('https://storage.ko-fi.com/cdn/scripts/overlay-widget.js', async (route) => {
+    await route.fulfill({
+      status: 200,
+      contentType: 'application/javascript',
+      body: 'window.kofiWidgetOverlay={draw:function(){}};',
+    })
+  })
+
   await page.route(/\/api\/proxy-image(?:\?.*)?$/u, async (route) => {
     await route.fulfill({
       status: 200,

From dc4e0a4e296aee439575b4c02fa78c916b710bbd Mon Sep 17 00:00:00 2001
From: Producdevity <y.gherbi.dev@gmail.com>
Date: Wed, 27 May 2026 18:51:56 +0200
Subject: [PATCH 06/35] refactor: remove beta feature flag and related
 components

---
 .env.docker.example                                     | 1 -
 .env.example                                            | 1 -
 .env.test.example                                       | 1 -
 src/app/listings/shared/components/MobileFiltersFab.tsx | 4 +---
 src/components/footer/Footer.tsx                        | 3 ---
 src/components/footer/components/FooterBetaBadge.tsx    | 9 ---------
 src/lib/env.ts                                          | 2 --
 7 files changed, 1 insertion(+), 20 deletions(-)
 delete mode 100644 src/components/footer/components/FooterBetaBadge.tsx

diff --git a/.env.docker.example b/.env.docker.example
index 9223417d4..7a7486e7f 100644
--- a/.env.docker.example
+++ b/.env.docker.example
@@ -72,7 +72,6 @@ NEXT_PUBLIC_GITHUB_URL="https://github.com/Producdevity/EmuReady"
 NEXT_PUBLIC_EMUREADY_LITE_GITHUB_URL="https://github.com/Producdevity/EmuReadyLite/releases"
 NEXT_PUBLIC_APP_URL="https://dev.emuready.com"
 NEXT_PUBLIC_ENABLE_SW=false
-NEXT_PUBLIC_IS_BETA=false
 NEXT_PUBLIC_ENABLE_ASYNC_LISTINGS_FILTERS=false
 NEXT_PUBLIC_ENABLE_V2_LISTINGS=false
 NEXT_TELEMETRY_DISABLED=1
diff --git a/.env.example b/.env.example
index 592c1934d..d52138dd4 100644
--- a/.env.example
+++ b/.env.example
@@ -46,7 +46,6 @@ NEXT_PUBLIC_GITHUB_URL="https://github.com/Producdevity/EmuReady"
 NEXT_PUBLIC_EMUREADY_LITE_GITHUB_URL="https://github.com/Producdevity/EmuReadyLite/releases"
 NEXT_PUBLIC_APP_URL="http://localhost:3000" # Make sure to change this if you are using a tunnel
 NEXT_PUBLIC_ENABLE_SW=false
-NEXT_PUBLIC_IS_BETA=false
 NEXT_PUBLIC_ENABLE_PATREON_VERIFICATION=true
 NEXT_PUBLIC_ENABLE_ASYNC_LISTINGS_FILTERS=false
 NEXT_PUBLIC_ENABLE_V2_LISTINGS=false
diff --git a/.env.test.example b/.env.test.example
index 8f23094b8..b627bb17c 100644
--- a/.env.test.example
+++ b/.env.test.example
@@ -36,7 +36,6 @@ NEXT_PUBLIC_KOFI_LINK="https://ko-fi.com/producdevity"
 NEXT_PUBLIC_APP_URL="https://dev.emuready.com"
 NEXT_PUBLIC_ENABLE_PATREON_VERIFICATION=true
 NEXT_PUBLIC_ENABLE_SW=false
-NEXT_PUBLIC_IS_BETA=false
 NEXT_PUBLIC_DISABLE_COOKIE_BANNER=true
 NEXT_PUBLIC_ENABLE_ASYNC_LISTINGS_FILTERS=false
 NEXT_PUBLIC_ENABLE_V2_LISTINGS=false
diff --git a/src/app/listings/shared/components/MobileFiltersFab.tsx b/src/app/listings/shared/components/MobileFiltersFab.tsx
index 2deecf745..fc1f39650 100644
--- a/src/app/listings/shared/components/MobileFiltersFab.tsx
+++ b/src/app/listings/shared/components/MobileFiltersFab.tsx
@@ -1,8 +1,6 @@
 'use client'
 
 import { FunnelIcon } from 'lucide-react'
-import { env } from '@/lib/env'
-import { cn } from '@/lib/utils'
 
 interface Props {
   ariaLabel: string
@@ -12,7 +10,7 @@ interface Props {
 
 export function MobileFiltersFab(props: Props) {
   return (
-    <div className={cn('lg:hidden fixed right-6 z-40', env.IS_BETA ? 'bottom-14' : 'bottom-10')}>
+    <div className="lg:hidden fixed right-6 z-40 bottom-10">
       <button
         type="button"
         onClick={props.onClick}
diff --git a/src/components/footer/Footer.tsx b/src/components/footer/Footer.tsx
index ac29fe935..7f213c1aa 100644
--- a/src/components/footer/Footer.tsx
+++ b/src/components/footer/Footer.tsx
@@ -3,7 +3,6 @@
 import Link from 'next/link'
 import { useEffect, useState } from 'react'
 import { FooterAppLinks } from '@/components/footer/components/FooterAppLinks'
-import { FooterBetaBadge } from '@/components/footer/components/FooterBetaBadge'
 import { FooterKofiButton } from '@/components/footer/components/FooterKofiButton'
 import { FooterLink } from '@/components/footer/components/FooterLink'
 import { FooterPatreonButton } from '@/components/footer/components/FooterPatreonButton'
@@ -196,8 +195,6 @@ function Footer() {
           </div>
         </div>
       </div>
-
-      {env.IS_BETA && <FooterBetaBadge />}
     </footer>
   )
 }
diff --git a/src/components/footer/components/FooterBetaBadge.tsx b/src/components/footer/components/FooterBetaBadge.tsx
deleted file mode 100644
index 023e0a30b..000000000
--- a/src/components/footer/components/FooterBetaBadge.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-export function FooterBetaBadge() {
-  return (
-    <div className="fixed bottom-4 right-6 z-50 pointer-events-none">
-      <div className="px-3 py-1.5 bg-gradient-to-r from-orange-500 to-red-500 text-white text-xs font-bold rounded-full shadow-lg backdrop-blur-sm border border-orange-400/20 animate-pulse-slow">
-        BETA
-      </div>
-    </div>
-  )
-}
diff --git a/src/lib/env.ts b/src/lib/env.ts
index fe2185fa3..bc2fa8439 100644
--- a/src/lib/env.ts
+++ b/src/lib/env.ts
@@ -17,7 +17,6 @@ interface Env {
   ENABLE_SW: boolean
   VERCEL_ANALYTICS_ENABLED: boolean
   DISABLE_COOKIE_BANNER: boolean
-  IS_BETA: boolean
   IS_PROD: boolean
   IS_DEV: boolean
   IS_TEST: boolean
@@ -67,7 +66,6 @@ export const env = {
 
   DISABLE_COOKIE_BANNER: process.env.NEXT_PUBLIC_DISABLE_COOKIE_BANNER === 'true',
 
-  IS_BETA: process.env.NEXT_PUBLIC_IS_BETA === 'true',
   IS_PROD: process.env.NODE_ENV === 'production',
   IS_DEV: process.env.NODE_ENV === 'development',
   IS_TEST: process.env.NODE_ENV === 'test',

From c075f789a30cbe1eaba3937a4f54b713ddd7d746 Mon Sep 17 00:00:00 2001
From: Producdevity <y.gherbi.dev@gmail.com>
Date: Wed, 27 May 2026 19:14:40 +0200
Subject: [PATCH 07/35] refactor: remove AnimatePresence wrapper from
 ActiveFiltersSummary component

---
 .../components/ActiveFiltersSummary.tsx       | 26 +++++++++----------
 1 file changed, 12 insertions(+), 14 deletions(-)

diff --git a/src/app/listings/shared/components/ActiveFiltersSummary.tsx b/src/app/listings/shared/components/ActiveFiltersSummary.tsx
index 1ad64ba38..eedd26a20 100644
--- a/src/app/listings/shared/components/ActiveFiltersSummary.tsx
+++ b/src/app/listings/shared/components/ActiveFiltersSummary.tsx
@@ -1,12 +1,12 @@
 'use client'
 
-import { AnimatePresence, motion } from 'framer-motion'
+import { motion } from 'framer-motion'
 import { motionPresets } from '@/lib/motionPresets'
 
 interface SummaryItem {
   key: string
   content: string
-  colorClass: string // e.g., 'bg-yellow-500'
+  colorClass: string
   delay?: number
 }
 
@@ -41,18 +41,16 @@ export function ActiveFiltersSummary(props: Props) {
         )}
       </div>
       <div className="text-xs text-gray-600 dark:text-gray-400 space-y-1">
-        <AnimatePresence>
-          {props.items.map((item, index) => (
-            <motion.div
-              key={item.key}
-              {...motionPresets.fadeInLeft(item.delay ?? 0.1 + index * 0.05)}
-              className="flex items-center gap-2"
-            >
-              <span className={`w-2 h-2 ${item.colorClass} rounded-full`} />
-              {item.content}
-            </motion.div>
-          ))}
-        </AnimatePresence>
+        {props.items.map((item, index) => (
+          <motion.div
+            key={item.key}
+            {...motionPresets.fadeInLeft(item.delay ?? 0.1 + index * 0.05)}
+            className="flex items-center gap-2"
+          >
+            <span className={`w-2 h-2 ${item.colorClass} rounded-full`} />
+            {item.content}
+          </motion.div>
+        ))}
       </div>
     </motion.div>
   )

From 6b8e661d884e14239fa68a13dd125c772bfa5777 Mon Sep 17 00:00:00 2001
From: Producdevity <y.gherbi.dev@gmail.com>
Date: Thu, 28 May 2026 00:38:31 +0200
Subject: [PATCH 08/35] refactor: replace individual Async*MultiSelect
 components with consolidated Async*FilterSelect components and adjust query
 logic for filters

---
 .env.docker.example                           |   5 +
 .env.example                                  |   5 +
 .env.test.example                             |   4 +
 next.config.ts                                |  13 +-
 playwright.config.ts                          |   9 +-
 .../migration.sql                             |  32 ++
 prisma/schema.prisma                          |  11 +
 prisma/seed.ts                                |  17 +
 prisma/seeders/azaharCustomFieldsSeeder.ts    |  77 +++-
 prisma/seeders/customFieldCategoryUtils.ts    |  41 ++
 prisma/seeders/customFieldValueSeederUtils.ts | 199 ++++++++++
 prisma/seeders/edenCustomFieldsSeeder.ts      | 107 +++++-
 .../seeders/gamenativeCustomFieldsSeeder.ts   |  86 ++++-
 prisma/seeders/listingsSeeder.ts              | 127 ++++++-
 prisma/seeders/pcListingsSeeder.ts            | 180 +++++++++
 sentry.edge.config.ts                         |  24 +-
 sentry.server.config.ts                       |  24 +-
 src/app/admin/brands/page.tsx                 |  15 +-
 src/app/admin/cpus/page.tsx                   |  12 +-
 .../admin/devices/components/DeviceModal.tsx  |   3 +-
 src/app/admin/devices/page.tsx                |  13 +-
 src/app/admin/gpus/page.tsx                   |  13 +-
 .../[id]/edit/components/ListingEditForm.tsx  |   2 +-
 src/app/admin/socs/page.tsx                   |  13 +-
 src/app/api/proxy-image/route.ts              |   4 +-
 .../home/components/HomeTrendingDevices.tsx   |  13 +-
 src/app/layout.tsx                            |  26 +-
 src/app/listings/ListingsPage.tsx             |  26 +-
 .../components/ListingsFiltersContent.tsx     |  40 +-
 .../components/ListingsFiltersSidebar.tsx     |  35 +-
 .../filters/AsyncDeviceFilterSelect.test.tsx  | 191 ++++++++++
 .../filters/AsyncDeviceFilterSelect.tsx       |  88 +++++
 .../filters/AsyncSocFilterSelect.test.tsx     |  85 +++++
 .../filters/AsyncSocFilterSelect.tsx          |  88 +++++
 src/app/listings/new/NewListingPage.tsx       |  13 +-
 src/app/pc-listings/PcListingsPage.tsx        |  41 +-
 .../components/PcFiltersContent.tsx           |  10 +-
 .../filters/AsyncCpuFilterSelect.test.tsx     |  87 +++++
 .../filters/AsyncCpuFilterSelect.tsx          |  88 +++++
 .../filters/AsyncGpuFilterSelect.test.tsx     |  85 +++++
 .../filters/AsyncGpuFilterSelect.tsx          |  88 +++++
 src/app/pc-listings/new/NewPcListingPage.tsx  |  22 +-
 src/app/profile/components/DeviceSelector.tsx |  17 +-
 src/app/profile/components/PcPresetModal.tsx  |  67 +++-
 src/app/profile/components/SocSelector.tsx    |   9 +-
 src/app/v2/listings/V2ListingsPage.tsx        |  30 +-
 .../v2/listings/components/ListingFilters.tsx |  41 +-
 src/components/popups/BetaWarningPopup.tsx    |   6 +-
 .../popups/StopKillingGamesPopup.tsx          |   7 +-
 src/components/ui/KofiWidget.tsx              |   5 +-
 .../ui/form/AsyncCpuMultiSelect.tsx           |  70 ----
 .../ui/form/AsyncDeviceMultiSelect.tsx        |  70 ----
 .../ui/form/AsyncGpuMultiSelect.tsx           |  70 ----
 src/components/ui/form/AsyncMultiSelect.tsx   | 231 ------------
 .../ui/form/AsyncSocMultiSelect.tsx           |  70 ----
 src/components/ui/form/MultiSelect.test.tsx   |  59 ++-
 src/components/ui/form/MultiSelect.tsx        | 111 ++++--
 .../AsyncMultiSelect.test.tsx                 | 354 ++++++++++++++++++
 .../async-multi-select/AsyncMultiSelect.tsx   | 128 +++++--
 src/components/ui/form/index.ts               |   1 -
 src/instrumentation-client.test.ts            |  55 +++
 src/instrumentation-client.ts                 |   5 +-
 .../utils/sendAnalyticsEvent.test.ts          |  89 +++++
 src/lib/analytics/utils/sendAnalyticsEvent.ts |  17 +-
 src/lib/cors.test.ts                          |  80 +++-
 src/lib/cors.ts                               |  23 +-
 src/lib/env.test.ts                           |  77 ++++
 src/lib/env.ts                                |  34 +-
 src/lib/trust/service.test.ts                 |  29 ++
 src/lib/trust/service.ts                      |   9 +-
 src/proxy.ts                                  |  10 +-
 src/schemas/cpu.ts                            |  10 +
 src/schemas/device.ts                         |  11 +
 src/schemas/gpu.ts                            |  10 +
 src/schemas/soc.ts                            |   9 +
 src/sentry-config.test.ts                     |  52 +++
 src/server/api/routers/cpus.ts                |   6 +
 src/server/api/routers/devices.ts             |  19 +-
 src/server/api/routers/gpus.ts                |   6 +
 src/server/api/routers/socs.ts                |   8 +-
 src/server/repositories/cpus.repository.ts    |  40 +-
 src/server/repositories/devices.repository.ts | 143 +++++--
 src/server/repositories/gpus.repository.ts    |  37 +-
 .../repositories/listings.repository.ts       |   2 +-
 .../repositories/pc-listings.repository.ts    |   2 +-
 src/server/repositories/socs.repository.ts    |  38 +-
 tests/filtering.spec.ts                       |  71 +++-
 tests/pages/ListingsPage.ts                   |   8 +-
 tests/third-party-services.spec.ts            |  30 ++
 89 files changed, 3445 insertions(+), 893 deletions(-)
 create mode 100644 prisma/migrations/20260526215009_add_listing_query_indexes/migration.sql
 create mode 100644 prisma/seeders/customFieldCategoryUtils.ts
 create mode 100644 prisma/seeders/customFieldValueSeederUtils.ts
 create mode 100644 prisma/seeders/pcListingsSeeder.ts
 create mode 100644 src/app/listings/components/filters/AsyncDeviceFilterSelect.test.tsx
 create mode 100644 src/app/listings/components/filters/AsyncDeviceFilterSelect.tsx
 create mode 100644 src/app/listings/components/filters/AsyncSocFilterSelect.test.tsx
 create mode 100644 src/app/listings/components/filters/AsyncSocFilterSelect.tsx
 create mode 100644 src/app/pc-listings/components/filters/AsyncCpuFilterSelect.test.tsx
 create mode 100644 src/app/pc-listings/components/filters/AsyncCpuFilterSelect.tsx
 create mode 100644 src/app/pc-listings/components/filters/AsyncGpuFilterSelect.test.tsx
 create mode 100644 src/app/pc-listings/components/filters/AsyncGpuFilterSelect.tsx
 delete mode 100644 src/components/ui/form/AsyncCpuMultiSelect.tsx
 delete mode 100644 src/components/ui/form/AsyncDeviceMultiSelect.tsx
 delete mode 100644 src/components/ui/form/AsyncGpuMultiSelect.tsx
 delete mode 100644 src/components/ui/form/AsyncMultiSelect.tsx
 delete mode 100644 src/components/ui/form/AsyncSocMultiSelect.tsx
 create mode 100644 src/components/ui/form/async-multi-select/AsyncMultiSelect.test.tsx
 create mode 100644 src/instrumentation-client.test.ts
 create mode 100644 src/lib/analytics/utils/sendAnalyticsEvent.test.ts
 create mode 100644 src/lib/env.test.ts
 create mode 100644 src/sentry-config.test.ts
 create mode 100644 tests/third-party-services.spec.ts

diff --git a/.env.docker.example b/.env.docker.example
index 7a7486e7f..79fea89fb 100644
--- a/.env.docker.example
+++ b/.env.docker.example
@@ -61,9 +61,14 @@ NEXT_PUBLIC_GA_ID="your_google_analytics_id_here"
 # APPLICATION CONFIGURATION
 # ===========================================
 # Public app configuration
+# local | test | preview | staging | production
+NEXT_PUBLIC_APP_ENV=local
 NEXT_PUBLIC_LOCAL_STORAGE_PREFIX="@DockerEmuReady_"
 NEXT_PUBLIC_EMUREADY_BETA_URL="https://play.google.com/store/apps/details?id=com.producdevity.emureadyapp"
 NEXT_PUBLIC_VERCEL_ANALYTICS_ENABLED=false
+NEXT_PUBLIC_ENABLE_ANALYTICS=false
+NEXT_PUBLIC_ENABLE_KOFI_WIDGET=false
+NEXT_PUBLIC_ENABLE_SENTRY=false
 NEXT_PUBLIC_DISCORD_LINK="https://discord.gg/CYhCzApXav"
 NEXT_PUBLIC_PATREON_LINK="https://www.patreon.com/Producdevity"
 NEXT_PUBLIC_KOFI_LINK="https://ko-fi.com/producdevity"
diff --git a/.env.example b/.env.example
index d52138dd4..7fe59b0d7 100644
--- a/.env.example
+++ b/.env.example
@@ -34,10 +34,15 @@ EMAIL_FROM_NAME="EmuReady Team"
 # EMAIL_API_KEY="your-mailersend-api-key-here"
 
 # Public Variables
+# local | test | preview | staging | production
+NEXT_PUBLIC_APP_ENV=local
 NEXT_PUBLIC_GA_ID="Google-Analytics-ID"
 NEXT_PUBLIC_LOCAL_STORAGE_PREFIX="@LocalEmuReady_"
 NEXT_PUBLIC_EMUREADY_BETA_URL="https://play.google.com/store/apps/details?id=com.producdevity.emureadyapp"
 NEXT_PUBLIC_VERCEL_ANALYTICS_ENABLED=false
+NEXT_PUBLIC_ENABLE_ANALYTICS=false
+NEXT_PUBLIC_ENABLE_KOFI_WIDGET=false
+NEXT_PUBLIC_ENABLE_SENTRY=false
 NEXT_PUBLIC_DISCORD_LINK="https://discord.gg/CYhCzApXav"
 NEXT_PUBLIC_PATREON_LINK="https://www.patreon.com/Producdevity"
 NEXT_PUBLIC_KOFI_LINK="https://ko-fi.com/producdevity"
diff --git a/.env.test.example b/.env.test.example
index b627bb17c..093105bf0 100644
--- a/.env.test.example
+++ b/.env.test.example
@@ -26,10 +26,14 @@ EMAIL_FROM_NAME="EmuReady Test Team"
 # Services
 
 RAWG_API_KEY=""
+NEXT_PUBLIC_APP_ENV=test
 NEXT_PUBLIC_GA_ID=""
 NEXT_PUBLIC_LOCAL_STORAGE_PREFIX="@TestEmuReady_"
 NEXT_PUBLIC_EMUREADY_BETA_URL="https://play.google.com/store/apps/details?id=com.producdevity.emureadyapp"
 NEXT_PUBLIC_VERCEL_ANALYTICS_ENABLED=false
+NEXT_PUBLIC_ENABLE_ANALYTICS=false
+NEXT_PUBLIC_ENABLE_KOFI_WIDGET=false
+NEXT_PUBLIC_ENABLE_SENTRY=false
 NEXT_PUBLIC_DISCORD_LINK="https://discord.gg/CYhCzApXav"
 NEXT_PUBLIC_PATREON_LINK="https://www.patreon.com/Producdevity"
 NEXT_PUBLIC_KOFI_LINK="https://ko-fi.com/producdevity"
diff --git a/next.config.ts b/next.config.ts
index 714a57299..bb0d6ef1a 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -6,6 +6,7 @@ import type { Configuration as WebpackConfiguration } from 'webpack'
 type Header = Awaited<ReturnType<NonNullable<NextConfig['headers']>>>[number]
 
 const isVercelBuild = process.env.VERCEL === '1'
+const isSentryEnabled = process.env.NEXT_PUBLIC_ENABLE_SENTRY === 'true'
 
 const contentSecurityPolicyDirectives = [
   {
@@ -359,7 +360,9 @@ const withBundleAnalyzer = NextBundleAnalyzer({
   enabled: process.env.ANALYZE === 'true',
 })
 
-export default withSentryConfig(withBundleAnalyzer(nextConfig), {
+const analyzedConfig = withBundleAnalyzer(nextConfig)
+
+const sentryBuildOptions = {
   // For all available options, see:
   // https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/build/
 
@@ -385,4 +388,10 @@ export default withSentryConfig(withBundleAnalyzer(nextConfig), {
   bundleSizeOptimizations: {
     excludeDebugStatements: true,
   },
-})
+
+  telemetry: false,
+}
+
+export default isSentryEnabled
+  ? withSentryConfig(analyzedConfig, sentryBuildOptions)
+  : analyzedConfig
diff --git a/playwright.config.ts b/playwright.config.ts
index 710a5e4e1..99c81291c 100644
--- a/playwright.config.ts
+++ b/playwright.config.ts
@@ -16,7 +16,14 @@ function createWebServerEnv(): { [key: string]: string } {
   for (const [key, value] of Object.entries(process.env)) {
     if (typeof value === 'string') env[key] = value
   }
-  env.NODE_ENV ||= 'test'
+  env.NODE_ENV = 'test'
+  env.NEXT_PUBLIC_APP_ENV = 'test'
+  env.NEXT_PUBLIC_ENABLE_ANALYTICS = 'false'
+  env.NEXT_PUBLIC_ENABLE_KOFI_WIDGET = 'false'
+  env.NEXT_PUBLIC_ENABLE_SENTRY = 'false'
+  env.NEXT_PUBLIC_VERCEL_ANALYTICS_ENABLED = 'false'
+  env.NEXT_PUBLIC_DISABLE_COOKIE_BANNER = 'true'
+  env.PLAYWRIGHT_TEST = 'true'
   return env
 }
 
diff --git a/prisma/migrations/20260526215009_add_listing_query_indexes/migration.sql b/prisma/migrations/20260526215009_add_listing_query_indexes/migration.sql
new file mode 100644
index 000000000..9a46ee142
--- /dev/null
+++ b/prisma/migrations/20260526215009_add_listing_query_indexes/migration.sql
@@ -0,0 +1,32 @@
+-- CreateIndex
+CREATE INDEX "Listing_status_createdAt_deviceId_idx" ON "Listing"("status", "createdAt", "deviceId");
+
+-- CreateIndex
+CREATE INDEX "Listing_deviceId_status_createdAt_idx" ON "Listing"("deviceId", "status", "createdAt");
+
+-- CreateIndex
+CREATE INDEX "Listing_gameId_status_createdAt_idx" ON "Listing"("gameId", "status", "createdAt");
+
+-- CreateIndex
+CREATE INDEX "Listing_emulatorId_status_createdAt_idx" ON "Listing"("emulatorId", "status", "createdAt");
+
+-- CreateIndex
+CREATE INDEX "Listing_authorId_status_createdAt_idx" ON "Listing"("authorId", "status", "createdAt");
+
+-- CreateIndex
+CREATE INDEX "pc_listings_status_createdAt_cpuId_idx" ON "pc_listings"("status", "createdAt", "cpuId");
+
+-- CreateIndex
+CREATE INDEX "pc_listings_cpuId_status_createdAt_idx" ON "pc_listings"("cpuId", "status", "createdAt");
+
+-- CreateIndex
+CREATE INDEX "pc_listings_gpuId_status_createdAt_idx" ON "pc_listings"("gpuId", "status", "createdAt");
+
+-- CreateIndex
+CREATE INDEX "pc_listings_gameId_status_createdAt_idx" ON "pc_listings"("gameId", "status", "createdAt");
+
+-- CreateIndex
+CREATE INDEX "pc_listings_emulatorId_status_createdAt_idx" ON "pc_listings"("emulatorId", "status", "createdAt");
+
+-- CreateIndex
+CREATE INDEX "pc_listings_authorId_status_createdAt_idx" ON "pc_listings"("authorId", "status", "createdAt");
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 7eef08ba8..e7c9bf4f1 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -483,6 +483,11 @@ model Listing {
   pinnedByUser           User?                           @relation("ListingPinnedBy", fields: [pinnedByUserId], references: [id], onDelete: SetNull)
 
   @@index([processedByUserId])
+  @@index([status, createdAt, deviceId])
+  @@index([deviceId, status, createdAt])
+  @@index([gameId, status, createdAt])
+  @@index([emulatorId, status, createdAt])
+  @@index([authorId, status, createdAt])
   @@index([successRate, voteCount])
   @@index([voteCount])
   @@index([pinnedCommentId])
@@ -1022,6 +1027,12 @@ model PcListing {
 
   @@index([processedByUserId])
   @@index([status])
+  @@index([status, createdAt, cpuId])
+  @@index([cpuId, status, createdAt])
+  @@index([gpuId, status, createdAt])
+  @@index([gameId, status, createdAt])
+  @@index([emulatorId, status, createdAt])
+  @@index([authorId, status, createdAt])
   @@index([gameId])
   @@index([emulatorId])
   @@index([cpuId])
diff --git a/prisma/seed.ts b/prisma/seed.ts
index f0b89bd9e..c6febfb04 100644
--- a/prisma/seed.ts
+++ b/prisma/seed.ts
@@ -10,6 +10,7 @@ import gamenativeCustomFieldsSeeder from './seeders/gamenativeCustomFieldsSeeder
 import gamesSeeder from './seeders/gamesSeeder'
 import gpuSeeder from './seeders/gpuSeeder'
 import listingsSeeder from './seeders/listingsSeeder'
+import pcListingsSeeder from './seeders/pcListingsSeeder'
 import performanceScalesSeeder from './seeders/performanceScalesSeeder'
 import permissionsSeeder from './seeders/permissionsSeeder'
 import socSeeder from './seeders/socSeeder'
@@ -66,6 +67,7 @@ async function main() {
   const seedDevicesOnly = args.includes('--devices-only')
   const seedEmulatorsOnly = args.includes('--emulators-only')
   const seedCustomFieldsOnly = args.includes('--custom-fields-only')
+  const seedPcListingsOnly = args.includes('--pc-listings-only')
   const seedCpusOnly = args.includes('--cpus-only')
   const seedGpusOnly = args.includes('--gpus-only')
 
@@ -156,6 +158,20 @@ async function main() {
     return
   }
 
+  if (seedPcListingsOnly) {
+    console.info('🌱 Seeding PC listings only...')
+    try {
+      await pcListingsSeeder(prisma)
+      console.info('✅ PC listings seeded successfully!')
+    } catch (error) {
+      console.error('❌ Error seeding PC listings:', error)
+      throw error
+    } finally {
+      await prisma.$disconnect()
+    }
+    return
+  }
+
   if (seedGpusOnly) {
     console.info('🌱 Seeding GPUs only...')
     try {
@@ -200,6 +216,7 @@ async function main() {
     await devicesSeeder(prisma)
     await gamesSeeder(prisma)
     await listingsSeeder(prisma)
+    await pcListingsSeeder(prisma)
 
     console.info('✅ Database seeded successfully!')
   } catch (error) {
diff --git a/prisma/seeders/azaharCustomFieldsSeeder.ts b/prisma/seeders/azaharCustomFieldsSeeder.ts
index 253f34d60..4576ce8e4 100644
--- a/prisma/seeders/azaharCustomFieldsSeeder.ts
+++ b/prisma/seeders/azaharCustomFieldsSeeder.ts
@@ -1,4 +1,5 @@
 import { CustomFieldType, Prisma, type PrismaClient } from '@orm/client'
+import { syncCustomFieldCategories, type CustomFieldCategorySeed } from './customFieldCategoryUtils'
 
 interface SelectOption {
   value: string
@@ -26,6 +27,41 @@ interface AzaharCustomFieldSeed {
 
 const AZAHAR_EMULATOR_NAME = 'Azahar'
 
+const AZAHAR_CUSTOM_FIELD_CATEGORIES: CustomFieldCategorySeed[] = [
+  { name: 'General', displayOrder: 0 },
+  { name: 'Graphics', displayOrder: 1 },
+  { name: 'CPU', displayOrder: 2 },
+  { name: 'Advanced', displayOrder: 3 },
+  { name: 'Media', displayOrder: 4 },
+]
+
+const AZAHAR_FIELD_CATEGORY_CONFIG: Record<
+  string,
+  { categoryName: string; categoryOrder: number }
+> = {
+  emulator_version: { categoryName: 'General', categoryOrder: 0 },
+  game_version: { categoryName: 'General', categoryOrder: 1 },
+  average_fps: { categoryName: 'General', categoryOrder: 2 },
+  dynamic_driver_version: { categoryName: 'Graphics', categoryOrder: 0 },
+  graphics_api: { categoryName: 'Graphics', categoryOrder: 1 },
+  enable_spiri_v_shader_generation: { categoryName: 'Graphics', categoryOrder: 2 },
+  disable_spir_v_optimizer: { categoryName: 'Graphics', categoryOrder: 3 },
+  enable_async_shader_complication: { categoryName: 'Graphics', categoryOrder: 4 },
+  internal_resolution: { categoryName: 'Graphics', categoryOrder: 5 },
+  linear_filtering: { categoryName: 'Graphics', categoryOrder: 6 },
+  accurate_multiplication: { categoryName: 'Graphics', categoryOrder: 7 },
+  disk_shader_cache: { categoryName: 'Graphics', categoryOrder: 8 },
+  texture_filter: { categoryName: 'Graphics', categoryOrder: 9 },
+  stereoscopic_3d_mode: { categoryName: 'Graphics', categoryOrder: 10 },
+  enable_hardware_shader: { categoryName: 'Graphics', categoryOrder: 11 },
+  enable_vsync: { categoryName: 'Graphics', categoryOrder: 12 },
+  cpu_jit: { categoryName: 'CPU', categoryOrder: 0 },
+  delay_game_render_thread: { categoryName: 'Advanced', categoryOrder: 0 },
+  delay_start_with_lle_modules: { categoryName: 'Advanced', categoryOrder: 1 },
+  media_url: { categoryName: 'Media', categoryOrder: 0 },
+  youtube: { categoryName: 'Media', categoryOrder: 1 },
+}
+
 const AZAHAR_CUSTOM_FIELDS: AzaharCustomFieldSeed[] = [
   {
     name: 'emulator_version',
@@ -252,6 +288,11 @@ export default async function azaharCustomFieldsSeeder(prisma: PrismaClient) {
   }
 
   const fieldNames = AZAHAR_CUSTOM_FIELDS.map((field) => field.name)
+  const categoryIdByName = await syncCustomFieldCategories(
+    prisma,
+    azahar.id,
+    AZAHAR_CUSTOM_FIELD_CATEGORIES,
+  )
 
   for (const field of AZAHAR_CUSTOM_FIELDS) {
     await prisma.customFieldDefinition.upsert({
@@ -261,8 +302,8 @@ export default async function azaharCustomFieldsSeeder(prisma: PrismaClient) {
           name: field.name,
         },
       },
-      create: buildDefinitionCreate(azahar.id, field),
-      update: buildDefinitionUpdate(field),
+      create: buildDefinitionCreate(azahar.id, field, categoryIdByName),
+      update: buildDefinitionUpdate(field, categoryIdByName),
     })
   }
 
@@ -278,11 +319,17 @@ export default async function azaharCustomFieldsSeeder(prisma: PrismaClient) {
   )
 }
 
-function buildDefinitionCreate(emulatorId: string, field: AzaharCustomFieldSeed) {
+function buildDefinitionCreate(
+  emulatorId: string,
+  field: AzaharCustomFieldSeed,
+  categoryIdByName: ReadonlyMap<string, string>,
+) {
   const { options, range, defaultValue, placeholder, ...base } = field
+  const categoryConfig = AZAHAR_FIELD_CATEGORY_CONFIG[field.name]
 
   return {
     emulatorId,
+    categoryId: resolveCategoryId(field.name, categoryConfig?.categoryName, categoryIdByName),
     name: base.name,
     label: base.label,
     type: base.type,
@@ -295,13 +342,19 @@ function buildDefinitionCreate(emulatorId: string, field: AzaharCustomFieldSeed)
     rangeUnit: range?.unit ?? null,
     isRequired: base.required,
     displayOrder: base.displayOrder,
+    categoryOrder: categoryConfig?.categoryOrder ?? 0,
   }
 }
 
-function buildDefinitionUpdate(field: AzaharCustomFieldSeed) {
+function buildDefinitionUpdate(
+  field: AzaharCustomFieldSeed,
+  categoryIdByName: ReadonlyMap<string, string>,
+) {
   const { options, range, defaultValue, placeholder, ...base } = field
+  const categoryConfig = AZAHAR_FIELD_CATEGORY_CONFIG[field.name]
 
   return {
+    categoryId: resolveCategoryId(field.name, categoryConfig?.categoryName, categoryIdByName),
     label: base.label,
     type: base.type,
     options: normalizeJsonInput(options),
@@ -313,9 +366,25 @@ function buildDefinitionUpdate(field: AzaharCustomFieldSeed) {
     rangeUnit: range?.unit ?? null,
     isRequired: base.required,
     displayOrder: base.displayOrder,
+    categoryOrder: categoryConfig?.categoryOrder ?? 0,
   }
 }
 
+function resolveCategoryId(
+  fieldName: string,
+  categoryName: string | undefined,
+  categoryIdByName: ReadonlyMap<string, string>,
+) {
+  if (!categoryName) return null
+
+  const categoryId = categoryIdByName.get(categoryName)
+  if (!categoryId) {
+    throw new Error(`Missing Azahar custom field category "${categoryName}" for ${fieldName}`)
+  }
+
+  return categoryId
+}
+
 function normalizeJsonInput(
   value: unknown,
 ): Prisma.NullableJsonNullValueInput | Prisma.InputJsonValue | undefined {
diff --git a/prisma/seeders/customFieldCategoryUtils.ts b/prisma/seeders/customFieldCategoryUtils.ts
new file mode 100644
index 000000000..9b111c79f
--- /dev/null
+++ b/prisma/seeders/customFieldCategoryUtils.ts
@@ -0,0 +1,41 @@
+import type { PrismaClient } from '@orm/client'
+
+export interface CustomFieldCategorySeed {
+  name: string
+  displayOrder: number
+}
+
+export async function syncCustomFieldCategories(
+  prisma: PrismaClient,
+  emulatorId: string,
+  categories: readonly CustomFieldCategorySeed[],
+) {
+  const categoryIdByName = new Map<string, string>()
+
+  for (const category of categories) {
+    const syncedCategory = await prisma.customFieldCategory.upsert({
+      where: {
+        emulatorId_name: {
+          emulatorId,
+          name: category.name,
+        },
+      },
+      create: {
+        emulatorId,
+        name: category.name,
+        displayOrder: category.displayOrder,
+      },
+      update: {
+        displayOrder: category.displayOrder,
+      },
+      select: {
+        id: true,
+        name: true,
+      },
+    })
+
+    categoryIdByName.set(syncedCategory.name, syncedCategory.id)
+  }
+
+  return categoryIdByName
+}
diff --git a/prisma/seeders/customFieldValueSeederUtils.ts b/prisma/seeders/customFieldValueSeederUtils.ts
new file mode 100644
index 000000000..a6a1e4ae4
--- /dev/null
+++ b/prisma/seeders/customFieldValueSeederUtils.ts
@@ -0,0 +1,199 @@
+import { CustomFieldType, type Prisma, type PrismaClient } from '@orm/client'
+
+type CustomFieldDefinitionForSeed = {
+  id: string
+  name: string
+  type: CustomFieldType
+  options: Prisma.JsonValue | null
+  defaultValue: Prisma.JsonValue | null
+  rangeMin: number | null
+  rangeMax: number | null
+}
+
+export type CustomFieldDefinitionCache = Map<string, CustomFieldDefinitionForSeed[]>
+
+const CUSTOM_FIELD_VALUE_OVERRIDES: Record<string, Prisma.InputJsonValue[]> = {
+  emulator_version: ['2123.2', '0.0.2-pre-alpha', '1.0.0'],
+  dynamic_driver_version: ['Turnip v24.3.0 R10', 'Mesa Turnip Adreno 24.2', 'System default'],
+  graphics_driver: ['Turnip (Adreno)', 'Vortek (Universal)', 'VirGL (Universal)'],
+  graphics_api: ['Vulkan', 'OpenGLES'],
+  gpu_api: ['Vulkan', 'OpenGL'],
+  resolution: ['1280x720 (16:9)', '2x', 'Native'],
+  internal_resolution: ['native', '2', '3'],
+  average_fps: ['58', '45', '30'],
+  game_version: ['1.0.0', '1.1.0', 'Latest'],
+  youtube: ['https://www.youtube.com/watch?v=dQw4w9WgXcQ'],
+  media_url: ['https://example.com/emuready-seed-screenshot'],
+  dx_wrapper_config: ['DXVK async enabled, 60 FPS cap, 4096 MB max memory'],
+  env_variables: ['MESA_GL_VERSION_OVERRIDE=4.6\nBOX64_DYNAREC_SAFEFLAGS=1'],
+  exec_arguments: ['-skipinitialbootstrap -nocrashmonitor'],
+}
+
+export function createCustomFieldDefinitionCache(): CustomFieldDefinitionCache {
+  return new Map<string, CustomFieldDefinitionForSeed[]>()
+}
+
+export async function seedListingCustomFieldValues(
+  prisma: PrismaClient,
+  definitionCache: CustomFieldDefinitionCache,
+  listingId: string,
+  emulatorId: string,
+  seedIndex: number,
+): Promise<number> {
+  const definitions = await getCustomFieldDefinitions(prisma, definitionCache, emulatorId)
+  let createdOrUpdated = 0
+
+  for (const definition of definitions) {
+    const value = getSeedValueForCustomField(definition, seedIndex)
+    if (value === undefined) continue
+
+    await prisma.listingCustomFieldValue.upsert({
+      where: {
+        listingId_customFieldDefinitionId: {
+          listingId,
+          customFieldDefinitionId: definition.id,
+        },
+      },
+      create: {
+        listingId,
+        customFieldDefinitionId: definition.id,
+        value,
+      },
+      update: { value },
+    })
+    createdOrUpdated += 1
+  }
+
+  return createdOrUpdated
+}
+
+export async function seedPcListingCustomFieldValues(
+  prisma: PrismaClient,
+  definitionCache: CustomFieldDefinitionCache,
+  pcListingId: string,
+  emulatorId: string,
+  seedIndex: number,
+): Promise<number> {
+  const definitions = await getCustomFieldDefinitions(prisma, definitionCache, emulatorId)
+  let createdOrUpdated = 0
+
+  for (const definition of definitions) {
+    const value = getSeedValueForCustomField(definition, seedIndex)
+    if (value === undefined) continue
+
+    await prisma.pcListingCustomFieldValue.upsert({
+      where: {
+        pcListingId_customFieldDefinitionId: {
+          pcListingId,
+          customFieldDefinitionId: definition.id,
+        },
+      },
+      create: {
+        pcListingId,
+        customFieldDefinitionId: definition.id,
+        value,
+      },
+      update: { value },
+    })
+    createdOrUpdated += 1
+  }
+
+  return createdOrUpdated
+}
+
+async function getCustomFieldDefinitions(
+  prisma: PrismaClient,
+  definitionCache: CustomFieldDefinitionCache,
+  emulatorId: string,
+) {
+  const cachedDefinitions = definitionCache.get(emulatorId)
+  if (cachedDefinitions) return cachedDefinitions
+
+  const definitions = await prisma.customFieldDefinition.findMany({
+    where: { emulatorId },
+    select: {
+      id: true,
+      name: true,
+      type: true,
+      options: true,
+      defaultValue: true,
+      rangeMin: true,
+      rangeMax: true,
+    },
+    orderBy: [{ categoryOrder: 'asc' }, { displayOrder: 'asc' }],
+  })
+
+  definitionCache.set(emulatorId, definitions)
+  return definitions
+}
+
+function getSeedValueForCustomField(
+  definition: CustomFieldDefinitionForSeed,
+  seedIndex: number,
+): Prisma.InputJsonValue | undefined {
+  const override = getOverrideValue(definition, seedIndex)
+  if (override !== undefined) return override
+
+  if (isJsonPrimitive(definition.defaultValue)) {
+    return definition.defaultValue
+  }
+
+  switch (definition.type) {
+    case CustomFieldType.BOOLEAN:
+      return seedIndex % 2 === 0
+    case CustomFieldType.RANGE:
+      return getRangeValue(definition)
+    case CustomFieldType.SELECT:
+      return getFirstSelectOptionValue(definition.options)
+    case CustomFieldType.URL:
+      return `https://example.com/${definition.name}`
+    case CustomFieldType.TEXTAREA:
+      return `Seeded ${definition.name.replaceAll('_', ' ')} configuration.`
+    case CustomFieldType.TEXT:
+      return `Seeded ${definition.name.replaceAll('_', ' ')}`
+  }
+}
+
+function getOverrideValue(
+  definition: CustomFieldDefinitionForSeed,
+  seedIndex: number,
+): Prisma.InputJsonValue | undefined {
+  const values = CUSTOM_FIELD_VALUE_OVERRIDES[definition.name]
+  if (!values || values.length === 0) return undefined
+
+  const value = values[seedIndex % values.length]
+  if (definition.type !== CustomFieldType.SELECT) return value
+
+  const optionValues = getSelectOptionValues(definition.options)
+  if (optionValues.length === 0) return value
+  return typeof value === 'string' && optionValues.includes(value) ? value : optionValues[0]
+}
+
+function getRangeValue(definition: CustomFieldDefinitionForSeed): number {
+  if (typeof definition.defaultValue === 'number') return definition.defaultValue
+
+  const min = definition.rangeMin ?? 0
+  const max = definition.rangeMax ?? min
+  return Math.round((min + max) / 2)
+}
+
+function getFirstSelectOptionValue(options: Prisma.JsonValue | null): string | undefined {
+  return getSelectOptionValues(options)[0]
+}
+
+function getSelectOptionValues(options: Prisma.JsonValue | null): string[] {
+  if (!Array.isArray(options)) return []
+
+  return options.flatMap((option) => {
+    if (!isJsonObject(option)) return []
+    return typeof option.value === 'string' ? [option.value] : []
+  })
+}
+
+function isJsonPrimitive(value: Prisma.JsonValue | null): value is string | number | boolean {
+  return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean'
+}
+
+function isJsonObject(value: Prisma.JsonValue): value is Prisma.JsonObject {
+  return typeof value === 'object' && value !== null && !Array.isArray(value)
+}
diff --git a/prisma/seeders/edenCustomFieldsSeeder.ts b/prisma/seeders/edenCustomFieldsSeeder.ts
index befa8ef71..a9aded71b 100644
--- a/prisma/seeders/edenCustomFieldsSeeder.ts
+++ b/prisma/seeders/edenCustomFieldsSeeder.ts
@@ -1,4 +1,5 @@
 import { CustomFieldType, type Prisma, type PrismaClient } from '@orm/client'
+import { syncCustomFieldCategories, type CustomFieldCategorySeed } from './customFieldCategoryUtils'
 
 interface SelectOption {
   value: string
@@ -18,6 +19,8 @@ interface EdenCustomFieldSeed {
   type: CustomFieldType
   required: boolean
   displayOrder: number
+  categoryName?: string | null
+  categoryOrder?: number
   defaultValue?: string | number | boolean | null
   placeholder?: string | null
   options?: SelectOption[]
@@ -26,6 +29,14 @@ interface EdenCustomFieldSeed {
 
 const EDEN_EMULATOR_NAME = 'Eden'
 
+const EDEN_CUSTOM_FIELD_CATEGORIES: CustomFieldCategorySeed[] = [
+  { name: 'General', displayOrder: 0 },
+  { name: 'Graphics', displayOrder: 1 },
+  { name: 'Debug', displayOrder: 2 },
+  { name: 'CPU', displayOrder: 3 },
+  { name: 'Eden Veil', displayOrder: 4 },
+]
+
 const EDEN_CUSTOM_FIELDS: EdenCustomFieldSeed[] = [
   {
     name: 'emulator_version',
@@ -35,6 +46,8 @@ const EDEN_CUSTOM_FIELDS: EdenCustomFieldSeed[] = [
     defaultValue: null,
     placeholder: '0.0.2-pre-alpha',
     displayOrder: 0,
+    categoryName: 'Graphics',
+    categoryOrder: 4,
   },
   {
     name: 'dynamic_driver_version',
@@ -44,6 +57,8 @@ const EDEN_CUSTOM_FIELDS: EdenCustomFieldSeed[] = [
     defaultValue: null,
     placeholder: 'Select first options for non-Android devices',
     displayOrder: 1,
+    categoryName: 'Eden Veil',
+    categoryOrder: 5,
   },
   {
     name: 'accuracy_level',
@@ -57,6 +72,8 @@ const EDEN_CUSTOM_FIELDS: EdenCustomFieldSeed[] = [
       { value: 'Extreme (Slow)', label: 'Extreme (Slow)' },
     ],
     displayOrder: 2,
+    categoryName: 'CPU',
+    categoryOrder: 2,
   },
   {
     name: 'resolution',
@@ -65,6 +82,8 @@ const EDEN_CUSTOM_FIELDS: EdenCustomFieldSeed[] = [
     required: true,
     defaultValue: null,
     displayOrder: 3,
+    categoryName: 'General',
+    categoryOrder: 0,
   },
   {
     name: 'vsync_mode',
@@ -80,6 +99,8 @@ const EDEN_CUSTOM_FIELDS: EdenCustomFieldSeed[] = [
       { value: 'N/A', label: 'N/A' },
     ],
     displayOrder: 4,
+    categoryName: 'Graphics',
+    categoryOrder: 5,
   },
   {
     name: 'window_adapting_filter',
@@ -98,6 +119,8 @@ const EDEN_CUSTOM_FIELDS: EdenCustomFieldSeed[] = [
       { value: 'Other', label: 'Other' },
     ],
     displayOrder: 5,
+    categoryName: 'Graphics',
+    categoryOrder: 3,
   },
   {
     name: 'anti_aliasing_method',
@@ -112,6 +135,8 @@ const EDEN_CUSTOM_FIELDS: EdenCustomFieldSeed[] = [
       { value: 'Other', label: 'Other' },
     ],
     displayOrder: 6,
+    categoryName: 'General',
+    categoryOrder: 0,
   },
   {
     name: 'anisotropic_filtering',
@@ -128,6 +153,8 @@ const EDEN_CUSTOM_FIELDS: EdenCustomFieldSeed[] = [
       { value: '16x', label: '16x' },
     ],
     displayOrder: 7,
+    categoryName: 'Graphics',
+    categoryOrder: 8,
   },
   {
     name: 'disk_shader_cache',
@@ -136,6 +163,8 @@ const EDEN_CUSTOM_FIELDS: EdenCustomFieldSeed[] = [
     required: true,
     defaultValue: null,
     displayOrder: 8,
+    categoryName: 'Eden Veil',
+    categoryOrder: 3,
   },
   {
     name: 'use_async_shaders',
@@ -144,6 +173,8 @@ const EDEN_CUSTOM_FIELDS: EdenCustomFieldSeed[] = [
     required: true,
     defaultValue: null,
     displayOrder: 9,
+    categoryName: 'Eden Veil',
+    categoryOrder: 4,
   },
   {
     name: 'use_reactive_flushing',
@@ -152,6 +183,8 @@ const EDEN_CUSTOM_FIELDS: EdenCustomFieldSeed[] = [
     required: true,
     defaultValue: null,
     displayOrder: 10,
+    categoryName: 'Graphics',
+    categoryOrder: 6,
   },
   {
     name: 'docked_mode',
@@ -160,6 +193,8 @@ const EDEN_CUSTOM_FIELDS: EdenCustomFieldSeed[] = [
     required: true,
     defaultValue: null,
     displayOrder: 11,
+    categoryName: 'Eden Veil',
+    categoryOrder: 6,
   },
   {
     name: 'audio_output_engine',
@@ -174,6 +209,8 @@ const EDEN_CUSTOM_FIELDS: EdenCustomFieldSeed[] = [
       { value: 'Null', label: 'Null' },
     ],
     displayOrder: 12,
+    categoryName: 'Graphics',
+    categoryOrder: 7,
   },
   {
     name: 'gpu_api',
@@ -187,6 +224,8 @@ const EDEN_CUSTOM_FIELDS: EdenCustomFieldSeed[] = [
       { value: 'Other', label: 'Other' },
     ],
     displayOrder: 13,
+    categoryName: 'General',
+    categoryOrder: 1,
   },
   {
     name: 'cpu_accuracy',
@@ -201,6 +240,8 @@ const EDEN_CUSTOM_FIELDS: EdenCustomFieldSeed[] = [
       { value: 'Paranoid (Slow)', label: 'Paranoid (Slow)' },
     ],
     displayOrder: 14,
+    categoryName: 'General',
+    categoryOrder: 2,
   },
   {
     name: 'cpu_backend',
@@ -213,6 +254,8 @@ const EDEN_CUSTOM_FIELDS: EdenCustomFieldSeed[] = [
       { value: 'Native code execution (NCE)', label: 'Native code execution (NCE)' },
     ],
     displayOrder: 15,
+    categoryName: 'CPU',
+    categoryOrder: 0,
   },
   {
     name: 'extended_dynamic_state',
@@ -222,6 +265,8 @@ const EDEN_CUSTOM_FIELDS: EdenCustomFieldSeed[] = [
     defaultValue: 0,
     range: { min: 0, max: 3, unit: '', decimals: 0 },
     displayOrder: 16,
+    categoryName: 'CPU',
+    categoryOrder: 1,
   },
   {
     name: 'provoking_vertex',
@@ -230,6 +275,8 @@ const EDEN_CUSTOM_FIELDS: EdenCustomFieldSeed[] = [
     required: true,
     defaultValue: false,
     displayOrder: 17,
+    categoryName: 'Eden Veil',
+    categoryOrder: 7,
   },
   {
     name: 'descriptor_indexing',
@@ -238,6 +285,8 @@ const EDEN_CUSTOM_FIELDS: EdenCustomFieldSeed[] = [
     required: true,
     defaultValue: false,
     displayOrder: 18,
+    categoryName: 'Eden Veil',
+    categoryOrder: 8,
   },
   {
     name: 'enhanced_frame_pacing',
@@ -246,6 +295,8 @@ const EDEN_CUSTOM_FIELDS: EdenCustomFieldSeed[] = [
     required: true,
     defaultValue: true,
     displayOrder: 19,
+    categoryName: 'Eden Veil',
+    categoryOrder: 9,
   },
   {
     name: 'use_fast_gpu_time',
@@ -254,6 +305,8 @@ const EDEN_CUSTOM_FIELDS: EdenCustomFieldSeed[] = [
     required: true,
     defaultValue: false,
     displayOrder: 20,
+    categoryName: 'General',
+    categoryOrder: 2,
   },
   {
     name: 'nvdec_emulation',
@@ -267,6 +320,8 @@ const EDEN_CUSTOM_FIELDS: EdenCustomFieldSeed[] = [
       { value: 'GPU', label: 'GPU' },
     ],
     displayOrder: 21,
+    categoryName: 'Debug',
+    categoryOrder: 0,
   },
   {
     name: 'astc_recompression_method',
@@ -280,6 +335,8 @@ const EDEN_CUSTOM_FIELDS: EdenCustomFieldSeed[] = [
       { value: 'BC3 (Medium Quality)', label: 'BC3 (Medium Quality)' },
     ],
     displayOrder: 22,
+    categoryName: 'CPU',
+    categoryOrder: 3,
   },
   {
     name: 'vram_usage_mode',
@@ -292,6 +349,8 @@ const EDEN_CUSTOM_FIELDS: EdenCustomFieldSeed[] = [
       { value: 'Aggressive', label: 'Aggressive' },
     ],
     displayOrder: 23,
+    categoryName: 'General',
+    categoryOrder: 3,
   },
   {
     name: 'optimize_spirv_output',
@@ -305,6 +364,8 @@ const EDEN_CUSTOM_FIELDS: EdenCustomFieldSeed[] = [
       { value: 'Always', label: 'Always' },
     ],
     displayOrder: 24,
+    categoryName: 'Eden Veil',
+    categoryOrder: 10,
   },
   {
     name: 'fast_cpu_time',
@@ -313,6 +374,8 @@ const EDEN_CUSTOM_FIELDS: EdenCustomFieldSeed[] = [
     required: true,
     defaultValue: false,
     displayOrder: 25,
+    categoryName: 'CPU',
+    categoryOrder: 4,
   },
   {
     name: 'enable_lru_cache',
@@ -321,6 +384,8 @@ const EDEN_CUSTOM_FIELDS: EdenCustomFieldSeed[] = [
     required: true,
     defaultValue: false,
     displayOrder: 26,
+    categoryName: 'Debug',
+    categoryOrder: 1,
   },
   {
     name: 'synchronize_core_speed',
@@ -378,6 +443,11 @@ export default async function edenCustomFieldsSeeder(prisma: PrismaClient) {
   }
 
   const fieldNames = EDEN_CUSTOM_FIELDS.map((field) => field.name)
+  const categoryIdByName = await syncCustomFieldCategories(
+    prisma,
+    eden.id,
+    EDEN_CUSTOM_FIELD_CATEGORIES,
+  )
 
   for (const field of EDEN_CUSTOM_FIELDS) {
     await prisma.customFieldDefinition.upsert({
@@ -387,8 +457,8 @@ export default async function edenCustomFieldsSeeder(prisma: PrismaClient) {
           name: field.name,
         },
       },
-      create: buildDefinitionCreate(eden.id, field),
-      update: buildDefinitionUpdate(field),
+      create: buildDefinitionCreate(eden.id, field, categoryIdByName),
+      update: buildDefinitionUpdate(field, categoryIdByName),
     })
   }
 
@@ -404,11 +474,16 @@ export default async function edenCustomFieldsSeeder(prisma: PrismaClient) {
   )
 }
 
-function buildDefinitionCreate(emulatorId: string, field: EdenCustomFieldSeed) {
-  const { options, range, defaultValue, placeholder, ...base } = field
+function buildDefinitionCreate(
+  emulatorId: string,
+  field: EdenCustomFieldSeed,
+  categoryIdByName: ReadonlyMap<string, string>,
+) {
+  const { options, range, defaultValue, placeholder, categoryOrder, ...base } = field
 
   return {
     emulatorId,
+    categoryId: resolveCategoryId(field, categoryIdByName),
     name: base.name,
     label: base.label,
     type: base.type,
@@ -421,13 +496,18 @@ function buildDefinitionCreate(emulatorId: string, field: EdenCustomFieldSeed) {
     rangeUnit: range?.unit ?? null,
     isRequired: base.required,
     displayOrder: base.displayOrder,
+    categoryOrder: categoryOrder ?? 0,
   }
 }
 
-function buildDefinitionUpdate(field: EdenCustomFieldSeed) {
-  const { options, range, defaultValue, placeholder, ...base } = field
+function buildDefinitionUpdate(
+  field: EdenCustomFieldSeed,
+  categoryIdByName: ReadonlyMap<string, string>,
+) {
+  const { options, range, defaultValue, placeholder, categoryOrder, ...base } = field
 
   return {
+    categoryId: resolveCategoryId(field, categoryIdByName),
     label: base.label,
     type: base.type,
     options: normalizeJsonInput(options),
@@ -439,7 +519,22 @@ function buildDefinitionUpdate(field: EdenCustomFieldSeed) {
     rangeUnit: range?.unit ?? null,
     isRequired: base.required,
     displayOrder: base.displayOrder,
+    categoryOrder: categoryOrder ?? 0,
+  }
+}
+
+function resolveCategoryId(
+  field: EdenCustomFieldSeed,
+  categoryIdByName: ReadonlyMap<string, string>,
+) {
+  if (!field.categoryName) return null
+
+  const categoryId = categoryIdByName.get(field.categoryName)
+  if (!categoryId) {
+    throw new Error(`Missing Eden custom field category "${field.categoryName}" for ${field.name}`)
   }
+
+  return categoryId
 }
 
 function normalizeJsonInput(
diff --git a/prisma/seeders/gamenativeCustomFieldsSeeder.ts b/prisma/seeders/gamenativeCustomFieldsSeeder.ts
index 495edc455..8ca275a4a 100644
--- a/prisma/seeders/gamenativeCustomFieldsSeeder.ts
+++ b/prisma/seeders/gamenativeCustomFieldsSeeder.ts
@@ -1,4 +1,5 @@
 import { CustomFieldType, Prisma, type PrismaClient } from '@orm/client'
+import { syncCustomFieldCategories, type CustomFieldCategorySeed } from './customFieldCategoryUtils'
 
 interface SelectOption {
   value: string
@@ -18,6 +19,50 @@ interface GameNativeCustomFieldSeed {
 
 const GAMENATIVE_EMULATOR_NAME = 'GameNative'
 
+const GAMENATIVE_CUSTOM_FIELD_CATEGORIES: CustomFieldCategorySeed[] = [
+  { name: 'General', displayOrder: 0 },
+  { name: 'Graphics', displayOrder: 1 },
+  { name: 'Runtime', displayOrder: 2 },
+  { name: 'Input', displayOrder: 3 },
+  { name: 'Media', displayOrder: 4 },
+]
+
+const GAMENATIVE_FIELD_CATEGORY_CONFIG: Record<
+  string,
+  { categoryName: string; categoryOrder: number }
+> = {
+  emulator_version: { categoryName: 'General', categoryOrder: 0 },
+  game_version: { categoryName: 'General', categoryOrder: 1 },
+  average_fps: { categoryName: 'General', categoryOrder: 2 },
+  resolution: { categoryName: 'General', categoryOrder: 3 },
+  audio_driver: { categoryName: 'General', categoryOrder: 4 },
+  graphics_driver: { categoryName: 'Graphics', categoryOrder: 0 },
+  dynamic_driver_version: { categoryName: 'Graphics', categoryOrder: 1 },
+  dx_wrapper: { categoryName: 'Graphics', categoryOrder: 2 },
+  dxvk_version: { categoryName: 'Graphics', categoryOrder: 3 },
+  dx_wrapper_config: { categoryName: 'Graphics', categoryOrder: 4 },
+  max_device_memory: { categoryName: 'Graphics', categoryOrder: 5 },
+  use_adrenotools_turnip: { categoryName: 'Graphics', categoryOrder: 6 },
+  env_variables: { categoryName: 'Runtime', categoryOrder: 0 },
+  box64_version: { categoryName: 'Runtime', categoryOrder: 1 },
+  box64_preset: { categoryName: 'Runtime', categoryOrder: 2 },
+  startup_selection: { categoryName: 'Runtime', categoryOrder: 3 },
+  container_variant: { categoryName: 'Runtime', categoryOrder: 4 },
+  wine_version: { categoryName: 'Runtime', categoryOrder: 5 },
+  steam_type: { categoryName: 'Runtime', categoryOrder: 6 },
+  fex_core_version: { categoryName: 'Runtime', categoryOrder: 7 },
+  '32_bit_emulator': { categoryName: 'Runtime', categoryOrder: 8 },
+  '64_bit_emulator': { categoryName: 'Runtime', categoryOrder: 9 },
+  fex_core_preset: { categoryName: 'Runtime', categoryOrder: 10 },
+  exec_arguments: { categoryName: 'Runtime', categoryOrder: 11 },
+  use_steam_input: { categoryName: 'Input', categoryOrder: 0 },
+  enable_x_input_api: { categoryName: 'Input', categoryOrder: 1 },
+  enable_direct_input_api: { categoryName: 'Input', categoryOrder: 2 },
+  direct_input_mapper_type: { categoryName: 'Input', categoryOrder: 3 },
+  youtube: { categoryName: 'Media', categoryOrder: 0 },
+  media_url: { categoryName: 'Media', categoryOrder: 1 },
+}
+
 const GAMENATIVE_CUSTOM_FIELDS: GameNativeCustomFieldSeed[] = [
   {
     name: 'emulator_version',
@@ -391,6 +436,11 @@ export default async function gamenativeCustomFieldsSeeder(prisma: PrismaClient)
   }
 
   const fieldNames = GAMENATIVE_CUSTOM_FIELDS.map((field) => field.name)
+  const categoryIdByName = await syncCustomFieldCategories(
+    prisma,
+    gamenative.id,
+    GAMENATIVE_CUSTOM_FIELD_CATEGORIES,
+  )
 
   for (const field of GAMENATIVE_CUSTOM_FIELDS) {
     await prisma.customFieldDefinition.upsert({
@@ -400,8 +450,8 @@ export default async function gamenativeCustomFieldsSeeder(prisma: PrismaClient)
           name: field.name,
         },
       },
-      create: buildDefinitionCreate(gamenative.id, field),
-      update: buildDefinitionUpdate(field),
+      create: buildDefinitionCreate(gamenative.id, field, categoryIdByName),
+      update: buildDefinitionUpdate(field, categoryIdByName),
     })
   }
 
@@ -441,11 +491,17 @@ export default async function gamenativeCustomFieldsSeeder(prisma: PrismaClient)
   )
 }
 
-function buildDefinitionCreate(emulatorId: string, field: GameNativeCustomFieldSeed) {
+function buildDefinitionCreate(
+  emulatorId: string,
+  field: GameNativeCustomFieldSeed,
+  categoryIdByName: ReadonlyMap<string, string>,
+) {
   const { options, defaultValue, placeholder, ...base } = field
+  const categoryConfig = GAMENATIVE_FIELD_CATEGORY_CONFIG[field.name]
 
   return {
     emulatorId,
+    categoryId: resolveCategoryId(field.name, categoryConfig?.categoryName, categoryIdByName),
     name: base.name,
     label: base.label,
     type: base.type,
@@ -458,13 +514,19 @@ function buildDefinitionCreate(emulatorId: string, field: GameNativeCustomFieldS
     rangeUnit: null,
     isRequired: base.required,
     displayOrder: base.displayOrder,
+    categoryOrder: categoryConfig?.categoryOrder ?? 0,
   }
 }
 
-function buildDefinitionUpdate(field: GameNativeCustomFieldSeed) {
+function buildDefinitionUpdate(
+  field: GameNativeCustomFieldSeed,
+  categoryIdByName: ReadonlyMap<string, string>,
+) {
   const { options, defaultValue, placeholder, ...base } = field
+  const categoryConfig = GAMENATIVE_FIELD_CATEGORY_CONFIG[field.name]
 
   return {
+    categoryId: resolveCategoryId(field.name, categoryConfig?.categoryName, categoryIdByName),
     label: base.label,
     type: base.type,
     options: normalizeJsonInput(options),
@@ -476,9 +538,25 @@ function buildDefinitionUpdate(field: GameNativeCustomFieldSeed) {
     rangeUnit: null,
     isRequired: base.required,
     displayOrder: base.displayOrder,
+    categoryOrder: categoryConfig?.categoryOrder ?? 0,
   }
 }
 
+function resolveCategoryId(
+  fieldName: string,
+  categoryName: string | undefined,
+  categoryIdByName: ReadonlyMap<string, string>,
+) {
+  if (!categoryName) return null
+
+  const categoryId = categoryIdByName.get(categoryName)
+  if (!categoryId) {
+    throw new Error(`Missing GameNative custom field category "${categoryName}" for ${fieldName}`)
+  }
+
+  return categoryId
+}
+
 function normalizeJsonInput(
   value: unknown,
 ): Prisma.NullableJsonNullValueInput | Prisma.InputJsonValue | undefined {
diff --git a/prisma/seeders/listingsSeeder.ts b/prisma/seeders/listingsSeeder.ts
index f8079c7e8..63356c811 100644
--- a/prisma/seeders/listingsSeeder.ts
+++ b/prisma/seeders/listingsSeeder.ts
@@ -1,4 +1,8 @@
 import { Role, ApprovalStatus, type PrismaClient } from '@orm/client'
+import {
+  createCustomFieldDefinitionCache,
+  seedListingCustomFieldValues,
+} from './customFieldValueSeederUtils'
 
 function getRandomElement<T>(array: T[]): T {
   return array[Math.floor(Math.random() * array.length)]
@@ -86,11 +90,36 @@ const sampleComments = [
   'Anyone tried this with different emulator versions?',
 ]
 
-async function createVotesAndComments(
-  prisma: PrismaClient,
-  listingId: string,
-  users: { id: string; role: Role }[],
-) {
+const customFieldListingFixtures = [
+  {
+    emulatorName: 'Azahar',
+    systemName: 'Nintendo 3DS',
+    notes: 'Azahar fixture with custom graphics and CPU field values.',
+  },
+  {
+    emulatorName: 'Eden',
+    systemName: 'Nintendo Switch',
+    notes: 'Eden fixture with categorized graphics, CPU, and Eden Veil values.',
+  },
+  {
+    emulatorName: 'GameNative',
+    systemName: 'Microsoft Windows',
+    notes: 'GameNative fixture with runtime and graphics compatibility values.',
+  },
+] as const
+
+type SeedUser = { id: string; role: Role }
+type SeedDevice = { id: string; brand: { name: string }; modelName: string }
+type SeedGame = {
+  id: string
+  title: string
+  systemId: string
+  system: { id: string; name: string }
+}
+type SeedEmulator = { id: string; name: string; systems: { id: string }[] }
+type SeedPerformanceScale = { id: number; label: string }
+
+async function createVotesAndComments(prisma: PrismaClient, listingId: string, users: SeedUser[]) {
   // Add some random votes (60% upvotes, 40% downvotes)
   const votersCount = Math.floor(Math.random() * 8) + 2 // 2-9 voters
   const voters = getRandomUniqueElements(users, votersCount)
@@ -154,6 +183,7 @@ async function listingsSeeder(prisma: PrismaClient) {
   })
 
   const performanceScales = await prisma.performanceScale.findMany()
+  const customFieldDefinitionCache = createCustomFieldDefinitionCache()
 
   console.info(
     `📊 Found ${devices.length} devices, ${games.length} games, ${emulators.length} emulators`,
@@ -162,6 +192,7 @@ async function listingsSeeder(prisma: PrismaClient) {
   let totalListingsCreated = 0
   let totalVotesCreated = 0
   let totalCommentsCreated = 0
+  let totalCustomFieldValuesSynced = 0
 
   // Create 2 listings per device
   for (const device of devices) {
@@ -236,6 +267,13 @@ async function listingsSeeder(prisma: PrismaClient) {
         })
 
         totalListingsCreated++
+        totalCustomFieldValuesSynced += await seedListingCustomFieldValues(
+          prisma,
+          customFieldDefinitionCache,
+          listing.id,
+          selectedEmulator.id,
+          totalListingsCreated,
+        )
 
         // Add votes and comments to this listing
         const votesBefore = await prisma.vote.count()
@@ -261,12 +299,91 @@ async function listingsSeeder(prisma: PrismaClient) {
     }
   }
 
+  const fixtureResult = await createCustomFieldListingFixtures({
+    prisma,
+    users,
+    adminUsers,
+    devices,
+    games,
+    emulators,
+    performanceScales,
+    customFieldDefinitionCache,
+    seedIndexStart: totalListingsCreated,
+  })
+  totalListingsCreated += fixtureResult.listingsCreated
+  totalCustomFieldValuesSynced += fixtureResult.customFieldValuesSynced
+
   console.info('✅ Listings seeding completed!')
   console.info(`📈 Statistics:`)
   console.info(`   📝 ${totalListingsCreated} listings created`)
+  console.info(`   ⚙️ ${totalCustomFieldValuesSynced} custom field values synced`)
   console.info(`   👍 ${totalVotesCreated} votes added`)
   console.info(`   💬 ${totalCommentsCreated} comments added`)
   console.info(`   ✅ ~50% of listings are auto-approved for testing`)
 }
 
+interface CustomFieldListingFixtureInput {
+  prisma: PrismaClient
+  users: SeedUser[]
+  adminUsers: SeedUser[]
+  devices: SeedDevice[]
+  games: SeedGame[]
+  emulators: SeedEmulator[]
+  performanceScales: SeedPerformanceScale[]
+  customFieldDefinitionCache: ReturnType<typeof createCustomFieldDefinitionCache>
+  seedIndexStart: number
+}
+
+async function createCustomFieldListingFixtures(input: CustomFieldListingFixtureInput) {
+  let listingsCreated = 0
+  let customFieldValuesSynced = 0
+
+  const approvedPerformance =
+    input.performanceScales.find((scale) => scale.label === 'Great') ?? input.performanceScales[0]
+  const processor = input.adminUsers[0]
+  const author = input.users[0]
+  const device = input.devices[0]
+
+  if (!approvedPerformance || !processor || !author || !device) {
+    console.warn('Missing fixture dependencies, skipping custom field listing fixtures')
+    return { listingsCreated, customFieldValuesSynced }
+  }
+
+  for (const fixture of customFieldListingFixtures) {
+    const emulator = input.emulators.find((candidate) => candidate.name === fixture.emulatorName)
+    const game = input.games.find((candidate) => candidate.system.name === fixture.systemName)
+
+    if (!emulator || !game) {
+      console.warn(`Skipping ${fixture.emulatorName} fixture, missing emulator or game data`)
+      continue
+    }
+
+    const listing = await input.prisma.listing.create({
+      data: {
+        gameId: game.id,
+        deviceId: device.id,
+        emulatorId: emulator.id,
+        performanceId: approvedPerformance.id,
+        notes: fixture.notes,
+        authorId: author.id,
+        status: ApprovalStatus.APPROVED,
+        processedAt: new Date(),
+        processedNotes: 'Seeded custom field fixture.',
+        processedByUserId: processor.id,
+      },
+    })
+
+    listingsCreated += 1
+    customFieldValuesSynced += await seedListingCustomFieldValues(
+      input.prisma,
+      input.customFieldDefinitionCache,
+      listing.id,
+      emulator.id,
+      input.seedIndexStart + listingsCreated,
+    )
+  }
+
+  return { listingsCreated, customFieldValuesSynced }
+}
+
 export default listingsSeeder
diff --git a/prisma/seeders/pcListingsSeeder.ts b/prisma/seeders/pcListingsSeeder.ts
new file mode 100644
index 000000000..00c1b6cc1
--- /dev/null
+++ b/prisma/seeders/pcListingsSeeder.ts
@@ -0,0 +1,180 @@
+import { calculateWilsonScore } from '@/utils/wilson-score'
+import { ApprovalStatus, PcOs, Role, type PrismaClient } from '@orm/client'
+import {
+  createCustomFieldDefinitionCache,
+  seedPcListingCustomFieldValues,
+} from './customFieldValueSeederUtils'
+
+type SeedUser = { id: string; role: Role }
+
+const pcListingFixtures = [
+  {
+    emulatorName: 'Eden',
+    gameTitle: 'Hades',
+    systemName: 'Nintendo Switch',
+    cpuModelName: 'Core i7-12700K',
+    gpuModelName: 'GeForce RTX 4070',
+    performanceLabel: 'Great',
+    memorySize: 16,
+    os: PcOs.LINUX,
+    osVersion: 'SteamOS 3.6',
+    notes: 'Seeded PC report for Eden with categorized graphics, CPU, and Eden Veil values.',
+  },
+  {
+    emulatorName: 'Azahar',
+    gameTitle: 'The Legend of Zelda: Ocarina of Time 3D',
+    systemName: 'Nintendo 3DS',
+    cpuModelName: 'Core i5-12400',
+    gpuModelName: 'Radeon RX 7800 XT',
+    performanceLabel: 'Perfect',
+    memorySize: 32,
+    os: PcOs.WINDOWS,
+    osVersion: 'Windows 11 24H2',
+    notes: 'Seeded PC report for Azahar with categorized graphics and CPU values.',
+  },
+] as const
+
+export default async function pcListingsSeeder(prisma: PrismaClient) {
+  console.info('🌱 Seeding PC listings...')
+
+  const users = await prisma.user.findMany({ select: { id: true, role: true } })
+  const adminUsers = users.filter(
+    (user) => user.role === Role.ADMIN || user.role === Role.SUPER_ADMIN,
+  )
+  const author = users[0]
+  const processor = adminUsers[0]
+
+  if (!author || !processor) {
+    console.warn('No seed users or admin users found, skipping PC listings seeding')
+    return
+  }
+
+  const customFieldDefinitionCache = createCustomFieldDefinitionCache()
+  let pcListingsCreated = 0
+  let customFieldValuesSynced = 0
+  let votesCreated = 0
+  let commentsCreated = 0
+
+  for (const fixture of pcListingFixtures) {
+    const [game, emulator, cpu, gpu, performance] = await Promise.all([
+      prisma.game.findFirst({
+        where: { title: fixture.gameTitle, system: { name: fixture.systemName } },
+        select: { id: true },
+      }),
+      prisma.emulator.findUnique({
+        where: { name: fixture.emulatorName },
+        select: { id: true },
+      }),
+      prisma.cpu.findFirst({
+        where: { modelName: fixture.cpuModelName },
+        select: { id: true },
+      }),
+      prisma.gpu.findFirst({
+        where: { modelName: fixture.gpuModelName },
+        select: { id: true },
+      }),
+      prisma.performanceScale.findUnique({
+        where: { label: fixture.performanceLabel },
+        select: { id: true },
+      }),
+    ])
+
+    if (!game || !emulator || !cpu || !gpu || !performance) {
+      console.warn(`Skipping PC listing fixture for ${fixture.gameTitle}; missing dependency`)
+      continue
+    }
+
+    const pcListing = await prisma.pcListing.create({
+      data: {
+        gameId: game.id,
+        emulatorId: emulator.id,
+        cpuId: cpu.id,
+        gpuId: gpu.id,
+        performanceId: performance.id,
+        memorySize: fixture.memorySize,
+        os: fixture.os,
+        osVersion: fixture.osVersion,
+        notes: fixture.notes,
+        authorId: author.id,
+        status: ApprovalStatus.APPROVED,
+        processedAt: new Date(),
+        processedNotes: 'Seeded PC report fixture.',
+        processedByUserId: processor.id,
+      },
+    })
+
+    pcListingsCreated += 1
+    customFieldValuesSynced += await seedPcListingCustomFieldValues(
+      prisma,
+      customFieldDefinitionCache,
+      pcListing.id,
+      emulator.id,
+      pcListingsCreated,
+    )
+
+    const engagement = await createPcListingEngagement(prisma, pcListing.id, users)
+    votesCreated += engagement.votesCreated
+    commentsCreated += engagement.commentsCreated
+  }
+
+  console.info('✅ PC listings seeding completed!')
+  console.info(`📈 PC listing statistics:`)
+  console.info(`   🖥️ ${pcListingsCreated} PC listings created`)
+  console.info(`   ⚙️ ${customFieldValuesSynced} custom field values synced`)
+  console.info(`   👍 ${votesCreated} votes added`)
+  console.info(`   💬 ${commentsCreated} comments added`)
+}
+
+async function createPcListingEngagement(
+  prisma: PrismaClient,
+  pcListingId: string,
+  users: SeedUser[],
+) {
+  let votesCreated = 0
+  let commentsCreated = 0
+  let upvotesCreated = 0
+  let downvotesCreated = 0
+  const voters = users.slice(0, 4)
+
+  for (const [index, voter] of voters.entries()) {
+    const isUpvote = index !== voters.length - 1
+    await prisma.pcListingVote.create({
+      data: {
+        pcListingId,
+        userId: voter.id,
+        value: isUpvote,
+      },
+    })
+    votesCreated += 1
+    if (isUpvote) {
+      upvotesCreated += 1
+    } else {
+      downvotesCreated += 1
+    }
+  }
+
+  await prisma.pcListing.update({
+    where: { id: pcListingId },
+    data: {
+      upvoteCount: upvotesCreated,
+      downvoteCount: downvotesCreated,
+      voteCount: votesCreated,
+      successRate: calculateWilsonScore(upvotesCreated, downvotesCreated),
+    },
+  })
+
+  const commenter = users[1] ?? users[0]
+  if (commenter) {
+    await prisma.pcListingComment.create({
+      data: {
+        pcListingId,
+        userId: commenter.id,
+        content: 'Seeded PC report comment for local testing.',
+        score: 2,
+      },
+    })
+    commentsCreated += 1
+  }
+
+  return { votesCreated, commentsCreated }
+}
diff --git a/sentry.edge.config.ts b/sentry.edge.config.ts
index fd56d752e..f24441da4 100644
--- a/sentry.edge.config.ts
+++ b/sentry.edge.config.ts
@@ -5,18 +5,18 @@
 
 import * as Sentry from '@sentry/nextjs'
 
-Sentry.init({
-  dsn: 'https://85ca585e45005d8786e361c3456518bf@o74828.ingest.us.sentry.io/4509717207318529',
+// Only enable Sentry when explicitly configured for this deployment.
+if (process.env.NEXT_PUBLIC_ENABLE_SENTRY === 'true') {
+  Sentry.init({
+    dsn: 'https://85ca585e45005d8786e361c3456518bf@o74828.ingest.us.sentry.io/4509717207318529',
 
-  // Only enable Sentry in production
-  enabled: process.env.NODE_ENV === 'production',
+    // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
+    tracesSampleRate: 1,
 
-  // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
-  tracesSampleRate: 1,
+    // Enable logs to be sent to Sentry
+    enableLogs: true,
 
-  // Enable logs to be sent to Sentry
-  enableLogs: true,
-
-  // Setting this option to true will print useful information to the console while you're setting up Sentry.
-  debug: false,
-})
+    // Setting this option to true will print useful information to the console while you're setting up Sentry.
+    debug: false,
+  })
+}
diff --git a/sentry.server.config.ts b/sentry.server.config.ts
index a2304c1cc..15f1fcbb8 100644
--- a/sentry.server.config.ts
+++ b/sentry.server.config.ts
@@ -4,18 +4,18 @@
 
 import * as Sentry from '@sentry/nextjs'
 
-Sentry.init({
-  dsn: 'https://85ca585e45005d8786e361c3456518bf@o74828.ingest.us.sentry.io/4509717207318529',
+// Only enable Sentry when explicitly configured for this deployment.
+if (process.env.NEXT_PUBLIC_ENABLE_SENTRY === 'true') {
+  Sentry.init({
+    dsn: 'https://85ca585e45005d8786e361c3456518bf@o74828.ingest.us.sentry.io/4509717207318529',
 
-  // Only enable Sentry in production
-  enabled: process.env.NODE_ENV === 'production',
+    // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
+    tracesSampleRate: 1,
 
-  // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
-  tracesSampleRate: 1,
+    // Enable logs to be sent to Sentry
+    enableLogs: true,
 
-  // Enable logs to be sent to Sentry
-  enableLogs: true,
-
-  // Setting this option to true will print useful information to the console while you're setting up Sentry.
-  debug: false,
-})
+    // Setting this option to true will print useful information to the console while you're setting up Sentry.
+    debug: false,
+  })
+}
diff --git a/src/app/admin/brands/page.tsx b/src/app/admin/brands/page.tsx
index 34b451081..ee3428ecc 100644
--- a/src/app/admin/brands/page.tsx
+++ b/src/app/admin/brands/page.tsx
@@ -67,9 +67,19 @@ function AdminBrandsPage() {
 
   const utils = api.useUtils()
 
-  const handleModalSuccess = () => {
+  const invalidateBrandQueries = () => {
     utils.deviceBrands.get.invalidate().catch(console.error)
     utils.deviceBrands.stats.invalidate().catch(console.error)
+    utils.devices.get.invalidate().catch(console.error)
+    utils.devices.options.invalidate().catch(console.error)
+    utils.cpus.get.invalidate().catch(console.error)
+    utils.cpus.options.invalidate().catch(console.error)
+    utils.gpus.get.invalidate().catch(console.error)
+    utils.gpus.options.invalidate().catch(console.error)
+  }
+
+  const handleModalSuccess = () => {
+    invalidateBrandQueries()
     closeModal()
   }
 
@@ -85,8 +95,7 @@ function AdminBrandsPage() {
       await deleteBrand.mutateAsync({
         id,
       } satisfies RouterInput['deviceBrands']['delete'])
-      utils.deviceBrands.get.invalidate().catch(console.error)
-      utils.deviceBrands.stats.invalidate().catch(console.error)
+      invalidateBrandQueries()
     } catch (err) {
       toast.error(`Failed to delete brand: ${getErrorMessage(err)}`)
     }
diff --git a/src/app/admin/cpus/page.tsx b/src/app/admin/cpus/page.tsx
index 8a8d8cdd3..2c5b7f61c 100644
--- a/src/app/admin/cpus/page.tsx
+++ b/src/app/admin/cpus/page.tsx
@@ -83,6 +83,12 @@ function AdminCpusPage() {
     ['intel', 'amd', 'apple'].includes(brand.name.toLowerCase()),
   )
 
+  const invalidateCpuQueries = () => {
+    utils.cpus.get.invalidate().catch(console.error)
+    utils.cpus.options.invalidate().catch(console.error)
+    utils.cpus.stats.invalidate().catch(console.error)
+  }
+
   const openModal = (cpu?: CpuData) => {
     setEditId(cpu?.id ?? null)
     setCpuData(cpu ?? null)
@@ -106,8 +112,7 @@ function AdminCpusPage() {
   }
 
   const handleModalSuccess = () => {
-    utils.cpus.get.invalidate().catch(console.error)
-    utils.cpus.stats.invalidate().catch(console.error)
+    invalidateCpuQueries()
     closeModal()
   }
 
@@ -123,8 +128,7 @@ function AdminCpusPage() {
       await deleteCpu.mutateAsync({
         id,
       } satisfies RouterInput['cpus']['delete'])
-      utils.cpus.get.invalidate().catch(console.error)
-      utils.cpus.stats.invalidate().catch(console.error)
+      invalidateCpuQueries()
       toast.success('CPU deleted successfully!')
     } catch (err) {
       toast.error(`Failed to delete CPU: ${getErrorMessage(err)}`)
diff --git a/src/app/admin/devices/components/DeviceModal.tsx b/src/app/admin/devices/components/DeviceModal.tsx
index df2122231..d697457a4 100644
--- a/src/app/admin/devices/components/DeviceModal.tsx
+++ b/src/app/admin/devices/components/DeviceModal.tsx
@@ -20,7 +20,8 @@ function DeviceModal(props: Props) {
   const createDevice = api.devices.create.useMutation()
   const updateDevice = api.devices.update.useMutation()
   const deviceBrandsQuery = api.deviceBrands.get.useQuery({ limit: 100 })
-  const socsQuery = api.socs.get.useQuery({ limit: 1000 })
+  // TODO: Make this selector async instead of preloading 1000 options.
+  const socsQuery = api.socs.options.useQuery({ limit: 1000 })
 
   const [brandId, setBrandId] = useState('')
   const [modelName, setModelName] = useState('')
diff --git a/src/app/admin/devices/page.tsx b/src/app/admin/devices/page.tsx
index f4c835e3a..dd3834741 100644
--- a/src/app/admin/devices/page.tsx
+++ b/src/app/admin/devices/page.tsx
@@ -76,6 +76,12 @@ function AdminDevicesPage() {
   const userQuery = api.users.me.useQuery()
   const canManageDevices = hasPermission(userQuery.data?.permissions, PERMISSIONS.MANAGE_DEVICES)
 
+  const invalidateDeviceQueries = () => {
+    utils.devices.get.invalidate().catch(console.error)
+    utils.devices.options.invalidate().catch(console.error)
+    utils.devices.stats.invalidate().catch(console.error)
+  }
+
   const openModal = (device?: DeviceData) => {
     setEditId(device?.id ?? null)
     setDeviceData(device ?? null)
@@ -99,9 +105,7 @@ function AdminDevicesPage() {
   }
 
   const handleModalSuccess = () => {
-    // Invalidate queries to refetch fresh data
-    utils.devices.get.invalidate().catch(console.error)
-    utils.devices.stats.invalidate().catch(console.error)
+    invalidateDeviceQueries()
     closeModal()
   }
 
@@ -117,8 +121,7 @@ function AdminDevicesPage() {
       await deleteDevice.mutateAsync({
         id,
       } satisfies RouterInput['devices']['delete'])
-      utils.devices.get.invalidate().catch(console.error)
-      utils.devices.stats.invalidate().catch(console.error)
+      invalidateDeviceQueries()
       toast.success('Device deleted successfully!')
     } catch (err) {
       toast.error(`Failed to delete device: ${getErrorMessage(err)}`)
diff --git a/src/app/admin/gpus/page.tsx b/src/app/admin/gpus/page.tsx
index b30f7e2c6..6fb4bc8f2 100644
--- a/src/app/admin/gpus/page.tsx
+++ b/src/app/admin/gpus/page.tsx
@@ -84,6 +84,12 @@ function AdminGpusPage() {
     ['intel', 'amd', 'nvidia'].includes(brand.name.toLowerCase()),
   )
 
+  const invalidateGpuQueries = () => {
+    utils.gpus.get.invalidate().catch(console.error)
+    utils.gpus.options.invalidate().catch(console.error)
+    utils.gpus.stats.invalidate().catch(console.error)
+  }
+
   const openModal = (gpu?: GpuData) => {
     setEditId(gpu?.id ?? null)
     setGpuData(gpu ?? null)
@@ -107,9 +113,7 @@ function AdminGpusPage() {
   }
 
   const handleModalSuccess = () => {
-    // Invalidate queries to refetch fresh data
-    utils.gpus.get.invalidate().catch(console.error)
-    utils.gpus.stats.invalidate().catch(console.error)
+    invalidateGpuQueries()
     closeModal()
   }
 
@@ -125,8 +129,7 @@ function AdminGpusPage() {
       await deleteGpu.mutateAsync({
         id,
       } satisfies RouterInput['gpus']['delete'])
-      utils.gpus.get.invalidate().catch(console.error)
-      utils.gpus.stats.invalidate().catch(console.error)
+      invalidateGpuQueries()
       toast.success('GPU deleted successfully!')
     } catch (err) {
       toast.error(`Failed to delete GPU: ${getErrorMessage(err)}`)
diff --git a/src/app/admin/listings/[id]/edit/components/ListingEditForm.tsx b/src/app/admin/listings/[id]/edit/components/ListingEditForm.tsx
index cc569c44f..17dc95210 100644
--- a/src/app/admin/listings/[id]/edit/components/ListingEditForm.tsx
+++ b/src/app/admin/listings/[id]/edit/components/ListingEditForm.tsx
@@ -109,7 +109,7 @@ function ListingEditForm(props: Props) {
   const loadDeviceItems = useCallback(
     async (query: string): Promise<DeviceOption[]> => {
       try {
-        const result = await utils.client.devices.get.query({
+        const result = await utils.client.devices.options.query({
           search: query || undefined, // Pass undefined instead of empty string
           limit: 50,
         })
diff --git a/src/app/admin/socs/page.tsx b/src/app/admin/socs/page.tsx
index e60ca786d..20a72b5a1 100644
--- a/src/app/admin/socs/page.tsx
+++ b/src/app/admin/socs/page.tsx
@@ -75,6 +75,12 @@ function AdminSoCsPage() {
   const socs = socsQuery.data?.socs ?? []
   const pagination = socsQuery.data?.pagination
 
+  const invalidateSocQueries = () => {
+    utils.socs.get.invalidate().catch(console.error)
+    utils.socs.options.invalidate().catch(console.error)
+    utils.socs.stats.invalidate().catch(console.error)
+  }
+
   const openModal = (soc?: SocData) => {
     setEditId(soc?.id ?? null)
     setSocData(soc ?? null)
@@ -98,9 +104,7 @@ function AdminSoCsPage() {
   }
 
   const handleModalSuccess = () => {
-    // Invalidate queries to refetch fresh data
-    utils.socs.get.invalidate().catch(console.error)
-    utils.socs.stats.invalidate().catch(console.error)
+    invalidateSocQueries()
     closeModal()
   }
 
@@ -116,8 +120,7 @@ function AdminSoCsPage() {
       await deleteSoc.mutateAsync({
         id,
       } satisfies RouterInput['socs']['delete'])
-      utils.socs.get.invalidate().catch(console.error)
-      utils.socs.stats.invalidate().catch(console.error)
+      invalidateSocQueries()
       toast.success('SoC deleted successfully!')
     } catch (err) {
       toast.error(`Failed to delete SoC: ${getErrorMessage(err)}`)
diff --git a/src/app/api/proxy-image/route.ts b/src/app/api/proxy-image/route.ts
index 64fa7e902..91bf13c41 100644
--- a/src/app/api/proxy-image/route.ts
+++ b/src/app/api/proxy-image/route.ts
@@ -25,7 +25,7 @@ export async function GET(req: NextRequest) {
 
   try {
     const upstream = await fetch(url.toString(), {
-      cache: env.IS_PROD ? 'force-cache' : 'no-store',
+      cache: env.IS_PRODUCTION_BUILD ? 'force-cache' : 'no-store',
       redirect: 'follow',
       headers: {
         'User-Agent': 'EmuReadyImageProxy/1.0 (+https://www.emuready.com)',
@@ -37,7 +37,7 @@ export async function GET(req: NextRequest) {
     }
 
     const contentType = upstream.headers.get('content-type') || 'application/octet-stream'
-    const cacheControl = env.IS_PROD
+    const cacheControl = env.IS_PRODUCTION_BUILD
       ? 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600'
       : 'no-store, no-cache, must-revalidate'
 
diff --git a/src/app/home/components/HomeTrendingDevices.tsx b/src/app/home/components/HomeTrendingDevices.tsx
index 79713ef11..f98c67fb8 100644
--- a/src/app/home/components/HomeTrendingDevices.tsx
+++ b/src/app/home/components/HomeTrendingDevices.tsx
@@ -9,6 +9,7 @@ import { HOME_PAGE_LIMITS } from '@/data/constants'
 import analytics from '@/lib/analytics'
 import { api } from '@/lib/api'
 import { cn } from '@/lib/utils'
+import { ms } from '@/utils/time'
 import { TimeRangeTabs, type TimeRangeId } from './TimeRangeTabs'
 
 const TIME_RANGE_LABELS: Record<TimeRangeId, string> = {
@@ -18,9 +19,15 @@ const TIME_RANGE_LABELS: Record<TimeRangeId, string> = {
 }
 
 export function HomeTrendingDevices() {
-  const trendingDevicesQuery = api.devices.trendingSummary.useQuery({
-    limit: HOME_PAGE_LIMITS.TRENDING_DEVICES,
-  })
+  const trendingDevicesQuery = api.devices.trendingSummary.useQuery(
+    {
+      limit: HOME_PAGE_LIMITS.TRENDING_DEVICES,
+    },
+    {
+      staleTime: ms.hours(6),
+      gcTime: ms.hours(12),
+    },
+  )
 
   const [activeTimeRange, setActiveTimeRange] = useState<TimeRangeId>('thisMonth')
 
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index e0527f2eb..abbaa4ef6 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -36,8 +36,8 @@ export const metadata: Metadata = defaultMetadata
 export default function RootLayout(props: PropsWithChildren) {
   return (
     <html lang="en" suppressHydrationWarning>
-      <head>
-        {env.IS_PROD && env.GA_ID && (
+      <body className={cn(inter.className, 'min-h-screen bg-background font-sans antialiased')}>
+        {env.ENABLE_ANALYTICS && env.GA_ID && (
           <Script id="google-analytics-dataLayer" strategy="beforeInteractive">
             {`
                 window.dataLayer = window.dataLayer || [];
@@ -46,13 +46,11 @@ export default function RootLayout(props: PropsWithChildren) {
               `}
           </Script>
         )}
-      </head>
-      <body className={cn(inter.className, 'min-h-screen bg-background font-sans antialiased')}>
         <ServiceWorkerRegistrar enabled={env.ENABLE_SW} />
         <ClerkBoundary>
           <Providers>
             <Toaster richColors closeButton />
-            {env.IS_PROD && !env.DISABLE_COOKIE_BANNER && <CookieConsent />}
+            {env.ENABLE_ANALYTICS && !env.DISABLE_COOKIE_BANNER && <CookieConsent />}
             <div className="flex flex-col min-h-screen bg-background text-foreground">
               <Suspense fallback={null}>
                 <Navbar />
@@ -61,16 +59,20 @@ export default function RootLayout(props: PropsWithChildren) {
               <Footer />
             </div>
           </Providers>
-          {env.IS_PROD && (
+          {(env.ENABLE_ANALYTICS || env.ENABLE_KOFI_WIDGET) && (
             <Suspense fallback={null}>
-              <SessionTracker />
-              <PageViewTracker />
-              <SpeedInsights />
-              <KofiWidget />
-              <GoogleAnalytics gaId={env.GA_ID} />
+              {env.ENABLE_ANALYTICS && (
+                <>
+                  <SessionTracker />
+                  <PageViewTracker />
+                  <SpeedInsights />
+                  {env.GA_ID && <GoogleAnalytics gaId={env.GA_ID} />}
+                </>
+              )}
+              {env.ENABLE_KOFI_WIDGET && <KofiWidget />}
             </Suspense>
           )}
-          {env.VERCEL_ANALYTICS_ENABLED && (
+          {env.ENABLE_ANALYTICS && env.VERCEL_ANALYTICS_ENABLED && (
             <Suspense fallback={null}>
               <Analytics />
             </Suspense>
diff --git a/src/app/listings/ListingsPage.tsx b/src/app/listings/ListingsPage.tsx
index fe49b51e1..46639deac 100644
--- a/src/app/listings/ListingsPage.tsx
+++ b/src/app/listings/ListingsPage.tsx
@@ -66,8 +66,9 @@ const LISTINGS_COLUMNS: ColumnDefinition[] = [
   { key: 'actions', label: 'Actions', alwaysVisible: true },
 ]
 
-const LOOKUP_DATA_STALE_TIME = ms.minutes(30)
-const LOOKUP_DATA_GC_TIME = ms.hours(1)
+const LOOKUP_DATA_STALE_TIME = ms.hours(6)
+const LOOKUP_DATA_GC_TIME = ms.hours(12)
+const USE_ASYNC_LISTING_FILTERS = process.env.NEXT_PUBLIC_ENABLE_ASYNC_LISTINGS_FILTERS === 'true'
 
 function ListingsPage() {
   const { isSignedIn } = useUser()
@@ -115,18 +116,20 @@ function ListingsPage() {
     staleTime: LOOKUP_DATA_STALE_TIME,
     gcTime: LOOKUP_DATA_GC_TIME,
   })
-  // TODO: find a better alternative to hardcoding 10000 for devices (AsyncMultiselect)
-  const devicesQuery = api.devices.get.useQuery(
+  // TODO: Remove this legacy fallback once async filters no longer need an opt-out.
+  const devicesQuery = api.devices.options.useQuery(
     { limit: 10000 },
     {
+      enabled: !USE_ASYNC_LISTING_FILTERS,
       staleTime: LOOKUP_DATA_STALE_TIME,
       gcTime: LOOKUP_DATA_GC_TIME,
     },
   )
-  // TODO: find a better alternative to hardcoding 10000 for SoCs (AsyncMultiselect)
-  const socsQuery = api.socs.get.useQuery(
+  // TODO: Remove this legacy fallback once async filters no longer need an opt-out.
+  const socsQuery = api.socs.options.useQuery(
     { limit: 10000 },
     {
+      enabled: !USE_ASYNC_LISTING_FILTERS,
       staleTime: LOOKUP_DATA_STALE_TIME,
       gcTime: LOOKUP_DATA_GC_TIME,
     },
@@ -251,6 +254,9 @@ function ListingsPage() {
     return <div className="p-8 text-center text-red-500">Failed to load listings.</div>
   }
 
+  const devicesForFilters = devicesQuery.data?.devices ?? []
+  const socsForFilters = socsQuery.data?.socs ?? []
+
   return (
     <main className="bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900">
       <div className="lg:flex">
@@ -264,8 +270,8 @@ function ListingsPage() {
             performanceIds={listingsState.performanceIds}
             searchTerm={listingsState.searchInput}
             systems={systemsQuery.data ?? []}
-            devices={devicesQuery.data?.devices ?? []}
-            socs={socsQuery.data?.socs ?? []}
+            devices={devicesForFilters}
+            socs={socsForFilters}
             emulators={emulatorsQuery.data?.emulators ?? []}
             performanceScales={performanceScalesQuery.data ?? []}
             onSystemChange={handleSystemChange}
@@ -300,8 +306,8 @@ function ListingsPage() {
               performanceIds={listingsState.performanceIds}
               searchTerm={listingsState.searchInput}
               systems={systemsQuery.data ?? []}
-              devices={devicesQuery.data?.devices ?? []}
-              socs={socsQuery.data?.socs ?? []}
+              devices={devicesForFilters}
+              socs={socsForFilters}
               emulators={emulatorsQuery.data?.emulators ?? []}
               performanceScales={performanceScalesQuery.data ?? []}
               onSystemChange={handleSystemChange}
diff --git a/src/app/listings/components/ListingsFiltersContent.tsx b/src/app/listings/components/ListingsFiltersContent.tsx
index f783776bd..627f87b29 100644
--- a/src/app/listings/components/ListingsFiltersContent.tsx
+++ b/src/app/listings/components/ListingsFiltersContent.tsx
@@ -1,12 +1,10 @@
 'use client'
 
-import { AnimatePresence, motion } from 'framer-motion'
+import { motion } from 'framer-motion'
 import { Joystick, MonitorSmartphone, Cpu, Gamepad, Rocket } from 'lucide-react'
 import { ActiveFiltersSummary, ListingsSearchBar } from '@/app/listings/shared/components'
 import { buildActiveFilterItems } from '@/app/listings/shared/utils/buildActiveFilterItems'
 import { MultiSelect } from '@/components/ui'
-import AsyncDeviceMultiSelect from '@/components/ui/form/AsyncDeviceMultiSelect'
-import AsyncSocMultiSelect from '@/components/ui/form/AsyncSocMultiSelect'
 import {
   performanceOptions,
   deviceOptions,
@@ -14,6 +12,8 @@ import {
   systemOptions,
   emulatorOptions,
 } from '@/utils/options'
+import AsyncDeviceFilterSelect from './filters/AsyncDeviceFilterSelect'
+import AsyncSocFilterSelect from './filters/AsyncSocFilterSelect'
 
 interface Props {
   systemIds: string[]
@@ -77,7 +77,7 @@ export default function ListingsFiltersContent(props: Props) {
       />
 
       {ENABLE_ASYNC_LISTINGS ? (
-        <AsyncDeviceMultiSelect
+        <AsyncDeviceFilterSelect
           label="Devices"
           leftIcon={<MonitorSmartphone className="w-5 h-5" />}
           value={props.deviceIds}
@@ -99,7 +99,7 @@ export default function ListingsFiltersContent(props: Props) {
       )}
 
       {ENABLE_ASYNC_LISTINGS ? (
-        <AsyncSocMultiSelect
+        <AsyncSocFilterSelect
           label="SoCs"
           leftIcon={<Cpu className="w-5 h-5" />}
           value={props.socIds}
@@ -142,23 +142,19 @@ export default function ListingsFiltersContent(props: Props) {
         maxDisplayed={1}
       />
 
-      {props.showActiveFilters && props.onClearAll && (
-        <AnimatePresence>
-          {hasActiveFilters && (
-            <ActiveFiltersSummary
-              showClearAll
-              onClearAll={props.onClearAll}
-              items={buildActiveFilterItems({
-                searchTerm: props.searchTerm,
-                systemIds: props.systemIds,
-                deviceIds: props.deviceIds,
-                socIds: props.socIds,
-                emulatorIds: props.emulatorIds,
-                performanceIds: props.performanceIds,
-              })}
-            />
-          )}
-        </AnimatePresence>
+      {props.showActiveFilters && props.onClearAll && hasActiveFilters && (
+        <ActiveFiltersSummary
+          showClearAll
+          onClearAll={props.onClearAll}
+          items={buildActiveFilterItems({
+            searchTerm: props.searchTerm,
+            systemIds: props.systemIds,
+            deviceIds: props.deviceIds,
+            socIds: props.socIds,
+            emulatorIds: props.emulatorIds,
+            performanceIds: props.performanceIds,
+          })}
+        />
       )}
     </motion.div>
   )
diff --git a/src/app/listings/components/ListingsFiltersSidebar.tsx b/src/app/listings/components/ListingsFiltersSidebar.tsx
index 2ec61c09f..57ecaaa92 100644
--- a/src/app/listings/components/ListingsFiltersSidebar.tsx
+++ b/src/app/listings/components/ListingsFiltersSidebar.tsx
@@ -1,7 +1,7 @@
 'use client'
 
 import { type inferRouterOutputs } from '@trpc/server'
-import { motion, AnimatePresence } from 'framer-motion'
+import { motion } from 'framer-motion'
 import {
   Joystick,
   MonitorSmartphone,
@@ -46,9 +46,8 @@ interface FiltersProps {
   systems: { id: string; name: string }[]
   devices: {
     id: string
-    brandId: string
     modelName: string
-    brand: { id: string; name: string; createdAt: Date }
+    brand: { id: string; name: string }
   }[]
   socs: { id: string; name: string; manufacturer: string }[]
   emulators: { id: string; name: string }[]
@@ -388,22 +387,20 @@ function ListingsFiltersSidebar(props: FiltersProps) {
         {/* Fields rendered above in ListingsFiltersContent */}
       </motion.div>
 
-      <AnimatePresence>
-        {hasActiveFilters && (
-          <ActiveFiltersSummary
-            showClearAll
-            onClearAll={handleClearAll}
-            items={buildActiveFilterItems({
-              searchTerm: props.searchTerm,
-              systemIds: props.systemIds,
-              deviceIds: props.deviceIds,
-              socIds: props.socIds,
-              emulatorIds: props.emulatorIds,
-              performanceIds: props.performanceIds,
-            })}
-          />
-        )}
-      </AnimatePresence>
+      {hasActiveFilters && (
+        <ActiveFiltersSummary
+          showClearAll
+          onClearAll={handleClearAll}
+          items={buildActiveFilterItems({
+            searchTerm: props.searchTerm,
+            systemIds: props.systemIds,
+            deviceIds: props.deviceIds,
+            socIds: props.socIds,
+            emulatorIds: props.emulatorIds,
+            performanceIds: props.performanceIds,
+          })}
+        />
+      )}
     </>
   )
 
diff --git a/src/app/listings/components/filters/AsyncDeviceFilterSelect.test.tsx b/src/app/listings/components/filters/AsyncDeviceFilterSelect.test.tsx
new file mode 100644
index 000000000..35fb6395f
--- /dev/null
+++ b/src/app/listings/components/filters/AsyncDeviceFilterSelect.test.tsx
@@ -0,0 +1,191 @@
+import { render, screen, fireEvent, act } from '@testing-library/react'
+import { describe, it, expect, vi, beforeAll, beforeEach } from 'vitest'
+import type AsyncDeviceFilterSelectComponent from './AsyncDeviceFilterSelect'
+
+const apiMocks = vi.hoisted(() => ({
+  useQueries: vi.fn(),
+  devicesGetByIdsUseQuery: vi.fn(),
+}))
+
+vi.mock('@/lib/api', () => ({
+  api: {
+    useQueries: apiMocks.useQueries,
+    devices: {
+      getByIds: { useQuery: apiMocks.devicesGetByIdsUseQuery },
+    },
+  },
+}))
+
+let AsyncDeviceFilterSelect: typeof AsyncDeviceFilterSelectComponent
+
+interface OptionsInput {
+  search?: string
+  limit: number
+  offset: number
+}
+
+interface IdsInput {
+  ids: string[]
+}
+
+interface OptionsQueryDescriptor {
+  input: OptionsInput
+  queryOptions: unknown
+}
+
+interface QueryProxy {
+  devices: {
+    options: (input: OptionsInput, queryOptions: unknown) => OptionsQueryDescriptor
+  }
+}
+
+let requestedOptionInputs: OptionsInput[] = []
+let requestedQueryOptions: unknown[] = []
+
+const queryOptions = expect.objectContaining({
+  staleTime: expect.any(Number),
+  gcTime: expect.any(Number),
+})
+
+const firstPageData = {
+  devices: [
+    {
+      id: 'device-1',
+      modelName: 'Pocket 5',
+      brand: { id: 'brand-1', name: 'Retroid' },
+      soc: null,
+    },
+  ],
+  hasMore: true,
+}
+
+const secondPageData = {
+  devices: [
+    {
+      id: 'device-2',
+      modelName: 'Odin 2',
+      brand: { id: 'brand-2', name: 'AYN' },
+      soc: null,
+    },
+  ],
+  hasMore: false,
+}
+
+function setupApiMocks() {
+  requestedOptionInputs = []
+  requestedQueryOptions = []
+  apiMocks.useQueries.mockImplementation(
+    (queryCallback: (proxy: QueryProxy) => OptionsQueryDescriptor[]) => {
+      const descriptors = queryCallback({
+        devices: {
+          options: (input, queryOptionsInput) => ({
+            input,
+            queryOptions: queryOptionsInput,
+          }),
+        },
+      })
+
+      requestedOptionInputs = descriptors.map((descriptor) => descriptor.input)
+      requestedQueryOptions = descriptors.map((descriptor) => descriptor.queryOptions)
+
+      return descriptors.map((descriptor) => ({
+        data: descriptor.input.offset === 0 ? firstPageData : secondPageData,
+        isFetching: false,
+      }))
+    },
+  )
+  apiMocks.devicesGetByIdsUseQuery.mockImplementation((input: IdsInput) => ({
+    data: input.ids.map((id) => ({
+      id,
+      modelName: 'Deck OLED',
+      brand: { id: 'brand-selected', name: 'Steam' },
+      soc: null,
+    })),
+  }))
+}
+
+describe('AsyncDeviceFilterSelect', () => {
+  beforeAll(async () => {
+    ;({ default: AsyncDeviceFilterSelect } = await import('./AsyncDeviceFilterSelect'))
+  })
+
+  beforeEach(() => {
+    vi.clearAllMocks()
+    setupApiMocks()
+  })
+
+  it('loads devices through the slim options endpoint and selected labels through getByIds', () => {
+    render(
+      <AsyncDeviceFilterSelect label="Devices" value={['device-selected']} onChange={vi.fn()} />,
+    )
+
+    expect(screen.getByText('Steam Deck OLED')).toBeInTheDocument()
+    expect(apiMocks.devicesGetByIdsUseQuery).toHaveBeenCalledWith(
+      { ids: ['device-selected'] },
+      expect.objectContaining({ enabled: true }),
+    )
+    expect(requestedOptionInputs).toEqual([{ search: undefined, limit: 50, offset: 0 }])
+    expect(requestedQueryOptions[0]).toEqual(queryOptions)
+
+    fireEvent.click(screen.getByRole('button', { name: 'Devices multi-select' }))
+    expect(screen.getByText('Retroid Pocket 5')).toBeInTheDocument()
+  })
+
+  it('keeps initially loaded options after the initial debounce window', () => {
+    vi.useFakeTimers()
+
+    try {
+      render(<AsyncDeviceFilterSelect label="Devices" value={[]} onChange={vi.fn()} />)
+
+      fireEvent.click(screen.getByRole('button', { name: 'Devices multi-select' }))
+      expect(screen.getByText('Retroid Pocket 5')).toBeInTheDocument()
+
+      act(() => {
+        vi.advanceTimersByTime(300)
+      })
+
+      expect(screen.getByText('Retroid Pocket 5')).toBeInTheDocument()
+    } finally {
+      vi.useRealTimers()
+    }
+  })
+
+  it('updates the search query and resets pagination', () => {
+    vi.useFakeTimers()
+
+    try {
+      render(<AsyncDeviceFilterSelect label="Devices" value={[]} onChange={vi.fn()} />)
+
+      fireEvent.click(screen.getByRole('button', { name: 'Devices multi-select' }))
+      fireEvent.change(screen.getByPlaceholderText('Search devices...'), {
+        target: { value: 'odin' },
+      })
+
+      act(() => {
+        vi.advanceTimersByTime(300)
+      })
+
+      expect(requestedOptionInputs).toEqual([{ search: 'odin', limit: 50, offset: 0 }])
+      expect(requestedQueryOptions[0]).toEqual(queryOptions)
+    } finally {
+      vi.useRealTimers()
+    }
+  })
+
+  it('requests the next page when scrolled near the bottom', () => {
+    render(<AsyncDeviceFilterSelect label="Devices" value={[]} onChange={vi.fn()} />)
+
+    fireEvent.click(screen.getByRole('button', { name: 'Devices multi-select' }))
+    const scrollContainer = screen.getByTestId('async-multi-select-options')
+    Object.defineProperty(scrollContainer, 'scrollTop', { value: 260, configurable: true })
+    Object.defineProperty(scrollContainer, 'clientHeight', { value: 80, configurable: true })
+    Object.defineProperty(scrollContainer, 'scrollHeight', { value: 340, configurable: true })
+    fireEvent.scroll(scrollContainer)
+
+    expect(requestedOptionInputs).toEqual([
+      { search: undefined, limit: 50, offset: 0 },
+      { search: undefined, limit: 50, offset: 50 },
+    ])
+    expect(requestedQueryOptions[1]).toEqual(queryOptions)
+  })
+})
diff --git a/src/app/listings/components/filters/AsyncDeviceFilterSelect.tsx b/src/app/listings/components/filters/AsyncDeviceFilterSelect.tsx
new file mode 100644
index 000000000..fb3a17a75
--- /dev/null
+++ b/src/app/listings/components/filters/AsyncDeviceFilterSelect.tsx
@@ -0,0 +1,88 @@
+'use client'
+
+import { type ReactNode, useCallback, useMemo, useState } from 'react'
+import AsyncMultiSelect from '@/components/ui/form/async-multi-select/AsyncMultiSelect'
+import { api } from '@/lib/api'
+import { ms } from '@/utils/time'
+
+interface Props {
+  label: string
+  leftIcon?: ReactNode
+  value: string[]
+  onChange: (values: string[]) => void
+  placeholder?: string
+  className?: string
+  maxDisplayed?: number
+}
+
+const PAGE_SIZE = 50
+const LOOKUP_DATA_QUERY_OPTIONS = {
+  staleTime: ms.hours(6),
+  gcTime: ms.hours(12),
+}
+
+export default function AsyncDeviceFilterSelect(props: Props) {
+  const [query, setQuery] = useState('')
+  const [pageOffsets, setPageOffsets] = useState([0])
+
+  const byIdsQuery = api.devices.getByIds.useQuery(
+    { ids: props.value },
+    { ...LOOKUP_DATA_QUERY_OPTIONS, enabled: props.value.length > 0 },
+  )
+
+  const pageQueries = api.useQueries((t) =>
+    pageOffsets.map((offset) =>
+      t.devices.options(
+        { search: query || undefined, limit: PAGE_SIZE, offset },
+        LOOKUP_DATA_QUERY_OPTIONS,
+      ),
+    ),
+  )
+
+  const options = useMemo(
+    () =>
+      pageQueries.flatMap((pageQuery) =>
+        (pageQuery.data?.devices ?? []).map((d) => ({
+          id: d.id,
+          name: `${d.brand.name} ${d.modelName}`,
+          badgeName: d.modelName,
+        })),
+      ),
+    [pageQueries],
+  )
+
+  const selectedByIds = useMemo(
+    () =>
+      (byIdsQuery.data ?? []).map((d) => ({
+        id: d.id,
+        name: `${d.brand.name} ${d.modelName}`,
+        badgeName: d.modelName,
+      })),
+    [byIdsQuery.data],
+  )
+
+  const lastPageQuery = pageQueries[pageQueries.length - 1]
+  const hasMore = lastPageQuery?.data?.hasMore ?? false
+  const isFetching = pageQueries.some((pageQuery) => pageQuery.isFetching)
+
+  const handleLoadMore = useCallback(() => {
+    setPageOffsets((offsets) => [...offsets, offsets[offsets.length - 1] + PAGE_SIZE])
+  }, [])
+
+  const handleQueryChange = useCallback((q: string) => {
+    setQuery(q)
+    setPageOffsets([0])
+  }, [])
+
+  return (
+    <AsyncMultiSelect
+      {...props}
+      options={options}
+      selectedByIds={selectedByIds}
+      isFetching={isFetching}
+      hasMore={hasMore}
+      onLoadMore={handleLoadMore}
+      onQueryChange={handleQueryChange}
+    />
+  )
+}
diff --git a/src/app/listings/components/filters/AsyncSocFilterSelect.test.tsx b/src/app/listings/components/filters/AsyncSocFilterSelect.test.tsx
new file mode 100644
index 000000000..dc2d90412
--- /dev/null
+++ b/src/app/listings/components/filters/AsyncSocFilterSelect.test.tsx
@@ -0,0 +1,85 @@
+import { render, screen, fireEvent } from '@testing-library/react'
+import { describe, it, expect, vi, beforeAll, beforeEach } from 'vitest'
+import type AsyncSocFilterSelectComponent from './AsyncSocFilterSelect'
+
+const apiMocks = vi.hoisted(() => ({
+  useQueries: vi.fn(),
+  socsGetByIdsUseQuery: vi.fn(),
+}))
+
+vi.mock('@/lib/api', () => ({
+  api: {
+    useQueries: apiMocks.useQueries,
+    socs: {
+      getByIds: { useQuery: apiMocks.socsGetByIdsUseQuery },
+    },
+  },
+}))
+
+let AsyncSocFilterSelect: typeof AsyncSocFilterSelectComponent
+
+interface IdsInput {
+  ids: string[]
+}
+
+interface OptionsInput {
+  search?: string
+  limit: number
+  offset: number
+}
+
+interface OptionsQueryDescriptor {
+  input: OptionsInput
+}
+
+interface QueryProxy {
+  socs: {
+    options: (input: OptionsInput) => OptionsQueryDescriptor
+  }
+}
+
+function setupApiMocks() {
+  apiMocks.useQueries.mockImplementation(
+    (queryCallback: (proxy: QueryProxy) => OptionsQueryDescriptor[]) => {
+      const descriptors = queryCallback({
+        socs: {
+          options: (input) => ({ input }),
+        },
+      })
+
+      return descriptors.map(() => ({
+        data: {
+          socs: [{ id: 'soc-1', name: 'Snapdragon 8 Gen 2', manufacturer: 'Qualcomm' }],
+          hasMore: false,
+        },
+        isFetching: false,
+      }))
+    },
+  )
+  apiMocks.socsGetByIdsUseQuery.mockImplementation((input: IdsInput) => ({
+    data: input.ids.map((id) => ({
+      id,
+      name: 'Dimensity 1100',
+      manufacturer: 'MediaTek',
+    })),
+  }))
+}
+
+describe('AsyncSocFilterSelect', () => {
+  beforeAll(async () => {
+    ;({ default: AsyncSocFilterSelect } = await import('./AsyncSocFilterSelect'))
+  })
+
+  beforeEach(() => {
+    vi.clearAllMocks()
+    setupApiMocks()
+  })
+
+  it('maps SoC option and selected labels', () => {
+    render(<AsyncSocFilterSelect label="SoCs" value={['soc-selected']} onChange={vi.fn()} />)
+
+    expect(screen.getByText('MediaTek Dimensity 1100')).toBeInTheDocument()
+    fireEvent.click(screen.getByRole('button', { name: 'SoCs multi-select' }))
+    expect(screen.getByText('Qualcomm Snapdragon 8 Gen 2')).toBeInTheDocument()
+  })
+})
diff --git a/src/app/listings/components/filters/AsyncSocFilterSelect.tsx b/src/app/listings/components/filters/AsyncSocFilterSelect.tsx
new file mode 100644
index 000000000..ac568ed44
--- /dev/null
+++ b/src/app/listings/components/filters/AsyncSocFilterSelect.tsx
@@ -0,0 +1,88 @@
+'use client'
+
+import { type ReactNode, useCallback, useMemo, useState } from 'react'
+import AsyncMultiSelect from '@/components/ui/form/async-multi-select/AsyncMultiSelect'
+import { api } from '@/lib/api'
+import { ms } from '@/utils/time'
+
+interface Props {
+  label: string
+  leftIcon?: ReactNode
+  value: string[]
+  onChange: (values: string[]) => void
+  placeholder?: string
+  className?: string
+  maxDisplayed?: number
+}
+
+const PAGE_SIZE = 50
+const LOOKUP_DATA_QUERY_OPTIONS = {
+  staleTime: ms.hours(6),
+  gcTime: ms.hours(12),
+}
+
+export default function AsyncSocFilterSelect(props: Props) {
+  const [query, setQuery] = useState('')
+  const [pageOffsets, setPageOffsets] = useState([0])
+
+  const byIdsQuery = api.socs.getByIds.useQuery(
+    { ids: props.value },
+    { ...LOOKUP_DATA_QUERY_OPTIONS, enabled: props.value.length > 0 },
+  )
+
+  const pageQueries = api.useQueries((t) =>
+    pageOffsets.map((offset) =>
+      t.socs.options(
+        { search: query || undefined, limit: PAGE_SIZE, offset },
+        LOOKUP_DATA_QUERY_OPTIONS,
+      ),
+    ),
+  )
+
+  const options = useMemo(
+    () =>
+      pageQueries.flatMap((pageQuery) =>
+        (pageQuery.data?.socs ?? []).map((s) => ({
+          id: s.id,
+          name: `${s.manufacturer} ${s.name}`,
+          badgeName: s.name,
+        })),
+      ),
+    [pageQueries],
+  )
+
+  const selectedByIds = useMemo(
+    () =>
+      (byIdsQuery.data ?? []).map((s) => ({
+        id: s.id,
+        name: `${s.manufacturer} ${s.name}`,
+        badgeName: s.name,
+      })),
+    [byIdsQuery.data],
+  )
+
+  const lastPageQuery = pageQueries[pageQueries.length - 1]
+  const hasMore = lastPageQuery?.data?.hasMore ?? false
+  const isFetching = pageQueries.some((pageQuery) => pageQuery.isFetching)
+
+  const handleLoadMore = useCallback(() => {
+    setPageOffsets((offsets) => [...offsets, offsets[offsets.length - 1] + PAGE_SIZE])
+  }, [])
+
+  const handleQueryChange = useCallback((q: string) => {
+    setQuery(q)
+    setPageOffsets([0])
+  }, [])
+
+  return (
+    <AsyncMultiSelect
+      {...props}
+      options={options}
+      selectedByIds={selectedByIds}
+      isFetching={isFetching}
+      hasMore={hasMore}
+      onLoadMore={handleLoadMore}
+      onQueryChange={handleQueryChange}
+    />
+  )
+}
diff --git a/src/app/listings/new/NewListingPage.tsx b/src/app/listings/new/NewListingPage.tsx
index 48f5efb75..9dee18a70 100644
--- a/src/app/listings/new/NewListingPage.tsx
+++ b/src/app/listings/new/NewListingPage.tsx
@@ -51,6 +51,10 @@ import { reconcileDriverValue } from '../components/shared/custom-fields/driverV
 export type ListingFormValues = RouterInput['listings']['create']
 
 const HIGHLIGHT_DURATION_MS = 1800
+const LOOKUP_DATA_QUERY_OPTIONS = {
+  staleTime: ms.hours(6),
+  gcTime: ms.hours(12),
+}
 
 function AddListingPage() {
   const router = useRouter()
@@ -186,7 +190,10 @@ function AddListingPage() {
     [form, parsedCustomFields, driverVersionsQuery.data?.releases],
   )
 
-  const performanceScalesQuery = api.listings.performanceScales.useQuery()
+  const performanceScalesQuery = api.listings.performanceScales.useQuery(
+    undefined,
+    LOOKUP_DATA_QUERY_OPTIONS,
+  )
   const customFieldDefinitionsQuery = api.customFieldDefinitions.getByEmulator.useQuery(
     { emulatorId: selectedEmulatorId },
     {
@@ -266,7 +273,7 @@ function AddListingPage() {
       setDeviceSearchTerm(query)
       if (query.length < 2) return Promise.resolve([])
       try {
-        const result = await utils.devices.get.fetch({
+        const result = await utils.devices.options.fetch({
           search: query,
           limit: 50,
         })
@@ -291,7 +298,7 @@ function AddListingPage() {
         return []
       }
     },
-    [utils.devices.get],
+    [utils.devices.options],
   )
 
   useEffect(() => {
diff --git a/src/app/pc-listings/PcListingsPage.tsx b/src/app/pc-listings/PcListingsPage.tsx
index d1fbaf329..357a1313a 100644
--- a/src/app/pc-listings/PcListingsPage.tsx
+++ b/src/app/pc-listings/PcListingsPage.tsx
@@ -49,6 +49,7 @@ import {
 } from '@/utils/navigation-events'
 import { roleIncludesRole } from '@/utils/permission-system'
 import { hasRolePermission } from '@/utils/permissions'
+import { ms } from '@/utils/time'
 import { Role, ApprovalStatus } from '@orm'
 import PcFiltersContent from './components/PcFiltersContent'
 import PcFiltersSidebar from './components/PcFiltersSidebar'
@@ -69,6 +70,12 @@ const PC_LISTINGS_COLUMNS: ColumnDefinition[] = [
   { key: 'actions', label: 'Actions', alwaysVisible: true },
 ]
 
+const LOOKUP_DATA_QUERY_OPTIONS = {
+  staleTime: ms.hours(6),
+  gcTime: ms.hours(12),
+}
+const USE_ASYNC_LISTING_FILTERS = process.env.NEXT_PUBLIC_ENABLE_ASYNC_LISTINGS_FILTERS === 'true'
+
 function PcListingsPage() {
   const router = useRouter()
   const listingsState = usePcListingsState()
@@ -96,13 +103,22 @@ function PcListingsPage() {
   const isAdmin = userRole ? hasRolePermission(userRole, Role.ADMIN) : false
   const isModerator = userRole ? roleIncludesRole(userRole, Role.MODERATOR) : false
 
-  // TODO: handle MultiSelect async instead of fetching 1000 items
-  const cpusQuery = api.cpus.get.useQuery({ limit: 1000 })
-  // TODO: handle MultiSelect async instead of fetching 1000 items
-  const gpusQuery = api.gpus.get.useQuery({ limit: 1000 })
-  const emulatorsQuery = api.emulators.get.useQuery({ limit: 100 })
-  const performanceScalesQuery = api.listings.performanceScales.useQuery()
-  const systemsQuery = api.systems.get.useQuery()
+  // TODO: Remove this legacy fallback once async PC filters no longer need an opt-out.
+  const cpusQuery = api.cpus.options.useQuery(
+    { limit: 1000 },
+    { ...LOOKUP_DATA_QUERY_OPTIONS, enabled: !USE_ASYNC_LISTING_FILTERS },
+  )
+  // TODO: Remove this legacy fallback once async PC filters no longer need an opt-out.
+  const gpusQuery = api.gpus.options.useQuery(
+    { limit: 1000 },
+    { ...LOOKUP_DATA_QUERY_OPTIONS, enabled: !USE_ASYNC_LISTING_FILTERS },
+  )
+  const emulatorsQuery = api.emulators.get.useQuery({ limit: 100 }, LOOKUP_DATA_QUERY_OPTIONS)
+  const performanceScalesQuery = api.listings.performanceScales.useQuery(
+    undefined,
+    LOOKUP_DATA_QUERY_OPTIONS,
+  )
+  const systemsQuery = api.systems.get.useQuery(undefined, LOOKUP_DATA_QUERY_OPTIONS)
 
   const filterParams: RouterInput['pcListings']['get'] = {
     page: listingsState.page,
@@ -167,6 +183,9 @@ function PcListingsPage() {
     return <div className="p-8 text-center text-red-500">Failed to load PC listings.</div>
   }
 
+  const cpusForFilters = cpusQuery.data?.cpus ?? []
+  const gpusForFilters = gpusQuery.data?.gpus ?? []
+
   return (
     <main className="bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900">
       <div className="lg:flex">
@@ -184,8 +203,8 @@ function PcListingsPage() {
             minMemory={listingsState.minMemory}
             maxMemory={listingsState.maxMemory}
             searchTerm={listingsState.searchInput}
-            cpus={cpusQuery.data?.cpus ?? []}
-            gpus={gpusQuery.data?.gpus ?? []}
+            cpus={cpusForFilters}
+            gpus={gpusForFilters}
             systems={systemsQuery.data ?? []}
             emulators={emulatorsQuery.data?.emulators ?? []}
             performanceScales={performanceScalesQuery.data ?? []}
@@ -212,8 +231,8 @@ function PcListingsPage() {
               minMemory={listingsState.minMemory}
               maxMemory={listingsState.maxMemory}
               searchTerm={listingsState.searchInput}
-              cpus={cpusQuery.data?.cpus ?? []}
-              gpus={gpusQuery.data?.gpus ?? []}
+              cpus={cpusForFilters}
+              gpus={gpusForFilters}
               systems={systemsQuery.data ?? []}
               emulators={emulatorsQuery.data?.emulators ?? []}
               performanceScales={performanceScalesQuery.data ?? []}
diff --git a/src/app/pc-listings/components/PcFiltersContent.tsx b/src/app/pc-listings/components/PcFiltersContent.tsx
index 3a9d3fbf3..c8894bc17 100644
--- a/src/app/pc-listings/components/PcFiltersContent.tsx
+++ b/src/app/pc-listings/components/PcFiltersContent.tsx
@@ -6,8 +6,6 @@ import { type ChangeEvent } from 'react'
 import { ListingsSearchBar, ActiveFiltersSummary } from '@/app/listings/shared/components'
 import { buildPcActiveFilterItems } from '@/app/pc-listings/utils/buildPcActiveFilterItems'
 import { MultiSelect, Input } from '@/components/ui'
-import AsyncCpuMultiSelect from '@/components/ui/form/AsyncCpuMultiSelect'
-import AsyncGpuMultiSelect from '@/components/ui/form/AsyncGpuMultiSelect'
 import {
   cpuOptions,
   emulatorOptions,
@@ -16,6 +14,8 @@ import {
   systemOptions,
 } from '@/utils/options'
 import { type System, type PerformanceScale, type Emulator } from '@orm'
+import AsyncCpuFilterSelect from './filters/AsyncCpuFilterSelect'
+import AsyncGpuFilterSelect from './filters/AsyncGpuFilterSelect'
 
 type CpuWithBrand = { id: string; modelName: string; brand: { name: string } }
 type GpuWithBrand = { id: string; modelName: string; brand: { name: string } }
@@ -80,7 +80,7 @@ export default function PcFiltersContent(props: Props) {
     props.onPerformanceChange(values)
   }
 
-  const ENABLE_ASYNC = process.env.NEXT_PUBLIC_ENABLE_ASYNC_PC_FILTERS === 'true'
+  const ENABLE_ASYNC = process.env.NEXT_PUBLIC_ENABLE_ASYNC_LISTINGS_FILTERS === 'true'
 
   const hasActiveFilters =
     props.searchTerm ||
@@ -117,7 +117,7 @@ export default function PcFiltersContent(props: Props) {
 
       {/* CPUs */}
       {ENABLE_ASYNC ? (
-        <AsyncCpuMultiSelect
+        <AsyncCpuFilterSelect
           label="CPUs"
           leftIcon={<Cpu className="w-5 h-5" />}
           value={props.cpuIds}
@@ -139,7 +139,7 @@ export default function PcFiltersContent(props: Props) {
 
       {/* GPUs */}
       {ENABLE_ASYNC ? (
-        <AsyncGpuMultiSelect
+        <AsyncGpuFilterSelect
           label="GPUs"
           leftIcon={<HardDrive className="w-5 h-5" />}
           value={props.gpuIds}
diff --git a/src/app/pc-listings/components/filters/AsyncCpuFilterSelect.test.tsx b/src/app/pc-listings/components/filters/AsyncCpuFilterSelect.test.tsx
new file mode 100644
index 000000000..fb501b8cc
--- /dev/null
+++ b/src/app/pc-listings/components/filters/AsyncCpuFilterSelect.test.tsx
@@ -0,0 +1,87 @@
+import { render, screen, fireEvent } from '@testing-library/react'
+import { describe, it, expect, vi, beforeAll, beforeEach } from 'vitest'
+import type AsyncCpuFilterSelectComponent from './AsyncCpuFilterSelect'
+
+const apiMocks = vi.hoisted(() => ({
+  useQueries: vi.fn(),
+  cpusGetByIdsUseQuery: vi.fn(),
+}))
+
+vi.mock('@/lib/api', () => ({
+  api: {
+    useQueries: apiMocks.useQueries,
+    cpus: {
+      getByIds: { useQuery: apiMocks.cpusGetByIdsUseQuery },
+    },
+  },
+}))
+
+let AsyncCpuFilterSelect: typeof AsyncCpuFilterSelectComponent
+
+interface IdsInput {
+  ids: string[]
+}
+
+interface OptionsInput {
+  search?: string
+  limit: number
+  offset: number
+}
+
+interface OptionsQueryDescriptor {
+  input: OptionsInput
+}
+
+interface QueryProxy {
+  cpus: {
+    options: (input: OptionsInput) => OptionsQueryDescriptor
+  }
+}
+
+function setupApiMocks() {
+  apiMocks.useQueries.mockImplementation(
+    (queryCallback: (proxy: QueryProxy) => OptionsQueryDescriptor[]) => {
+      const descriptors = queryCallback({
+        cpus: {
+          options: (input) => ({ input }),
+        },
+      })
+
+      return descriptors.map(() => ({
+        data: {
+          cpus: [
+            { id: 'cpu-1', modelName: 'Core i7-12700K', brand: { id: 'intel', name: 'Intel' } },
+          ],
+          hasMore: false,
+        },
+        isFetching: false,
+      }))
+    },
+  )
+  apiMocks.cpusGetByIdsUseQuery.mockImplementation((input: IdsInput) => ({
+    data: input.ids.map((id) => ({
+      id,
+      modelName: 'Ryzen 7 7800X3D',
+      brand: { id: 'amd', name: 'AMD' },
+    })),
+  }))
+}
+
+describe('AsyncCpuFilterSelect', () => {
+  beforeAll(async () => {
+    ;({ default: AsyncCpuFilterSelect } = await import('./AsyncCpuFilterSelect'))
+  })
+
+  beforeEach(() => {
+    vi.clearAllMocks()
+    setupApiMocks()
+  })
+
+  it('maps CPU option and selected labels', () => {
+    render(<AsyncCpuFilterSelect label="CPUs" value={['cpu-selected']} onChange={vi.fn()} />)
+
+    expect(screen.getByText('AMD Ryzen 7 7800X3D')).toBeInTheDocument()
+    fireEvent.click(screen.getByRole('button', { name: 'CPUs multi-select' }))
+    expect(screen.getByText('Intel Core i7-12700K')).toBeInTheDocument()
+  })
+})
diff --git a/src/app/pc-listings/components/filters/AsyncCpuFilterSelect.tsx b/src/app/pc-listings/components/filters/AsyncCpuFilterSelect.tsx
new file mode 100644
index 000000000..474daf663
--- /dev/null
+++ b/src/app/pc-listings/components/filters/AsyncCpuFilterSelect.tsx
@@ -0,0 +1,88 @@
+'use client'
+
+import { type ReactNode, useCallback, useMemo, useState } from 'react'
+import AsyncMultiSelect from '@/components/ui/form/async-multi-select/AsyncMultiSelect'
+import { api } from '@/lib/api'
+import { ms } from '@/utils/time'
+
+interface Props {
+  label: string
+  leftIcon?: ReactNode
+  value: string[]
+  onChange: (values: string[]) => void
+  placeholder?: string
+  className?: string
+  maxDisplayed?: number
+}
+
+const PAGE_SIZE = 50
+const LOOKUP_DATA_QUERY_OPTIONS = {
+  staleTime: ms.hours(6),
+  gcTime: ms.hours(12),
+}
+
+export default function AsyncCpuFilterSelect(props: Props) {
+  const [query, setQuery] = useState('')
+  const [pageOffsets, setPageOffsets] = useState([0])
+
+  const byIdsQuery = api.cpus.getByIds.useQuery(
+    { ids: props.value },
+    { ...LOOKUP_DATA_QUERY_OPTIONS, enabled: props.value.length > 0 },
+  )
+
+  const pageQueries = api.useQueries((t) =>
+    pageOffsets.map((offset) =>
+      t.cpus.options(
+        { search: query || undefined, limit: PAGE_SIZE, offset },
+        LOOKUP_DATA_QUERY_OPTIONS,
+      ),
+    ),
+  )
+
+  const options = useMemo(
+    () =>
+      pageQueries.flatMap((pageQuery) =>
+        (pageQuery.data?.cpus ?? []).map((c) => ({
+          id: c.id,
+          name: `${c.brand.name} ${c.modelName}`,
+          badgeName: c.modelName,
+        })),
+      ),
+    [pageQueries],
+  )
+
+  const selectedByIds = useMemo(
+    () =>
+      (byIdsQuery.data ?? []).map((c) => ({
+        id: c.id,
+        name: `${c.brand.name} ${c.modelName}`,
+        badgeName: c.modelName,
+      })),
+    [byIdsQuery.data],
+  )
+
+  const lastPageQuery = pageQueries[pageQueries.length - 1]
+  const hasMore = lastPageQuery?.data?.hasMore ?? false
+  const isFetching = pageQueries.some((pageQuery) => pageQuery.isFetching)
+
+  const handleLoadMore = useCallback(() => {
+    setPageOffsets((offsets) => [...offsets, offsets[offsets.length - 1] + PAGE_SIZE])
+  }, [])
+
+  const handleQueryChange = useCallback((q: string) => {
+    setQuery(q)
+    setPageOffsets([0])
+  }, [])
+
+  return (
+    <AsyncMultiSelect
+      {...props}
+      options={options}
+      selectedByIds={selectedByIds}
+      isFetching={isFetching}
+      hasMore={hasMore}
+      onLoadMore={handleLoadMore}
+      onQueryChange={handleQueryChange}
+    />
+  )
+}
diff --git a/src/app/pc-listings/components/filters/AsyncGpuFilterSelect.test.tsx b/src/app/pc-listings/components/filters/AsyncGpuFilterSelect.test.tsx
new file mode 100644
index 000000000..97bb9e9a5
--- /dev/null
+++ b/src/app/pc-listings/components/filters/AsyncGpuFilterSelect.test.tsx
@@ -0,0 +1,85 @@
+import { render, screen, fireEvent } from '@testing-library/react'
+import { describe, it, expect, vi, beforeAll, beforeEach } from 'vitest'
+import type AsyncGpuFilterSelectComponent from './AsyncGpuFilterSelect'
+
+const apiMocks = vi.hoisted(() => ({
+  useQueries: vi.fn(),
+  gpusGetByIdsUseQuery: vi.fn(),
+}))
+
+vi.mock('@/lib/api', () => ({
+  api: {
+    useQueries: apiMocks.useQueries,
+    gpus: {
+      getByIds: { useQuery: apiMocks.gpusGetByIdsUseQuery },
+    },
+  },
+}))
+
+let AsyncGpuFilterSelect: typeof AsyncGpuFilterSelectComponent
+
+interface IdsInput {
+  ids: string[]
+}
+
+interface OptionsInput {
+  search?: string
+  limit: number
+  offset: number
+}
+
+interface OptionsQueryDescriptor {
+  input: OptionsInput
+}
+
+interface QueryProxy {
+  gpus: {
+    options: (input: OptionsInput) => OptionsQueryDescriptor
+  }
+}
+
+function setupApiMocks() {
+  apiMocks.useQueries.mockImplementation(
+    (queryCallback: (proxy: QueryProxy) => OptionsQueryDescriptor[]) => {
+      const descriptors = queryCallback({
+        gpus: {
+          options: (input) => ({ input }),
+        },
+      })
+
+      return descriptors.map(() => ({
+        data: {
+          gpus: [{ id: 'gpu-1', modelName: 'RTX 4070', brand: { id: 'nvidia', name: 'NVIDIA' } }],
+          hasMore: false,
+        },
+        isFetching: false,
+      }))
+    },
+  )
+  apiMocks.gpusGetByIdsUseQuery.mockImplementation((input: IdsInput) => ({
+    data: input.ids.map((id) => ({
+      id,
+      modelName: 'Radeon RX 7800 XT',
+      brand: { id: 'amd', name: 'AMD' },
+    })),
+  }))
+}
+
+describe('AsyncGpuFilterSelect', () => {
+  beforeAll(async () => {
+    ;({ default: AsyncGpuFilterSelect } = await import('./AsyncGpuFilterSelect'))
+  })
+
+  beforeEach(() => {
+    vi.clearAllMocks()
+    setupApiMocks()
+  })
+
+  it('maps GPU option and selected labels', () => {
+    render(<AsyncGpuFilterSelect label="GPUs" value={['gpu-selected']} onChange={vi.fn()} />)
+
+    expect(screen.getByText('AMD Radeon RX 7800 XT')).toBeInTheDocument()
+    fireEvent.click(screen.getByRole('button', { name: 'GPUs multi-select' }))
+    expect(screen.getByText('NVIDIA RTX 4070')).toBeInTheDocument()
+  })
+})
diff --git a/src/app/pc-listings/components/filters/AsyncGpuFilterSelect.tsx b/src/app/pc-listings/components/filters/AsyncGpuFilterSelect.tsx
new file mode 100644
index 000000000..679749d77
--- /dev/null
+++ b/src/app/pc-listings/components/filters/AsyncGpuFilterSelect.tsx
@@ -0,0 +1,88 @@
+'use client'
+
+import { type ReactNode, useCallback, useMemo, useState } from 'react'
+import AsyncMultiSelect from '@/components/ui/form/async-multi-select/AsyncMultiSelect'
+import { api } from '@/lib/api'
+import { ms } from '@/utils/time'
+
+interface Props {
+  label: string
+  leftIcon?: ReactNode
+  value: string[]
+  onChange: (values: string[]) => void
+  placeholder?: string
+  className?: string
+  maxDisplayed?: number
+}
+
+const PAGE_SIZE = 50
+const LOOKUP_DATA_QUERY_OPTIONS = {
+  staleTime: ms.hours(6),
+  gcTime: ms.hours(12),
+}
+
+export default function AsyncGpuFilterSelect(props: Props) {
+  const [query, setQuery] = useState('')
+  const [pageOffsets, setPageOffsets] = useState([0])
+
+  const byIdsQuery = api.gpus.getByIds.useQuery(
+    { ids: props.value },
+    { ...LOOKUP_DATA_QUERY_OPTIONS, enabled: props.value.length > 0 },
+  )
+
+  const pageQueries = api.useQueries((t) =>
+    pageOffsets.map((offset) =>
+      t.gpus.options(
+        { search: query || undefined, limit: PAGE_SIZE, offset },
+        LOOKUP_DATA_QUERY_OPTIONS,
+      ),
+    ),
+  )
+
+  const options = useMemo(
+    () =>
+      pageQueries.flatMap((pageQuery) =>
+        (pageQuery.data?.gpus ?? []).map((g) => ({
+          id: g.id,
+          name: `${g.brand.name} ${g.modelName}`,
+          badgeName: g.modelName,
+        })),
+      ),
+    [pageQueries],
+  )
+
+  const selectedByIds = useMemo(
+    () =>
+      (byIdsQuery.data ?? []).map((g) => ({
+        id: g.id,
+        name: `${g.brand.name} ${g.modelName}`,
+        badgeName: g.modelName,
+      })),
+    [byIdsQuery.data],
+  )
+
+  const lastPageQuery = pageQueries[pageQueries.length - 1]
+  const hasMore = lastPageQuery?.data?.hasMore ?? false
+  const isFetching = pageQueries.some((pageQuery) => pageQuery.isFetching)
+
+  const handleLoadMore = useCallback(() => {
+    setPageOffsets((offsets) => [...offsets, offsets[offsets.length - 1] + PAGE_SIZE])
+  }, [])
+
+  const handleQueryChange = useCallback((q: string) => {
+    setQuery(q)
+    setPageOffsets([0])
+  }, [])
+
+  return (
+    <AsyncMultiSelect
+      {...props}
+      options={options}
+      selectedByIds={selectedByIds}
+      isFetching={isFetching}
+      hasMore={hasMore}
+      onLoadMore={handleLoadMore}
+      onQueryChange={handleQueryChange}
+    />
+  )
+}
diff --git a/src/app/pc-listings/new/NewPcListingPage.tsx b/src/app/pc-listings/new/NewPcListingPage.tsx
index 55772811d..ca019771f 100644
--- a/src/app/pc-listings/new/NewPcListingPage.tsx
+++ b/src/app/pc-listings/new/NewPcListingPage.tsx
@@ -31,16 +31,21 @@ import { type RouterInput, type RouterOutput } from '@/types/trpc'
 import { type CustomFieldDefinitionWithOptions } from '@/utils/custom-field-validation'
 import { parseCustomFieldOptions, getCustomFieldDefaultValue } from '@/utils/custom-fields'
 import getErrorMessage from '@/utils/getErrorMessage'
+import { ms } from '@/utils/time'
 import { PcOs } from '@orm'
 import createDynamicPcListingSchema from './form-schemas/createDynamicPcListingSchema'
 
 export type PcListingFormValues = RouterInput['pcListings']['create']
 
-type CpuOption = RouterOutput['cpus']['get']['cpus'][number]
-type GpuOption = RouterOutput['gpus']['get']['gpus'][number]
+type CpuOption = RouterOutput['cpus']['options']['cpus'][number]
+type GpuOption = RouterOutput['gpus']['options']['gpus'][number]
 type PcPresetOption = RouterOutput['pcListings']['presets']['get'][number]
 
 const OS_OPTIONS = PC_OS_OPTIONS
+const LOOKUP_DATA_QUERY_OPTIONS = {
+  staleTime: ms.hours(6),
+  gcTime: ms.hours(12),
+}
 
 function AddPcListingPage() {
   const router = useRouter()
@@ -63,7 +68,10 @@ function AddPcListingPage() {
 
   const utils = api.useUtils()
   const createPcListing = api.pcListings.create.useMutation()
-  const performanceScalesQuery = api.performanceScales.get.useQuery()
+  const performanceScalesQuery = api.performanceScales.get.useQuery(
+    undefined,
+    LOOKUP_DATA_QUERY_OPTIONS,
+  )
   const presetsQuery = api.pcListings.presets.get.useQuery({})
   const { handleKeyDown } = useFormKeyDown()
 
@@ -75,28 +83,28 @@ function AddPcListingPage() {
     async (query: string): Promise<CpuOption[]> => {
       if (query.length < 2) return Promise.resolve([])
       try {
-        const result = await utils.cpus.get.fetch({ search: query, limit: 20 })
+        const result = await utils.cpus.options.fetch({ search: query, limit: 20 })
         return result.cpus ?? []
       } catch (error) {
         console.error('Error fetching CPUs:', error)
         return []
       }
     },
-    [utils.cpus.get],
+    [utils.cpus.options],
   )
 
   const loadGpuItems = useCallback(
     async (query: string): Promise<GpuOption[]> => {
       if (query.length < 2) return Promise.resolve([])
       try {
-        const result = await utils.gpus.get.fetch({ search: query, limit: 20 })
+        const result = await utils.gpus.options.fetch({ search: query, limit: 20 })
         return result.gpus ?? []
       } catch (error) {
         console.error('Error fetching GPUs:', error)
         return []
       }
     },
-    [utils.gpus.get],
+    [utils.gpus.options],
   )
 
   const form = useForm<PcListingFormValues>({
diff --git a/src/app/profile/components/DeviceSelector.tsx b/src/app/profile/components/DeviceSelector.tsx
index ff36d166c..8404d1baa 100644
--- a/src/app/profile/components/DeviceSelector.tsx
+++ b/src/app/profile/components/DeviceSelector.tsx
@@ -8,6 +8,7 @@ import { api } from '@/lib/api'
 import { cn } from '@/lib/utils'
 import getErrorMessage from '@/utils/getErrorMessage'
 import { searchItems, getDeviceSearchText } from '@/utils/simpleSearch'
+import { ms } from '@/utils/time'
 
 interface Device {
   id: string
@@ -29,16 +30,24 @@ interface Props {
   className?: string
 }
 
+const LOOKUP_DATA_QUERY_OPTIONS = {
+  staleTime: ms.hours(6),
+  gcTime: ms.hours(12),
+}
+
+const EMPTY_DEVICES: Device[] = []
+
 function DeviceSelector(props: Props) {
   const [searchQuery, setSearchQuery] = useState('')
   const [expandedBrands, setExpandedBrands] = useState<Set<string>>(new Set())
 
-  const devicesQuery = api.devices.get.useQuery({ limit: 1000 })
+  // TODO: Make this selector async instead of preloading 1000 options.
+  const devicesQuery = api.devices.options.useQuery({ limit: 1000 }, LOOKUP_DATA_QUERY_OPTIONS)
+  const devices = devicesQuery.data?.devices ?? EMPTY_DEVICES
 
   const filteredDevices = useMemo(() => {
-    if (!devicesQuery.data?.devices) return []
-    return searchItems(devicesQuery.data.devices, searchQuery, getDeviceSearchText)
-  }, [devicesQuery.data?.devices, searchQuery])
+    return searchItems(devices, searchQuery, getDeviceSearchText)
+  }, [devices, searchQuery])
 
   // Group devices by brand for better organization
   const devicesByBrand = useMemo(() => {
diff --git a/src/app/profile/components/PcPresetModal.tsx b/src/app/profile/components/PcPresetModal.tsx
index 4abc52499..e7c330c57 100644
--- a/src/app/profile/components/PcPresetModal.tsx
+++ b/src/app/profile/components/PcPresetModal.tsx
@@ -1,17 +1,20 @@
 'use client'
 
-import { useState, useEffect, type FormEvent } from 'react'
+import { useCallback, useState, useEffect, type FormEvent } from 'react'
 import { Button, Input, Modal, Autocomplete, SelectInput } from '@/components/ui'
 import { PC_OS_OPTIONS } from '@/data/pc-os'
 import { api } from '@/lib/api'
 import { type RouterInput, type RouterOutput } from '@/types/trpc'
 import getErrorMessage from '@/utils/getErrorMessage'
+import { ms } from '@/utils/time'
 import { PcOs } from '@orm'
 
 type PcPreset = RouterOutput['pcListings']['presets']['get'][number]
 type PcPresetMutationResult =
   | RouterOutput['pcListings']['presets']['create']
   | RouterOutput['pcListings']['presets']['update']
+type CpuOption = RouterOutput['cpus']['options']['cpus'][number]
+type GpuOption = RouterOutput['gpus']['options']['gpus'][number]
 
 interface Props {
   isOpen: boolean
@@ -21,12 +24,15 @@ interface Props {
 }
 
 const OS_OPTIONS = PC_OS_OPTIONS
+const LOOKUP_DATA_QUERY_OPTIONS = {
+  staleTime: ms.hours(6),
+  gcTime: ms.hours(12),
+}
 
 function PcPresetModal(props: Props) {
+  const utils = api.useUtils()
   const createPreset = api.pcListings.presets.create.useMutation()
   const updatePreset = api.pcListings.presets.update.useMutation()
-  const cpusQuery = api.cpus.get.useQuery({ limit: 500 }) // TODO: make this async
-  const gpusQuery = api.gpus.get.useQuery({ limit: 500 }) // TODO: make this async
 
   const [name, setName] = useState('')
   const [cpuId, setCpuId] = useState('')
@@ -37,6 +43,43 @@ function PcPresetModal(props: Props) {
   const [error, setError] = useState('')
   const [success, setSuccess] = useState('')
 
+  const selectedCpuQuery = api.cpus.getByIds.useQuery(
+    { ids: cpuId ? [cpuId] : [] },
+    { ...LOOKUP_DATA_QUERY_OPTIONS, enabled: cpuId !== '' },
+  )
+  const selectedGpuQuery = api.gpus.getByIds.useQuery(
+    { ids: gpuId ? [gpuId] : [] },
+    { ...LOOKUP_DATA_QUERY_OPTIONS, enabled: gpuId !== '' },
+  )
+
+  const loadCpuItems = useCallback(
+    async (query: string): Promise<CpuOption[]> => {
+      if (query.length < 2) return []
+      try {
+        const result = await utils.cpus.options.fetch({ search: query, limit: 20 })
+        return result.cpus
+      } catch (err) {
+        console.error('Error fetching CPUs:', err)
+        return []
+      }
+    },
+    [utils.cpus.options],
+  )
+
+  const loadGpuItems = useCallback(
+    async (query: string): Promise<GpuOption[]> => {
+      if (query.length < 2) return []
+      try {
+        const result = await utils.gpus.options.fetch({ search: query, limit: 20 })
+        return result.gpus
+      } catch (err) {
+        console.error('Error fetching GPUs:', err)
+        return []
+      }
+    },
+    [utils.gpus.options],
+  )
+
   // Update form fields when preset changes
   useEffect(() => {
     if (props.preset) {
@@ -106,9 +149,9 @@ function PcPresetModal(props: Props) {
     }
   }
 
-  const formatCpuLabel = (cpu: RouterOutput['cpus']['get']['cpus'][number]) =>
+  const formatCpuLabel = (cpu: { brand: { name: string }; modelName: string }) =>
     `${cpu.brand.name} ${cpu.modelName}`
-  const formatGpuLabel = (gpu: RouterOutput['gpus']['get']['gpus'][number]) =>
+  const formatGpuLabel = (gpu: { brand: { name: string }; modelName: string }) =>
     `${gpu.brand.name} ${gpu.modelName}`
 
   return (
@@ -141,14 +184,15 @@ function PcPresetModal(props: Props) {
             CPU
           </label>
           <Autocomplete
-            value={cpuId}
+            value={cpuId || null}
             onChange={(value) => setCpuId(value ?? '')}
-            items={cpusQuery.data?.cpus ?? []}
+            items={selectedCpuQuery.data ?? []}
+            loadItems={loadCpuItems}
             optionToValue={(cpu) => cpu.id}
             optionToLabel={formatCpuLabel}
             placeholder="Select a CPU..."
             className="w-full"
-            filterKeys={['modelName']}
+            minCharsToTrigger={2}
           />
         </div>
 
@@ -157,14 +201,15 @@ function PcPresetModal(props: Props) {
             GPU
           </label>
           <Autocomplete
-            value={gpuId}
+            value={gpuId || null}
             onChange={(value) => setGpuId(value ?? '')}
-            items={gpusQuery.data?.gpus ?? []}
+            items={selectedGpuQuery.data ?? []}
+            loadItems={loadGpuItems}
             optionToValue={(gpu) => gpu.id}
             optionToLabel={formatGpuLabel}
             placeholder="Select a GPU..."
             className="w-full"
-            filterKeys={['modelName']}
+            minCharsToTrigger={2}
           />
         </div>
 
diff --git a/src/app/profile/components/SocSelector.tsx b/src/app/profile/components/SocSelector.tsx
index fb95964be..b2c9f601a 100644
--- a/src/app/profile/components/SocSelector.tsx
+++ b/src/app/profile/components/SocSelector.tsx
@@ -7,6 +7,7 @@ import { Input } from '@/components/ui'
 import { api } from '@/lib/api'
 import { cn } from '@/lib/utils'
 import getErrorMessage from '@/utils/getErrorMessage'
+import { ms } from '@/utils/time'
 
 interface Soc {
   id: string
@@ -19,10 +20,16 @@ interface Props {
   onSocsChange: (socs: Soc[]) => void
 }
 
+const LOOKUP_DATA_QUERY_OPTIONS = {
+  staleTime: ms.hours(6),
+  gcTime: ms.hours(12),
+}
+
 function SocSelector(props: Props) {
   const [searchTerm, setSearchTerm] = useState('')
   const [expandedManufacturers, setExpandedManufacturers] = useState<Set<string>>(new Set())
-  const socsQuery = api.socs.get.useQuery({ limit: 1000 }) // TODO: let's maybe not do this
+  // TODO: Make this selector async instead of preloading 1000 options.
+  const socsQuery = api.socs.options.useQuery({ limit: 1000 }, LOOKUP_DATA_QUERY_OPTIONS)
 
   const filteredSocs = useMemo(() => {
     const allSocs: Soc[] =
diff --git a/src/app/v2/listings/V2ListingsPage.tsx b/src/app/v2/listings/V2ListingsPage.tsx
index 8136c3295..c6bab30b8 100644
--- a/src/app/v2/listings/V2ListingsPage.tsx
+++ b/src/app/v2/listings/V2ListingsPage.tsx
@@ -24,6 +24,12 @@ type SortField = NonNullable<RouterInput['listings']['get']['sortField']>
 
 type ListingType = RouterOutput['listings']['get']['listings'][number]
 
+const LOOKUP_DATA_QUERY_OPTIONS = {
+  staleTime: ms.hours(6),
+  gcTime: ms.hours(12),
+}
+const USE_ASYNC_LISTING_FILTERS = process.env.NEXT_PUBLIC_ENABLE_ASYNC_LISTINGS_FILTERS === 'true'
+
 function V2ListingsPage() {
   const listingsState = useListingsState()
 
@@ -39,7 +45,10 @@ function V2ListingsPage() {
   const [allListings, setAllListings] = useState<ListingType[]>([])
   const [myListingsOnly, setMyListingsOnly] = useState(false)
 
-  const performanceScalesQuery = api.listings.performanceScales.useQuery()
+  const performanceScalesQuery = api.listings.performanceScales.useQuery(
+    undefined,
+    LOOKUP_DATA_QUERY_OPTIONS,
+  )
 
   // User preferences and device filtering
   const userQuery = api.users.me.useQuery()
@@ -287,11 +296,19 @@ function V2ListingsPage() {
     listingsState.setSortDirection(direction || null)
   }
 
-  // Preload filter data with 500 item limits
-  const systemsQuery = api.systems.get.useQuery()
-  const devicesQuery = api.devices.get.useQuery({ limit: 500, offset: 0 })
-  const emulatorsQuery = api.emulators.get.useQuery({ limit: 500, offset: 0 })
-  const socsQuery = api.socs.get.useQuery({ limit: 500, offset: 0 })
+  const systemsQuery = api.systems.get.useQuery(undefined, LOOKUP_DATA_QUERY_OPTIONS)
+  const devicesQuery = api.devices.options.useQuery(
+    { limit: 500, offset: 0 },
+    { ...LOOKUP_DATA_QUERY_OPTIONS, enabled: !USE_ASYNC_LISTING_FILTERS },
+  )
+  const emulatorsQuery = api.emulators.get.useQuery(
+    { limit: 500, offset: 0 },
+    LOOKUP_DATA_QUERY_OPTIONS,
+  )
+  const socsQuery = api.socs.options.useQuery(
+    { limit: 500, offset: 0 },
+    { ...LOOKUP_DATA_QUERY_OPTIONS, enabled: !USE_ASYNC_LISTING_FILTERS },
+  )
 
   // Transform preloaded data into options format
   const systemOpts = useMemo(
@@ -404,6 +421,7 @@ function V2ListingsPage() {
             performanceIds={listingsState.performanceIds.map(String)}
             handlePerformanceChange={(values) => handlePerformanceChange(values.map(Number))}
             performanceScales={performanceScalesQuery.data}
+            useAsyncHardwareFilters={USE_ASYNC_LISTING_FILTERS}
             deviceIds={listingsState.deviceIds}
             handleDeviceChange={handleDeviceChange}
             deviceOptions={deviceOpts}
diff --git a/src/app/v2/listings/components/ListingFilters.tsx b/src/app/v2/listings/components/ListingFilters.tsx
index fc5312b9d..97a95de76 100644
--- a/src/app/v2/listings/components/ListingFilters.tsx
+++ b/src/app/v2/listings/components/ListingFilters.tsx
@@ -13,6 +13,8 @@ import {
   Search,
 } from 'lucide-react'
 import { useState, useEffect, type ReactNode } from 'react'
+import AsyncDeviceFilterSelect from '@/app/listings/components/filters/AsyncDeviceFilterSelect'
+import AsyncSocFilterSelect from '@/app/listings/components/filters/AsyncSocFilterSelect'
 import { MultiSelect, Button, Input, Badge } from '@/components/ui'
 import analytics from '@/lib/analytics'
 import { cn } from '@/lib/utils'
@@ -45,6 +47,7 @@ interface Props {
   handlePerformanceChange: (values: string[]) => void
   performanceScales: PerformanceScale[] | undefined
   // Device filters
+  useAsyncHardwareFilters: boolean
   deviceIds: string[]
   handleDeviceChange: (values: string[]) => void
   deviceOptions: { id: string; name: string }[] | undefined
@@ -345,18 +348,31 @@ export function ListingFilters(props: Props) {
                               />
                             )}
 
-                            {section.id === 'devices' && props.deviceOptions && (
-                              <MultiSelect
+                            {section.id === 'devices' && props.useAsyncHardwareFilters && (
+                              <AsyncDeviceFilterSelect
                                 label="Devices"
                                 value={props.deviceIds}
                                 onChange={props.handleDeviceChange}
                                 placeholder="Select devices..."
-                                options={props.deviceOptions}
                                 maxDisplayed={3}
                                 className="mobile-optimized"
                               />
                             )}
 
+                            {section.id === 'devices' &&
+                              !props.useAsyncHardwareFilters &&
+                              props.deviceOptions && (
+                                <MultiSelect
+                                  label="Devices"
+                                  value={props.deviceIds}
+                                  onChange={props.handleDeviceChange}
+                                  placeholder="Select devices..."
+                                  options={props.deviceOptions}
+                                  maxDisplayed={3}
+                                  className="mobile-optimized"
+                                />
+                              )}
+
                             {section.id === 'emulators' && props.emulatorOptions && (
                               <MultiSelect
                                 label="Emulators"
@@ -369,17 +385,30 @@ export function ListingFilters(props: Props) {
                               />
                             )}
 
-                            {section.id === 'socs' && props.socOptions && (
-                              <MultiSelect
+                            {section.id === 'socs' && props.useAsyncHardwareFilters && (
+                              <AsyncSocFilterSelect
                                 label="SoCs"
                                 value={props.socIds}
                                 onChange={props.handleSocChange}
                                 placeholder="Select system on chips..."
-                                options={props.socOptions}
                                 maxDisplayed={3}
                                 className="mobile-optimized"
                               />
                             )}
+
+                            {section.id === 'socs' &&
+                              !props.useAsyncHardwareFilters &&
+                              props.socOptions && (
+                                <MultiSelect
+                                  label="SoCs"
+                                  value={props.socIds}
+                                  onChange={props.handleSocChange}
+                                  placeholder="Select system on chips..."
+                                  options={props.socOptions}
+                                  maxDisplayed={3}
+                                  className="mobile-optimized"
+                                />
+                              )}
                           </motion.div>
                         )}
                       </AnimatePresence>
diff --git a/src/components/popups/BetaWarningPopup.tsx b/src/components/popups/BetaWarningPopup.tsx
index 262351279..8b1721d84 100644
--- a/src/components/popups/BetaWarningPopup.tsx
+++ b/src/components/popups/BetaWarningPopup.tsx
@@ -4,6 +4,7 @@ import { AlertTriangle, X } from 'lucide-react'
 import { useState, useEffect } from 'react'
 import { Modal } from '@/components/ui'
 import storageKeys from '@/data/storageKeys'
+import { env } from '@/lib/env'
 
 const discordUrl = process.env.NEXT_PUBLIC_DISCORD_LINK
 
@@ -11,8 +12,7 @@ export function BetaWarningPopup() {
   const [isOpen, setIsOpen] = useState(false)
 
   useEffect(() => {
-    // Only show in production
-    if (process.env.NODE_ENV !== 'production') return
+    if (!env.IS_PUBLIC_PRODUCTION) return
 
     // Don't show on admin pages
     if (window.location.pathname.startsWith('/admin')) return
@@ -34,7 +34,7 @@ export function BetaWarningPopup() {
     setIsOpen(false)
   }
 
-  if (process.env.NODE_ENV !== 'production' || !isOpen) return null
+  if (!env.IS_PUBLIC_PRODUCTION || !isOpen) return null
 
   return (
     <Modal isOpen={isOpen} onClose={handleDismiss} hideCloseButton={true} className="max-w-lg">
diff --git a/src/components/popups/StopKillingGamesPopup.tsx b/src/components/popups/StopKillingGamesPopup.tsx
index 4409362cd..176f871f6 100644
--- a/src/components/popups/StopKillingGamesPopup.tsx
+++ b/src/components/popups/StopKillingGamesPopup.tsx
@@ -5,9 +5,11 @@ import { useState, useEffect, useRef } from 'react'
 import { Modal } from '@/components/ui'
 import storageKeys from '@/data/storageKeys'
 import analytics from '@/lib/analytics'
+import { env } from '@/lib/env'
 
 const signPetitionUrl = 'https://eci.ec.europa.eu/045/public/#/screen/home'
 
+// TODO: check if we still need this for something
 export function StopKillingGamesPopup() {
   const [isOpen, setIsOpen] = useState(false)
 
@@ -20,8 +22,7 @@ export function StopKillingGamesPopup() {
   }
 
   useEffect(() => {
-    // Only show in production
-    if (process.env.NODE_ENV !== 'production') return
+    if (!env.IS_PUBLIC_PRODUCTION) return
 
     // Don't show on admin pages
     if (window.location.pathname.startsWith('/admin')) return
@@ -52,7 +53,7 @@ export function StopKillingGamesPopup() {
     window.open(signPetitionUrl, '_blank', 'noopener,noreferrer')
   }
 
-  if (process.env.NODE_ENV !== 'production' || !isOpen) return null
+  if (!env.IS_PUBLIC_PRODUCTION || !isOpen) return null
 
   return (
     <Modal isOpen={isOpen} onClose={handleDismiss} hideCloseButton={true} className="max-w-lg">
diff --git a/src/components/ui/KofiWidget.tsx b/src/components/ui/KofiWidget.tsx
index 59a945dc5..8b35ac859 100644
--- a/src/components/ui/KofiWidget.tsx
+++ b/src/components/ui/KofiWidget.tsx
@@ -6,17 +6,18 @@ import useMounted from '@/hooks/useMounted'
 function KofiWidget() {
   const mounted = useMounted()
 
-  if (!mounted) return
+  if (!mounted) return null
 
   const isMobile =
     window.innerWidth <= 768 ||
     /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
 
-  if (isMobile) return
+  if (isMobile) return null
 
   return (
     <Script
       src="https://storage.ko-fi.com/cdn/scripts/overlay-widget.js"
+      strategy="lazyOnload"
       onLoad={() => {
         if (window.kofiWidgetOverlay) {
           window.kofiWidgetOverlay.draw('producdevity', {
diff --git a/src/components/ui/form/AsyncCpuMultiSelect.tsx b/src/components/ui/form/AsyncCpuMultiSelect.tsx
deleted file mode 100644
index d77d164f0..000000000
--- a/src/components/ui/form/AsyncCpuMultiSelect.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-'use client'
-
-import { type ReactNode, useEffect, useState } from 'react'
-import AsyncMultiSelect, {
-  type Option,
-} from '@/components/ui/form/async-multi-select/AsyncMultiSelect'
-import { api } from '@/lib/api'
-
-interface Props {
-  label: string
-  leftIcon?: ReactNode
-  value: string[]
-  onChange: (values: string[]) => void
-  placeholder?: string
-  className?: string
-  maxDisplayed?: number
-}
-
-const PAGE_SIZE = 50
-
-export default function AsyncCpuMultiSelect(props: Props) {
-  const [query, setQuery] = useState('')
-  const [offset, setOffset] = useState(0)
-  const [options, setOptions] = useState<Option[]>([])
-  const [hasMore, setHasMore] = useState(true)
-
-  const byIdsQuery = api.cpus.getByIds.useQuery(
-    { ids: props.value },
-    { enabled: props.value.length > 0, staleTime: 300000 },
-  )
-
-  const pageQuery = api.cpus.get.useQuery(
-    { search: query || undefined, limit: PAGE_SIZE, offset },
-    { enabled: true, staleTime: 30000 },
-  )
-
-  useEffect(() => {
-    if (!pageQuery.data) return
-    const { cpus, pagination } = pageQuery.data
-    const mapped = cpus.map((c) => ({
-      id: c.id,
-      name: `${c.brand.name} ${c.modelName}`,
-      badgeName: c.modelName,
-    }))
-    setOptions((prev) => (offset === 0 ? mapped : [...prev, ...mapped]))
-    setHasMore(pagination.page < pagination.pages)
-  }, [pageQuery.data, offset])
-
-  const selectedByIds = (byIdsQuery.data || []).map((c) => ({
-    id: c.id,
-    name: `${c.brand.name} ${c.modelName}`,
-    badgeName: c.modelName,
-  }))
-
-  return (
-    <AsyncMultiSelect
-      {...props}
-      options={options}
-      selectedByIds={selectedByIds}
-      isFetching={pageQuery.isFetching}
-      hasMore={hasMore}
-      onLoadMore={() => setOffset((o) => o + PAGE_SIZE)}
-      onQueryChange={(q) => {
-        setQuery(q)
-        setOffset(0)
-        setOptions([])
-      }}
-    />
-  )
-}
diff --git a/src/components/ui/form/AsyncDeviceMultiSelect.tsx b/src/components/ui/form/AsyncDeviceMultiSelect.tsx
deleted file mode 100644
index b20c41c3c..000000000
--- a/src/components/ui/form/AsyncDeviceMultiSelect.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-'use client'
-
-import { type ReactNode, useEffect, useState } from 'react'
-import AsyncMultiSelect, {
-  type Option,
-} from '@/components/ui/form/async-multi-select/AsyncMultiSelect'
-import { api } from '@/lib/api'
-
-interface Props {
-  label: string
-  leftIcon?: ReactNode
-  value: string[]
-  onChange: (values: string[]) => void
-  placeholder?: string
-  className?: string
-  maxDisplayed?: number
-}
-
-const PAGE_SIZE = 50
-
-export default function AsyncDeviceMultiSelect(props: Props) {
-  const [query, setQuery] = useState('')
-  const [offset, setOffset] = useState(0)
-  const [options, setOptions] = useState<Option[]>([])
-  const [hasMore, setHasMore] = useState(true)
-
-  const byIdsQuery = api.devices.getByIds.useQuery(
-    { ids: props.value },
-    { enabled: props.value.length > 0, staleTime: 300000 },
-  )
-
-  const pageQuery = api.devices.get.useQuery(
-    { search: query || undefined, limit: PAGE_SIZE, offset },
-    { enabled: true, staleTime: 30000 },
-  )
-
-  useEffect(() => {
-    if (!pageQuery.data) return
-    const { devices, pagination } = pageQuery.data
-    const mapped = devices.map((d) => ({
-      id: d.id,
-      name: `${d.brand.name} ${d.modelName}`,
-      badgeName: d.modelName,
-    }))
-    setOptions((prev) => (offset === 0 ? mapped : [...prev, ...mapped]))
-    setHasMore(pagination.page < pagination.pages)
-  }, [pageQuery.data, offset])
-
-  const selectedByIds = (byIdsQuery.data || []).map((d) => ({
-    id: d.id,
-    name: `${d.brand.name} ${d.modelName}`,
-    badgeName: d.modelName,
-  }))
-
-  return (
-    <AsyncMultiSelect
-      {...props}
-      options={options}
-      selectedByIds={selectedByIds}
-      isFetching={pageQuery.isFetching}
-      hasMore={hasMore}
-      onLoadMore={() => setOffset((o) => o + PAGE_SIZE)}
-      onQueryChange={(q) => {
-        setQuery(q)
-        setOffset(0)
-        setOptions([])
-      }}
-    />
-  )
-}
diff --git a/src/components/ui/form/AsyncGpuMultiSelect.tsx b/src/components/ui/form/AsyncGpuMultiSelect.tsx
deleted file mode 100644
index 98b003e7b..000000000
--- a/src/components/ui/form/AsyncGpuMultiSelect.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-'use client'
-
-import { type ReactNode, useEffect, useState } from 'react'
-import AsyncMultiSelect, {
-  type Option,
-} from '@/components/ui/form/async-multi-select/AsyncMultiSelect'
-import { api } from '@/lib/api'
-
-interface Props {
-  label: string
-  leftIcon?: ReactNode
-  value: string[]
-  onChange: (values: string[]) => void
-  placeholder?: string
-  className?: string
-  maxDisplayed?: number
-}
-
-const PAGE_SIZE = 50
-
-export default function AsyncGpuMultiSelect(props: Props) {
-  const [query, setQuery] = useState('')
-  const [offset, setOffset] = useState(0)
-  const [options, setOptions] = useState<Option[]>([])
-  const [hasMore, setHasMore] = useState(true)
-
-  const byIdsQuery = api.gpus.getByIds.useQuery(
-    { ids: props.value },
-    { enabled: props.value.length > 0, staleTime: 300000 },
-  )
-
-  const pageQuery = api.gpus.get.useQuery(
-    { search: query || undefined, limit: PAGE_SIZE, offset },
-    { enabled: true, staleTime: 30000 },
-  )
-
-  useEffect(() => {
-    if (!pageQuery.data) return
-    const { gpus, pagination } = pageQuery.data
-    const mapped = gpus.map((g) => ({
-      id: g.id,
-      name: `${g.brand.name} ${g.modelName}`,
-      badgeName: g.modelName,
-    }))
-    setOptions((prev) => (offset === 0 ? mapped : [...prev, ...mapped]))
-    setHasMore(pagination.page < pagination.pages)
-  }, [pageQuery.data, offset])
-
-  const selectedByIds = (byIdsQuery.data || []).map((g) => ({
-    id: g.id,
-    name: `${g.brand.name} ${g.modelName}`,
-    badgeName: g.modelName,
-  }))
-
-  return (
-    <AsyncMultiSelect
-      {...props}
-      options={options}
-      selectedByIds={selectedByIds}
-      isFetching={pageQuery.isFetching}
-      hasMore={hasMore}
-      onLoadMore={() => setOffset((o) => o + PAGE_SIZE)}
-      onQueryChange={(q) => {
-        setQuery(q)
-        setOffset(0)
-        setOptions([])
-      }}
-    />
-  )
-}
diff --git a/src/components/ui/form/AsyncMultiSelect.tsx b/src/components/ui/form/AsyncMultiSelect.tsx
deleted file mode 100644
index 5eb5492a0..000000000
--- a/src/components/ui/form/AsyncMultiSelect.tsx
+++ /dev/null
@@ -1,231 +0,0 @@
-'use client'
-
-import { Command as CommandPrimitive } from 'cmdk'
-import { ChevronsUpDown, X, Search } from 'lucide-react'
-import { useState, useEffect, useRef, useCallback } from 'react'
-import {
-  Button,
-  Badge,
-  Popover,
-  PopoverContent,
-  PopoverTrigger,
-  LoadingSpinner,
-} from '@/components/ui'
-import { cn } from '@/lib/utils'
-
-export interface AsyncOption {
-  id: string
-  name: string
-}
-
-export interface AsyncMultiSelectProps {
-  label: string
-  value: string[]
-  onChange: (value: string[]) => void
-  loadOptions: (inputValue: string) => Promise<AsyncOption[]>
-  placeholder?: string
-  emptyMessage?: string
-  className?: string
-  maxSelected?: number
-}
-
-export function AsyncMultiSelect({
-  label,
-  value = [],
-  onChange,
-  loadOptions,
-  placeholder = 'Select options',
-  emptyMessage = 'No options found.',
-  className,
-  maxSelected,
-}: AsyncMultiSelectProps) {
-  const [options, setOptions] = useState<AsyncOption[]>([])
-  const [inputValue, setInputValue] = useState<string>('')
-  const [loading, setLoading] = useState<boolean>(false)
-  const [error, setError] = useState<string | null>(null)
-  const [open, setOpen] = useState<boolean>(false)
-  const debouncedFetchRef = useRef<NodeJS.Timeout | null>(null)
-
-  const fetchOptions = useCallback(
-    async (query: string) => {
-      setLoading(true)
-      setError(null)
-
-      try {
-        const results = await loadOptions(query)
-        setOptions(results)
-      } catch (err) {
-        setError('Failed to load options')
-        console.error('Error loading options:', err)
-      } finally {
-        setLoading(false)
-      }
-    },
-    [loadOptions],
-  )
-
-  useEffect(() => {
-    if (open) {
-      fetchOptions(inputValue)
-    }
-
-    const ref = debouncedFetchRef.current
-    return () => {
-      if (ref) {
-        clearTimeout(ref)
-      }
-    }
-  }, [open, inputValue, fetchOptions])
-
-  const handleSelect = useCallback(
-    (optionId: string) => {
-      if (value.includes(optionId)) {
-        onChange(value.filter((item) => item !== optionId))
-      } else {
-        if (maxSelected && value.length >= maxSelected) {
-          return
-        }
-        onChange([...value, optionId])
-      }
-    },
-    [onChange, value, maxSelected],
-  )
-
-  const handleRemove = useCallback(
-    (optionId: string) => {
-      onChange(value.filter((item) => item !== optionId))
-    },
-    [onChange, value],
-  )
-
-  const selectedOptions = options.filter((option) => value.includes(option.id))
-
-  return (
-    <div className={className}>
-      <Popover open={open} onOpenChange={setOpen}>
-        <PopoverTrigger asChild>
-          <Button
-            variant="outline"
-            role="combobox"
-            aria-expanded={open}
-            className="w-full justify-between"
-          >
-            <span className="truncate">
-              {value.length > 0 ? `${value.length} selected` : placeholder}
-            </span>
-            <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
-          </Button>
-        </PopoverTrigger>
-        <PopoverContent className="w-full min-w-[200px] p-0" align="start">
-          <div className="flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground">
-            <div className="flex items-center border-b px-3" cmdk-input-wrapper="">
-              <Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
-              <CommandPrimitive.Input
-                placeholder={`Search ${label.toLowerCase()}...`}
-                value={inputValue}
-                onValueChange={setInputValue}
-                className="flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50"
-              />
-            </div>
-            <CommandPrimitive.List className="max-h-[300px] overflow-y-auto overflow-x-hidden">
-              {error ? (
-                <CommandPrimitive.Empty className="py-6 text-center text-sm">
-                  <div className="text-red-500">{error}</div>
-                  <Button
-                    variant="outline"
-                    size="sm"
-                    className="mt-2"
-                    onClick={() => fetchOptions(inputValue)}
-                  >
-                    Retry
-                  </Button>
-                </CommandPrimitive.Empty>
-              ) : options.length === 0 && !loading ? (
-                <CommandPrimitive.Empty className="py-6 text-center text-sm">
-                  {emptyMessage}
-                </CommandPrimitive.Empty>
-              ) : (
-                <CommandPrimitive.Group>
-                  {value.length > 0 && (
-                    <div className="flex items-center justify-between px-2 py-1.5 text-sm border-b">
-                      <span className="font-medium">Selected</span>
-                      <Button
-                        variant="ghost"
-                        size="sm"
-                        onClick={() => onChange([])}
-                        className="h-auto p-1"
-                      >
-                        Clear all
-                      </Button>
-                    </div>
-                  )}
-
-                  {loading ? (
-                    <div className="flex items-center justify-center py-6">
-                      <LoadingSpinner />
-                    </div>
-                  ) : (
-                    options.map((option) => (
-                      <CommandPrimitive.Item
-                        key={option.id}
-                        value={option.id}
-                        onSelect={() => handleSelect(option.id)}
-                        className="relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50"
-                      >
-                        <div
-                          className={cn(
-                            'mr-2 h-4 w-4',
-                            value.includes(option.id) ? 'opacity-100' : 'opacity-0',
-                          )}
-                        >
-                          {value.includes(option.id) && (
-                            <svg
-                              xmlns="http://www.w3.org/2000/svg"
-                              viewBox="0 0 24 24"
-                              fill="none"
-                              stroke="currentColor"
-                              strokeWidth="2"
-                              strokeLinecap="round"
-                              strokeLinejoin="round"
-                              className="h-4 w-4"
-                            >
-                              <polyline points="20 6 9 17 4 12" />
-                            </svg>
-                          )}
-                        </div>
-                        {option.name}
-                      </CommandPrimitive.Item>
-                    ))
-                  )}
-
-                  {maxSelected && value.length >= maxSelected && (
-                    <div className="px-2 py-1.5 text-xs text-muted-foreground">
-                      Max {maxSelected} items selected
-                    </div>
-                  )}
-                </CommandPrimitive.Group>
-              )}
-            </CommandPrimitive.List>
-          </div>
-        </PopoverContent>
-      </Popover>
-      {value.length > 0 && (
-        <div className="flex flex-wrap gap-1 mt-2">
-          {selectedOptions.map((option) => (
-            <Badge key={option.id}>
-              {option.name}
-              <button
-                type="button"
-                className="ml-1 rounded-full outline-none ring-offset-background focus:ring-2 focus:ring-ring focus:ring-offset-2"
-                onClick={() => handleRemove(option.id)}
-              >
-                <X className="h-3 w-3" />
-                <span className="sr-only">Remove {option.name}</span>
-              </button>
-            </Badge>
-          ))}
-        </div>
-      )}
-    </div>
-  )
-}
diff --git a/src/components/ui/form/AsyncSocMultiSelect.tsx b/src/components/ui/form/AsyncSocMultiSelect.tsx
deleted file mode 100644
index 9b2e98ce9..000000000
--- a/src/components/ui/form/AsyncSocMultiSelect.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-'use client'
-
-import { type ReactNode, useEffect, useState } from 'react'
-import AsyncMultiSelect, {
-  type Option,
-} from '@/components/ui/form/async-multi-select/AsyncMultiSelect'
-import { api } from '@/lib/api'
-
-interface Props {
-  label: string
-  leftIcon?: ReactNode
-  value: string[]
-  onChange: (values: string[]) => void
-  placeholder?: string
-  className?: string
-  maxDisplayed?: number
-}
-
-const PAGE_SIZE = 50
-
-export default function AsyncSocMultiSelect(props: Props) {
-  const [query, setQuery] = useState('')
-  const [offset, setOffset] = useState(0)
-  const [options, setOptions] = useState<Option[]>([])
-  const [hasMore, setHasMore] = useState(true)
-
-  const byIdsQuery = api.socs.getByIds.useQuery(
-    { ids: props.value },
-    { enabled: props.value.length > 0, staleTime: 300000 },
-  )
-
-  const pageQuery = api.socs.get.useQuery(
-    { search: query || undefined, limit: PAGE_SIZE, offset },
-    { enabled: true, staleTime: 30000 },
-  )
-
-  useEffect(() => {
-    if (!pageQuery.data) return
-    const { socs, pagination } = pageQuery.data
-    const mapped = socs.map((s) => ({
-      id: s.id,
-      name: `${s.manufacturer} ${s.name}`,
-      badgeName: s.name,
-    }))
-    setOptions((prev) => (offset === 0 ? mapped : [...prev, ...mapped]))
-    setHasMore(pagination.page < pagination.pages)
-  }, [pageQuery.data, offset])
-
-  const selectedByIds = (byIdsQuery.data || []).map((s) => ({
-    id: s.id,
-    name: `${s.manufacturer} ${s.name}`,
-    badgeName: s.name,
-  }))
-
-  return (
-    <AsyncMultiSelect
-      {...props}
-      options={options}
-      selectedByIds={selectedByIds}
-      isFetching={pageQuery.isFetching}
-      hasMore={hasMore}
-      onLoadMore={() => setOffset((o) => o + PAGE_SIZE)}
-      onQueryChange={(q) => {
-        setQuery(q)
-        setOffset(0)
-        setOptions([])
-      }}
-    />
-  )
-}
diff --git a/src/components/ui/form/MultiSelect.test.tsx b/src/components/ui/form/MultiSelect.test.tsx
index e17e2a9af..7db00c560 100644
--- a/src/components/ui/form/MultiSelect.test.tsx
+++ b/src/components/ui/form/MultiSelect.test.tsx
@@ -1,5 +1,5 @@
 import { render, screen, fireEvent, waitFor } from '@testing-library/react'
-import { describe, it, expect, vi } from 'vitest'
+import { describe, it, expect, vi, afterEach } from 'vitest'
 import { MultiSelect } from './MultiSelect'
 
 const mockOptions = [
@@ -10,6 +10,11 @@ const mockOptions = [
 ]
 
 describe('MultiSelect', () => {
+  afterEach(() => {
+    vi.restoreAllMocks()
+    vi.unstubAllGlobals()
+  })
+
   it('renders with label and placeholder', () => {
     const onChange = vi.fn()
     render(
@@ -189,7 +194,7 @@ describe('MultiSelect', () => {
       expect(screen.getByPlaceholderText('Search test select...')).toBeInTheDocument()
     })
 
-    fireEvent.mouseDown(screen.getByTestId('outside'))
+    fireEvent.click(screen.getByTestId('outside'))
 
     await waitFor(() => {
       expect(screen.queryByPlaceholderText('Search test select...')).not.toBeInTheDocument()
@@ -313,6 +318,56 @@ describe('MultiSelect', () => {
     expect(searchInput).toHaveValue('')
   })
 
+  it('opens below the trigger when there is enough visible space', async () => {
+    const onChange = vi.fn()
+    vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockReturnValue({
+      bottom: 150,
+      height: 40,
+      left: 0,
+      right: 320,
+      top: 110,
+      width: 320,
+      x: 0,
+      y: 110,
+      toJSON: () => ({}),
+    })
+
+    render(<MultiSelect label="Test Select" value={[]} onChange={onChange} options={mockOptions} />)
+
+    fireEvent.click(screen.getByRole('button', { expanded: false }))
+
+    const optionList = await screen.findByTestId('multi-select-options')
+    expect(optionList.parentElement).toHaveClass('top-full')
+  })
+
+  it('opens above the trigger when the visual viewport is constrained below it', async () => {
+    const onChange = vi.fn()
+    vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockReturnValue({
+      bottom: 350,
+      height: 40,
+      left: 0,
+      right: 320,
+      top: 310,
+      width: 320,
+      x: 0,
+      y: 310,
+      toJSON: () => ({}),
+    })
+    vi.stubGlobal('visualViewport', {
+      addEventListener: vi.fn(),
+      height: 360,
+      offsetTop: 0,
+      removeEventListener: vi.fn(),
+    })
+
+    render(<MultiSelect label="Test Select" value={[]} onChange={onChange} options={mockOptions} />)
+
+    fireEvent.click(screen.getByRole('button', { expanded: false }))
+
+    const optionList = await screen.findByTestId('multi-select-options')
+    expect(optionList.parentElement).toHaveClass('bottom-full')
+  })
+
   it('displays selected badges by default', () => {
     const onChange = vi.fn()
     render(
diff --git a/src/components/ui/form/MultiSelect.tsx b/src/components/ui/form/MultiSelect.tsx
index 2012a97af..0b6c9ab06 100644
--- a/src/components/ui/form/MultiSelect.tsx
+++ b/src/components/ui/form/MultiSelect.tsx
@@ -1,7 +1,15 @@
 'use client'
 
 import { ChevronDown, X } from 'lucide-react'
-import { useState, useRef, useEffect, useMemo, type KeyboardEvent, type ReactNode } from 'react'
+import {
+  type KeyboardEvent,
+  type ReactNode,
+  useCallback,
+  useEffect,
+  useMemo,
+  useRef,
+  useState,
+} from 'react'
 import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui'
 import { cn } from '@/lib/utils'
 import { searchItems } from '@/utils/simpleSearch'
@@ -38,6 +46,12 @@ const badgeColors = {
   red: 'bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-200 border-red-200 dark:border-red-800 hover:bg-red-200 dark:hover:bg-red-900/50',
 }
 
+const MOBILE_LIST_MAX_HEIGHT_PX = 192
+const DESKTOP_LIST_MAX_HEIGHT_PX = 320
+const SEARCH_HEADER_HEIGHT_PX = 64
+const FOOTER_HEIGHT_PX = 48
+const OPTION_ROW_HEIGHT_ESTIMATE_PX = 40
+
 export function MultiSelect(props: Props) {
   const [isOpen, setIsOpen] = useState(false)
   const [searchQuery, setSearchQuery] = useState('')
@@ -70,45 +84,63 @@ export function MultiSelect(props: Props) {
   const maxDisplayed = props.maxDisplayed ?? 2
   const showSelectedBadges = props.showSelectedBadges ?? true
 
-  // Calculate dropdown position based on available space
+  const closeDropdown = useCallback((restoreFocus: boolean) => {
+    setIsOpen(false)
+    setSearchQuery('')
+    if (restoreFocus) buttonRef.current?.focus()
+  }, [])
+
   useEffect(() => {
-    if (isOpen && buttonRef.current) {
+    if (!isOpen) return
+
+    function updateDropdownPosition() {
+      if (!buttonRef.current) return
+
+      const visualViewport = window.visualViewport
+      const visibleTop = visualViewport?.offsetTop ?? 0
+      const visibleHeight = visualViewport?.height ?? window.innerHeight
+      const visibleBottom = visibleTop + visibleHeight
       const buttonRect = buttonRef.current.getBoundingClientRect()
-      const viewportHeight = window.innerHeight
-      const spaceBelow = viewportHeight - buttonRect.bottom
-      const spaceAbove = buttonRect.top
-
-      // Estimate dropdown height (max-height is 320px + padding)
-      const estimatedDropdownHeight = Math.min(360, filteredOptions.length * 40 + 100)
-
-      // Position upward if there's not enough space below but enough space above
-      if (spaceBelow < estimatedDropdownHeight && spaceAbove > estimatedDropdownHeight) {
-        setDropdownPosition('top')
-      } else {
-        setDropdownPosition('bottom')
-      }
+      const listMaxHeight = window.matchMedia('(min-width: 640px)').matches
+        ? DESKTOP_LIST_MAX_HEIGHT_PX
+        : MOBILE_LIST_MAX_HEIGHT_PX
+      const footerHeight = selectedOptions.length > 0 ? FOOTER_HEIGHT_PX : 0
+      const estimatedPanelHeight =
+        Math.min(listMaxHeight, sortedFilteredOptions.length * OPTION_ROW_HEIGHT_ESTIMATE_PX) +
+        SEARCH_HEADER_HEIGHT_PX +
+        footerHeight
+      const spaceBelow = visibleBottom - buttonRect.bottom
+      const spaceAbove = buttonRect.top - visibleTop
+
+      setDropdownPosition(
+        spaceBelow < estimatedPanelHeight && spaceAbove > spaceBelow ? 'top' : 'bottom',
+      )
+    }
+
+    updateDropdownPosition()
+    window.addEventListener('resize', updateDropdownPosition)
+    window.visualViewport?.addEventListener('resize', updateDropdownPosition)
+    window.visualViewport?.addEventListener('scroll', updateDropdownPosition)
+
+    return () => {
+      window.removeEventListener('resize', updateDropdownPosition)
+      window.visualViewport?.removeEventListener('resize', updateDropdownPosition)
+      window.visualViewport?.removeEventListener('scroll', updateDropdownPosition)
     }
-  }, [isOpen, filteredOptions.length])
+  }, [isOpen, selectedOptions.length, sortedFilteredOptions.length])
 
   useEffect(() => {
     function handleClickOutside(event: MouseEvent) {
       if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
-        setIsOpen(false)
-        setSearchQuery('')
+        closeDropdown(false)
       }
     }
 
-    document.addEventListener('mousedown', handleClickOutside)
+    document.addEventListener('click', handleClickOutside)
     return () => {
-      document.removeEventListener('mousedown', handleClickOutside)
+      document.removeEventListener('click', handleClickOutside)
     }
-  }, [])
-
-  const closeDropdown = () => {
-    setIsOpen(false)
-    setSearchQuery('')
-    buttonRef.current?.focus()
-  }
+  }, [closeDropdown])
 
   const handleToggle = (optionId: string) => {
     const newValue = props.value.includes(optionId)
@@ -131,7 +163,7 @@ export function MultiSelect(props: Props) {
 
     ev.preventDefault()
     ev.stopPropagation()
-    closeDropdown()
+    closeDropdown(true)
   }
 
   const handleClearSearch = () => {
@@ -228,11 +260,12 @@ export function MultiSelect(props: Props) {
 
         {isOpen && (
           <div
-            className={`absolute z-[9999] w-full bg-white dark:bg-gray-800
+            className={cn(
+              `absolute left-0 z-[9999] w-full bg-white dark:bg-gray-800
               border border-gray-300 dark:border-gray-600 rounded-lg shadow-lg
-              animate-in fade-in-0 zoom-in-95 duration-200 ${
-                dropdownPosition === 'top' ? 'bottom-full mb-1' : 'top-full mt-1'
-              }`}
+              animate-in fade-in-0 zoom-in-95 duration-200`,
+              dropdownPosition === 'top' ? 'bottom-full mb-1' : 'top-full mt-1',
+            )}
           >
             <div className="p-2 border-b border-gray-200 dark:border-gray-700">
               <div className="relative">
@@ -264,7 +297,10 @@ export function MultiSelect(props: Props) {
               </div>
             </div>
 
-            <div className="max-h-80 overflow-y-auto p-1">
+            <div
+              className="scrollbar-native-thin max-h-48 overflow-y-auto overflow-x-hidden overscroll-contain p-1 sm:max-h-80"
+              data-testid="multi-select-options"
+            >
               {sortedFilteredOptions.length === 0 ? (
                 <div className="px-3 py-2 text-sm text-gray-500 dark:text-gray-400">
                   No options found
@@ -277,11 +313,10 @@ export function MultiSelect(props: Props) {
                   return (
                     <label
                       key={option.id}
-                      className={`flex items-center gap-3 px-3 py-2 rounded-md cursor-pointer
+                      className={`flex min-w-0 items-center gap-3 px-3 py-2 rounded-md cursor-pointer
                         transition-all duration-200 hover:bg-gray-50 dark:hover:bg-gray-700
                         ${isSelected ? 'bg-blue-50 dark:bg-blue-900/20' : ''}
-                        ${isTopSelected ? 'animate-in slide-in-from-top-1 duration-300' : ''}
-                        hover:scale-[1.02] hover:shadow-sm`}
+                        ${isTopSelected ? 'animate-in slide-in-from-top-1 duration-300' : ''}`}
                       style={{
                         animationDelay: isTopSelected ? `${index * 50}ms` : '0ms',
                       }}
@@ -297,7 +332,7 @@ export function MultiSelect(props: Props) {
                       />
                       <span
                         className={cn(
-                          'text-sm select-none flex-1 transition-all duration-200',
+                          'min-w-0 flex-1 break-words text-sm select-none transition-all duration-200',
                           isSelected
                             ? 'text-blue-700 dark:text-blue-300 font-medium'
                             : 'text-gray-700 dark:text-gray-300',
diff --git a/src/components/ui/form/async-multi-select/AsyncMultiSelect.test.tsx b/src/components/ui/form/async-multi-select/AsyncMultiSelect.test.tsx
new file mode 100644
index 000000000..d33a2046b
--- /dev/null
+++ b/src/components/ui/form/async-multi-select/AsyncMultiSelect.test.tsx
@@ -0,0 +1,354 @@
+import { render, screen, fireEvent, act } from '@testing-library/react'
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
+import AsyncMultiSelect, { type Option } from './AsyncMultiSelect'
+
+const options: Option[] = [
+  { id: 'device-1', name: 'Retroid Pocket 5', badgeName: 'Pocket 5' },
+  { id: 'device-2', name: 'AYN Odin 2', badgeName: 'Odin 2' },
+]
+
+type AsyncMultiSelectProps = Parameters<typeof AsyncMultiSelect>[0]
+
+function renderSelect(overrides: Partial<AsyncMultiSelectProps> = {}) {
+  const props: AsyncMultiSelectProps = {
+    label: 'Devices',
+    value: [],
+    onChange: vi.fn(),
+    options,
+    selectedByIds: [],
+    isFetching: false,
+    hasMore: false,
+    debounceMs: 10,
+    ...overrides,
+  }
+
+  render(<AsyncMultiSelect {...props} />)
+  return props
+}
+
+describe('AsyncMultiSelect', () => {
+  beforeEach(() => {
+    vi.useFakeTimers()
+  })
+
+  afterEach(() => {
+    vi.runOnlyPendingTimers()
+    vi.useRealTimers()
+    vi.restoreAllMocks()
+    vi.unstubAllGlobals()
+  })
+
+  it('renders selected labels from selectedByIds when the selected option is not on the current page', () => {
+    renderSelect({
+      value: ['device-99'],
+      options: [],
+      selectedByIds: [{ id: 'device-99', name: 'Steam Deck OLED', badgeName: 'Deck OLED' }],
+    })
+
+    expect(screen.getByText('Steam Deck OLED')).toBeInTheDocument()
+    expect(screen.getByText('Deck OLED')).toBeInTheDocument()
+  })
+
+  it('renders selected labels in value order from current page options and selectedByIds', () => {
+    renderSelect({
+      value: ['device-2', 'device-99', 'device-1'],
+      selectedByIds: [{ id: 'device-99', name: 'Steam Deck OLED', badgeName: 'Deck OLED' }],
+      maxDisplayed: 3,
+    })
+
+    expect(screen.getByText('AYN Odin 2, Steam Deck OLED, Retroid Pocket 5')).toBeInTheDocument()
+  })
+
+  it('does not emit an initial empty search query on mount', () => {
+    const onQueryChange = vi.fn()
+    renderSelect({ onQueryChange })
+
+    act(() => {
+      vi.advanceTimersByTime(10)
+    })
+
+    expect(onQueryChange).not.toHaveBeenCalled()
+  })
+
+  it('debounces search query changes', () => {
+    const onQueryChange = vi.fn()
+    renderSelect({ onQueryChange })
+
+    fireEvent.click(screen.getByRole('button', { name: 'Devices multi-select' }))
+    fireEvent.change(screen.getByPlaceholderText('Search devices...'), {
+      target: { value: 'odin' },
+    })
+
+    expect(onQueryChange).not.toHaveBeenCalledWith('odin')
+
+    act(() => {
+      vi.advanceTimersByTime(10)
+    })
+
+    expect(onQueryChange).toHaveBeenCalledWith('odin')
+  })
+
+  it('clears the search query from the search button', () => {
+    const onQueryChange = vi.fn()
+    renderSelect({ onQueryChange })
+
+    fireEvent.click(screen.getByRole('button', { name: 'Devices multi-select' }))
+    fireEvent.change(screen.getByPlaceholderText('Search devices...'), {
+      target: { value: 'odin' },
+    })
+
+    act(() => {
+      vi.advanceTimersByTime(10)
+    })
+    expect(onQueryChange).toHaveBeenLastCalledWith('odin')
+
+    fireEvent.click(screen.getByRole('button', { name: 'Clear search' }))
+
+    act(() => {
+      vi.advanceTimersByTime(10)
+    })
+    expect(onQueryChange).toHaveBeenLastCalledWith('')
+  })
+
+  it('closes the dropdown with Escape and clears the search query', () => {
+    const onQueryChange = vi.fn()
+    renderSelect({ onQueryChange })
+
+    fireEvent.click(screen.getByRole('button', { name: 'Devices multi-select' }))
+    const searchInput = screen.getByPlaceholderText('Search devices...')
+    fireEvent.change(searchInput, { target: { value: 'odin' } })
+
+    act(() => {
+      vi.advanceTimersByTime(10)
+    })
+    expect(onQueryChange).toHaveBeenLastCalledWith('odin')
+
+    fireEvent.keyDown(searchInput, { key: 'Escape' })
+
+    expect(screen.queryByText('Retroid Pocket 5')).not.toBeInTheDocument()
+
+    fireEvent.click(screen.getByRole('button', { name: 'Devices multi-select' }))
+    expect(screen.getByPlaceholderText('Search devices...')).toHaveValue('')
+  })
+
+  it('closes the dropdown on outside click', () => {
+    renderSelect()
+
+    fireEvent.click(screen.getByRole('button', { name: 'Devices multi-select' }))
+    expect(screen.getByText('Retroid Pocket 5')).toBeInTheDocument()
+
+    fireEvent.click(document.body)
+
+    expect(screen.queryByText('Retroid Pocket 5')).not.toBeInTheDocument()
+  })
+
+  it('opens another async select from the same click that closes the current select', () => {
+    render(
+      <>
+        <AsyncMultiSelect
+          label="Devices"
+          value={[]}
+          onChange={vi.fn()}
+          options={options}
+          selectedByIds={[]}
+          isFetching={false}
+          hasMore={false}
+        />
+        <AsyncMultiSelect
+          label="SoCs"
+          value={[]}
+          onChange={vi.fn()}
+          options={[{ id: 'soc-1', name: 'Snapdragon G3x Gen 2' }]}
+          selectedByIds={[]}
+          isFetching={false}
+          hasMore={false}
+        />
+      </>,
+    )
+
+    fireEvent.click(screen.getByRole('button', { name: 'Devices multi-select' }))
+    expect(screen.getByText('Retroid Pocket 5')).toBeInTheDocument()
+
+    fireEvent.click(screen.getByRole('button', { name: 'SoCs multi-select' }))
+
+    expect(screen.queryByText('Retroid Pocket 5')).not.toBeInTheDocument()
+    expect(screen.getByText('Snapdragon G3x Gen 2')).toBeInTheDocument()
+  })
+
+  it('selects and deselects options', () => {
+    const onChange = vi.fn()
+    renderSelect({ value: ['device-1'], onChange })
+
+    fireEvent.click(screen.getByRole('button', { name: 'Devices multi-select' }))
+    fireEvent.click(screen.getByLabelText('Retroid Pocket 5'))
+    expect(onChange).toHaveBeenCalledWith([])
+
+    fireEvent.click(screen.getByLabelText('AYN Odin 2'))
+    expect(onChange).toHaveBeenCalledWith(['device-1', 'device-2'])
+  })
+
+  it('selects an option when the visible row text is clicked', () => {
+    const onChange = vi.fn()
+    renderSelect({ onChange })
+
+    fireEvent.click(screen.getByRole('button', { name: 'Devices multi-select' }))
+    fireEvent.click(screen.getByText('AYN Odin 2'))
+
+    expect(onChange).toHaveBeenCalledWith(['device-2'])
+  })
+
+  it('clears all selections from the dropdown footer', () => {
+    const onChange = vi.fn()
+    renderSelect({ value: ['device-1', 'device-2'], onChange })
+
+    fireEvent.click(screen.getByRole('button', { name: 'Devices multi-select' }))
+    fireEvent.click(screen.getByText('Clear all (2)'))
+
+    expect(onChange).toHaveBeenCalledWith([])
+  })
+
+  it('removes one selected chip without clearing the other selected values', () => {
+    const onChange = vi.fn()
+    renderSelect({ value: ['device-1', 'device-2'], onChange })
+
+    fireEvent.click(screen.getByRole('button', { name: 'Remove Retroid Pocket 5' }))
+
+    expect(onChange).toHaveBeenCalledWith(['device-2'])
+  })
+
+  it('shows empty and loading states', () => {
+    const { rerender } = render(
+      <AsyncMultiSelect
+        label="Devices"
+        value={[]}
+        onChange={vi.fn()}
+        options={[]}
+        selectedByIds={[]}
+        isFetching={false}
+        hasMore={false}
+      />,
+    )
+
+    fireEvent.click(screen.getByRole('button', { name: 'Devices multi-select' }))
+    expect(screen.getByText('No options found')).toBeInTheDocument()
+
+    rerender(
+      <AsyncMultiSelect
+        label="Devices"
+        value={[]}
+        onChange={vi.fn()}
+        options={[]}
+        selectedByIds={[]}
+        isFetching={true}
+        hasMore={false}
+      />,
+    )
+
+    expect(screen.getByText(/Loading/)).toBeInTheDocument()
+  })
+
+  it('opens below the trigger when there is enough visible space', () => {
+    vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockReturnValue({
+      bottom: 150,
+      height: 40,
+      left: 0,
+      right: 320,
+      top: 110,
+      width: 320,
+      x: 0,
+      y: 110,
+      toJSON: () => ({}),
+    })
+
+    renderSelect()
+
+    fireEvent.click(screen.getByRole('button', { name: 'Devices multi-select' }))
+
+    expect(screen.getByTestId('async-multi-select-options').parentElement).toHaveClass('top-full')
+  })
+
+  it('opens above the trigger when the visual viewport is constrained below it', () => {
+    vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockReturnValue({
+      bottom: 350,
+      height: 40,
+      left: 0,
+      right: 320,
+      top: 310,
+      width: 320,
+      x: 0,
+      y: 310,
+      toJSON: () => ({}),
+    })
+    vi.stubGlobal('visualViewport', {
+      addEventListener: vi.fn(),
+      height: 360,
+      offsetTop: 0,
+      removeEventListener: vi.fn(),
+    })
+
+    renderSelect()
+
+    fireEvent.click(screen.getByRole('button', { name: 'Devices multi-select' }))
+
+    expect(screen.getByTestId('async-multi-select-options').parentElement).toHaveClass(
+      'bottom-full',
+    )
+  })
+
+  it('loads more options when scrolled near the bottom', () => {
+    const onLoadMore = vi.fn()
+    renderSelect({ hasMore: true, onLoadMore })
+
+    fireEvent.click(screen.getByRole('button', { name: 'Devices multi-select' }))
+    const scrollContainer = screen.getByTestId('async-multi-select-options')
+
+    Object.defineProperty(scrollContainer, 'scrollTop', { value: 260, configurable: true })
+    Object.defineProperty(scrollContainer, 'clientHeight', { value: 80, configurable: true })
+    Object.defineProperty(scrollContainer, 'scrollHeight', { value: 340, configurable: true })
+
+    fireEvent.scroll(scrollContainer)
+
+    expect(onLoadMore).toHaveBeenCalledTimes(1)
+  })
+
+  it('does not load more while already fetching or when no more pages exist', () => {
+    const onLoadMore = vi.fn()
+    const { rerender } = render(
+      <AsyncMultiSelect
+        label="Devices"
+        value={[]}
+        onChange={vi.fn()}
+        options={options}
+        selectedByIds={[]}
+        isFetching={true}
+        hasMore={true}
+        onLoadMore={onLoadMore}
+      />,
+    )
+
+    fireEvent.click(screen.getByRole('button', { name: 'Devices multi-select' }))
+    const scrollContainer = screen.getByTestId('async-multi-select-options')
+    Object.defineProperty(scrollContainer, 'scrollTop', { value: 260, configurable: true })
+    Object.defineProperty(scrollContainer, 'clientHeight', { value: 80, configurable: true })
+    Object.defineProperty(scrollContainer, 'scrollHeight', { value: 340, configurable: true })
+
+    fireEvent.scroll(scrollContainer)
+    expect(onLoadMore).not.toHaveBeenCalled()
+
+    rerender(
+      <AsyncMultiSelect
+        label="Devices"
+        value={[]}
+        onChange={vi.fn()}
+        options={options}
+        selectedByIds={[]}
+        isFetching={false}
+        hasMore={false}
+        onLoadMore={onLoadMore}
+      />,
+    )
+
+    fireEvent.scroll(scrollContainer)
+    expect(onLoadMore).not.toHaveBeenCalled()
+  })
+})
diff --git a/src/components/ui/form/async-multi-select/AsyncMultiSelect.tsx b/src/components/ui/form/async-multi-select/AsyncMultiSelect.tsx
index 3995d6602..04fe1773f 100644
--- a/src/components/ui/form/async-multi-select/AsyncMultiSelect.tsx
+++ b/src/components/ui/form/async-multi-select/AsyncMultiSelect.tsx
@@ -1,11 +1,25 @@
 'use client'
 
 import { ChevronDown, X } from 'lucide-react'
-import { type ReactNode, type UIEvent, useEffect, useMemo, useRef, useState } from 'react'
+import {
+  type KeyboardEvent,
+  type ReactNode,
+  type UIEvent,
+  useCallback,
+  useEffect,
+  useMemo,
+  useRef,
+  useState,
+} from 'react'
 import { cn } from '@/lib/utils'
 
 export type Option = { id: string; name: string; badgeName?: string }
 
+const MOBILE_LIST_MAX_HEIGHT_PX = 192
+const DESKTOP_LIST_MAX_HEIGHT_PX = 320
+const SEARCH_HEADER_HEIGHT_PX = 64
+const FOOTER_HEIGHT_PX = 48
+
 interface Props {
   label: string
   leftIcon?: ReactNode
@@ -34,26 +48,81 @@ export default function AsyncMultiSelect(props: Props) {
   const dropdownRef = useRef<HTMLDivElement>(null)
   const buttonRef = useRef<HTMLButtonElement>(null)
   const inputRef = useRef<HTMLInputElement>(null)
+  const hasMountedRef = useRef(false)
+  const loadMoreRequestedRef = useRef(false)
+
+  const closeDropdown = useCallback((restoreFocus: boolean) => {
+    setIsOpen(false)
+    setQuery('')
+    if (restoreFocus) buttonRef.current?.focus()
+  }, [])
 
   // Debounce query -> notify wrapper
   const { onQueryChange, debounceMs } = props
   useEffect(() => {
     if (!onQueryChange) return
+
+    if (!hasMountedRef.current) {
+      hasMountedRef.current = true
+      return
+    }
+
     const id = setTimeout(() => onQueryChange?.(query), debounceMs ?? 300)
     return () => clearTimeout(id)
   }, [query, debounceMs, onQueryChange])
 
-  // Dropdown position
   useEffect(() => {
-    if (isOpen && buttonRef.current) {
-      const rect = buttonRef.current.getBoundingClientRect()
-      const vh = window.innerHeight
-      const below = vh - rect.bottom
-      const above = rect.top
-      const est = 360
-      setDropdownPosition(below < est && above > est ? 'top' : 'bottom')
+    function handleClickOutside(event: MouseEvent) {
+      if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
+        closeDropdown(false)
+      }
+    }
+
+    document.addEventListener('click', handleClickOutside)
+    return () => {
+      document.removeEventListener('click', handleClickOutside)
+    }
+  }, [closeDropdown])
+
+  useEffect(() => {
+    if (!isOpen) return
+
+    function updateDropdownPosition() {
+      if (!buttonRef.current) return
+
+      const visualViewport = window.visualViewport
+      const visibleTop = visualViewport?.offsetTop ?? 0
+      const visibleHeight = visualViewport?.height ?? window.innerHeight
+      const visibleBottom = visibleTop + visibleHeight
+      const buttonRect = buttonRef.current.getBoundingClientRect()
+      const listMaxHeight = window.matchMedia('(min-width: 640px)').matches
+        ? DESKTOP_LIST_MAX_HEIGHT_PX
+        : MOBILE_LIST_MAX_HEIGHT_PX
+      const footerHeight = props.value.length > 0 ? FOOTER_HEIGHT_PX : 0
+      const estimatedPanelHeight = listMaxHeight + SEARCH_HEADER_HEIGHT_PX + footerHeight
+      const spaceBelow = visibleBottom - buttonRect.bottom
+      const spaceAbove = buttonRect.top - visibleTop
+
+      setDropdownPosition(
+        spaceBelow < estimatedPanelHeight && spaceAbove > spaceBelow ? 'top' : 'bottom',
+      )
+    }
+
+    updateDropdownPosition()
+    window.addEventListener('resize', updateDropdownPosition)
+    window.visualViewport?.addEventListener('resize', updateDropdownPosition)
+    window.visualViewport?.addEventListener('scroll', updateDropdownPosition)
+
+    return () => {
+      window.removeEventListener('resize', updateDropdownPosition)
+      window.visualViewport?.removeEventListener('resize', updateDropdownPosition)
+      window.visualViewport?.removeEventListener('scroll', updateDropdownPosition)
     }
-  }, [isOpen])
+  }, [isOpen, props.value.length])
+
+  useEffect(() => {
+    loadMoreRequestedRef.current = false
+  }, [props.hasMore, props.isFetching, props.options.length, query])
 
   // Selected options: merge page options + byIds; ordered by value
   const selectedOptions = useMemo(() => {
@@ -90,14 +159,24 @@ export default function AsyncMultiSelect(props: Props) {
   const handleClearAll = () => props.onChange([])
   const handleRemoveOption = (id: string) => props.onChange(props.value.filter((v) => v !== id))
 
+  const handleDropdownKeyDown = (ev: KeyboardEvent<HTMLDivElement>) => {
+    if (ev.key !== 'Escape' || !isOpen) return
+
+    ev.preventDefault()
+    ev.stopPropagation()
+    closeDropdown(true)
+  }
+
   const onScrollList = (e: UIEvent<HTMLDivElement>) => {
     if (!props.onLoadMore) return
     const el = e.currentTarget
     if (
       props.hasMore &&
       !props.isFetching &&
+      !loadMoreRequestedRef.current &&
       el.scrollTop + el.clientHeight >= el.scrollHeight - 40
     ) {
+      loadMoreRequestedRef.current = true
       props.onLoadMore()
     }
   }
@@ -108,7 +187,7 @@ export default function AsyncMultiSelect(props: Props) {
         {props.label}
       </label>
 
-      <div ref={dropdownRef} className="relative">
+      <div ref={dropdownRef} className="relative" onKeyDown={handleDropdownKeyDown}>
         <button
           type="button"
           onClick={() => setIsOpen((v) => !v)}
@@ -149,11 +228,12 @@ export default function AsyncMultiSelect(props: Props) {
 
         {isOpen && (
           <div
-            className={`absolute z-[9999] w-full bg-white dark:bg-gray-800
+            className={cn(
+              `absolute left-0 z-[9999] w-full bg-white dark:bg-gray-800
               border border-gray-300 dark:border-gray-600 rounded-lg shadow-lg
-              animate-in fade-in-0 zoom-in-95 duration-200 ${
-                dropdownPosition === 'top' ? 'bottom-full mb-1' : 'top-full mt-1'
-              }`}
+              animate-in fade-in-0 zoom-in-95 duration-200`,
+              dropdownPosition === 'top' ? 'bottom-full mb-1' : 'top-full mt-1',
+            )}
           >
             <div className="p-2 border-b border-gray-200 dark:border-gray-700">
               <div className="relative">
@@ -185,7 +265,11 @@ export default function AsyncMultiSelect(props: Props) {
               </div>
             </div>
 
-            <div className="max-h-80 overflow-y-auto p-1" onScroll={onScrollList}>
+            <div
+              className="scrollbar-native-thin max-h-48 overflow-y-auto overflow-x-hidden overscroll-contain p-1 sm:max-h-80"
+              data-testid="async-multi-select-options"
+              onScroll={onScrollList}
+            >
               {props.options.length === 0 && !props.isFetching ? (
                 <div className="px-3 py-2 text-sm text-gray-500 dark:text-gray-400">
                   No options found
@@ -197,24 +281,26 @@ export default function AsyncMultiSelect(props: Props) {
                   return (
                     <label
                       key={option.id}
-                      className={`flex items-center gap-3 px-3 py-2 rounded-md cursor-pointer
+                      className={`flex min-w-0 items-center gap-3 px-3 py-2 rounded-md cursor-pointer
                         transition-all duration-200 hover:bg-gray-50 dark:hover:bg-gray-700
                         ${isSelected ? 'bg-blue-50 dark:bg-blue-900/20' : ''}
-                        ${isTopSelected ? 'animate-in slide-in-from-top-1 duration-300' : ''}
-                        hover:scale-[1.02] hover:shadow-sm`}
+                        ${isTopSelected ? 'animate-in slide-in-from-top-1 duration-300' : ''}`}
                       style={{ animationDelay: isTopSelected ? `${index * 50}ms` : '0ms' }}
                     >
                       <input
                         type="checkbox"
                         checked={isSelected}
-                        onChange={() => toggleId(option.id)}
+                        onChange={(event) => {
+                          event.stopPropagation()
+                          toggleId(option.id)
+                        }}
                         className="rounded border-gray-300 dark:border-gray-600
                           text-blue-600 focus:ring-blue-500 focus:ring-2
                           transition-all duration-200 hover:scale-110
                           dark:bg-gray-700 dark:focus:ring-blue-600"
                       />
                       <span
-                        className={`text-sm select-none flex-1 transition-all duration-200
+                        className={`min-w-0 flex-1 break-words text-sm select-none transition-all duration-200
                           ${
                             isSelected
                               ? 'text-blue-700 dark:text-blue-300 font-medium'
diff --git a/src/components/ui/form/index.ts b/src/components/ui/form/index.ts
index 841ef9492..0b332e42d 100644
--- a/src/components/ui/form/index.ts
+++ b/src/components/ui/form/index.ts
@@ -1,5 +1,4 @@
 export * from './AnimatedToggle'
-export * from './AsyncMultiSelect'
 export * from './Autocomplete'
 export * from './Input'
 export * from './InputPlaceholder'
diff --git a/src/instrumentation-client.test.ts b/src/instrumentation-client.test.ts
new file mode 100644
index 000000000..daa398005
--- /dev/null
+++ b/src/instrumentation-client.test.ts
@@ -0,0 +1,55 @@
+import { afterEach, describe, expect, it, vi } from 'vitest'
+
+const sentry = vi.hoisted(() => ({
+  captureRouterTransitionStart: vi.fn(),
+  init: vi.fn(),
+  replayIntegration: vi.fn(() => ({ name: 'replay' })),
+}))
+
+vi.mock('@sentry/nextjs', () => sentry)
+
+async function loadInstrumentationClient(overrides: Record<string, string>) {
+  vi.resetModules()
+  vi.unstubAllEnvs()
+
+  for (const [key, value] of Object.entries(overrides)) {
+    vi.stubEnv(key, value)
+  }
+
+  return import('./instrumentation-client')
+}
+
+afterEach(() => {
+  vi.unstubAllEnvs()
+  vi.resetModules()
+  vi.clearAllMocks()
+})
+
+describe('instrumentation-client', () => {
+  it('does not initialize Sentry when the Sentry flag is disabled', async () => {
+    await loadInstrumentationClient({
+      NODE_ENV: 'production',
+      NEXT_PUBLIC_APP_ENV: 'production',
+      NEXT_PUBLIC_ENABLE_SENTRY: 'false',
+    })
+
+    expect(sentry.init).not.toHaveBeenCalled()
+    expect(sentry.replayIntegration).not.toHaveBeenCalled()
+  })
+
+  it('initializes Sentry when the Sentry flag is enabled', async () => {
+    await loadInstrumentationClient({
+      NODE_ENV: 'production',
+      NEXT_PUBLIC_APP_ENV: 'production',
+      NEXT_PUBLIC_ENABLE_SENTRY: 'true',
+    })
+
+    expect(sentry.replayIntegration).toHaveBeenCalledTimes(1)
+    expect(sentry.init).toHaveBeenCalledWith(
+      expect.objectContaining({
+        dsn: expect.stringContaining('ingest.us.sentry.io'),
+        integrations: [{ name: 'replay' }],
+      }),
+    )
+  })
+})
diff --git a/src/instrumentation-client.ts b/src/instrumentation-client.ts
index 1fe7ae9dd..28394f6c9 100644
--- a/src/instrumentation-client.ts
+++ b/src/instrumentation-client.ts
@@ -3,8 +3,9 @@
 // https://docs.sentry.io/platforms/javascript/guides/nextjs/
 
 import * as Sentry from '@sentry/nextjs'
+import { env } from '@/lib/env'
 
-if (process.env.NODE_ENV === 'production') {
+if (env.ENABLE_SENTRY) {
   Sentry.init({
     dsn: 'https://85ca585e45005d8786e361c3456518bf@o74828.ingest.us.sentry.io/4509717207318529',
 
@@ -19,8 +20,6 @@ if (process.env.NODE_ENV === 'production') {
     debug: false,
 
     beforeSend(event, hint) {
-      if (typeof window !== 'undefined' && window.location.hostname === 'localhost') return null
-
       const error = hint?.originalException
       const errorMessage = error?.toString() || event.exception?.values?.[0]?.value || ''
       const errorUrl = event.request?.url || ''
diff --git a/src/lib/analytics/utils/sendAnalyticsEvent.test.ts b/src/lib/analytics/utils/sendAnalyticsEvent.test.ts
new file mode 100644
index 000000000..bd149e207
--- /dev/null
+++ b/src/lib/analytics/utils/sendAnalyticsEvent.test.ts
@@ -0,0 +1,89 @@
+import { afterEach, describe, expect, it, vi } from 'vitest'
+import { ANALYTICS_CATEGORIES } from '../actions'
+
+const mocks = vi.hoisted(() => ({
+  isTrackingAllowed: vi.fn(() => true),
+  loggerLog: vi.fn(),
+  sendGAEvent: vi.fn(),
+  track: vi.fn(),
+}))
+
+vi.mock('@next/third-parties/google', () => ({
+  sendGAEvent: mocks.sendGAEvent,
+}))
+
+vi.mock('@vercel/analytics', () => ({
+  track: mocks.track,
+}))
+
+vi.mock('@/lib/logger', () => ({
+  logger: {
+    log: mocks.loggerLog,
+  },
+}))
+
+vi.mock('./isTrackingAllowed', () => ({
+  isTrackingAllowed: mocks.isTrackingAllowed,
+}))
+
+async function loadSendAnalyticsEvent(overrides: Record<string, string>) {
+  vi.resetModules()
+  vi.unstubAllEnvs()
+
+  for (const [key, value] of Object.entries(overrides)) {
+    vi.stubEnv(key, value)
+  }
+
+  return import('./sendAnalyticsEvent')
+}
+
+afterEach(() => {
+  vi.unstubAllEnvs()
+  vi.resetModules()
+  vi.clearAllMocks()
+})
+
+describe('sendAnalyticsEvent', () => {
+  it('does not send analytics when the master analytics flag is disabled', async () => {
+    const { sendAnalyticsEvent } = await loadSendAnalyticsEvent({
+      NODE_ENV: 'production',
+      NEXT_PUBLIC_APP_ENV: 'production',
+      NEXT_PUBLIC_ENABLE_ANALYTICS: 'false',
+      NEXT_PUBLIC_GA_ID: 'G-TEST',
+      NEXT_PUBLIC_VERCEL_ANALYTICS_ENABLED: 'true',
+    })
+
+    sendAnalyticsEvent({
+      category: ANALYTICS_CATEGORIES.ENGAGEMENT,
+      action: 'support_banner_shown',
+    })
+
+    expect(mocks.track).not.toHaveBeenCalled()
+    expect(mocks.sendGAEvent).not.toHaveBeenCalled()
+  })
+
+  it('sends enabled analytics services when the master analytics flag is enabled', async () => {
+    const { sendAnalyticsEvent } = await loadSendAnalyticsEvent({
+      NODE_ENV: 'production',
+      NEXT_PUBLIC_APP_ENV: 'production',
+      NEXT_PUBLIC_ENABLE_ANALYTICS: 'true',
+      NEXT_PUBLIC_GA_ID: 'G-TEST',
+      NEXT_PUBLIC_VERCEL_ANALYTICS_ENABLED: 'true',
+    })
+
+    sendAnalyticsEvent({
+      category: ANALYTICS_CATEGORIES.ENGAGEMENT,
+      action: 'support_banner_shown',
+    })
+
+    expect(mocks.track).toHaveBeenCalledWith(
+      'support_banner_shown',
+      expect.objectContaining({ category: ANALYTICS_CATEGORIES.ENGAGEMENT }),
+    )
+    expect(mocks.sendGAEvent).toHaveBeenCalledWith(
+      'event',
+      'support_banner_shown',
+      expect.objectContaining({ event_category: ANALYTICS_CATEGORIES.ENGAGEMENT }),
+    )
+  })
+})
diff --git a/src/lib/analytics/utils/sendAnalyticsEvent.ts b/src/lib/analytics/utils/sendAnalyticsEvent.ts
index 6d06089de..e641c518f 100644
--- a/src/lib/analytics/utils/sendAnalyticsEvent.ts
+++ b/src/lib/analytics/utils/sendAnalyticsEvent.ts
@@ -47,8 +47,8 @@ export function sendAnalyticsEvent(params: AnalyticsEventData) {
     })
   }
 
-  // Log in development, send it to external service in production
-  if (process.env.NODE_ENV === 'development') {
+  // Log in development, send it to external services only when explicitly enabled.
+  if (env.IS_DEVELOPMENT_BUILD) {
     const context = typeof window !== 'undefined' ? 'CLIENT' : 'SERVER'
     return logger.log(`📊 Analytics Event [${context}]:`, {
       category: params.category,
@@ -57,12 +57,13 @@ export function sendAnalyticsEvent(params: AnalyticsEventData) {
     })
   }
 
-  // Only send to analytics services in production and on client-side
-  if (typeof window !== 'undefined' && process.env.NODE_ENV === 'production') {
+  if (typeof window !== 'undefined' && env.ENABLE_ANALYTICS) {
     if (env.VERCEL_ANALYTICS_ENABLED) track(params.action, eventData)
-    sendGAEvent('event', params.action, {
-      event_category: params.category,
-      ...eventData,
-    })
+    if (env.GA_ID) {
+      sendGAEvent('event', params.action, {
+        event_category: params.category,
+        ...eventData,
+      })
+    }
   }
 }
diff --git a/src/lib/cors.test.ts b/src/lib/cors.test.ts
index fd09607de..fd85b0d6c 100644
--- a/src/lib/cors.test.ts
+++ b/src/lib/cors.test.ts
@@ -1,35 +1,77 @@
 import { afterEach, describe, expect, it, vi } from 'vitest'
-import { getAllowedOrigins, getOriginFromUrl, isAllowedRequestOrigin } from './cors'
 
 const allowedOrigins = ['https://emuready.com', 'capacitor://localhost']
 
+async function loadCors(overrides: Record<string, string> = {}) {
+  vi.resetModules()
+  vi.unstubAllEnvs()
+  vi.stubEnv('ALLOWED_ORIGINS', '')
+  vi.stubEnv('NEXT_PUBLIC_ALLOWED_ORIGINS', '')
+  vi.stubEnv('NEXT_PUBLIC_APP_URL', '')
+  vi.stubEnv('CI', '')
+  vi.stubEnv('PLAYWRIGHT_TEST', '')
+
+  for (const [key, value] of Object.entries(overrides)) {
+    vi.stubEnv(key, value)
+  }
+
+  return import('./cors')
+}
+
+afterEach(() => {
+  vi.unstubAllEnvs()
+  vi.resetModules()
+})
+
 describe('getOriginFromUrl', () => {
-  it('normalizes full URLs to their origin', () => {
+  it('normalizes full URLs to their origin', async () => {
+    const { getOriginFromUrl } = await loadCors()
+
     expect(getOriginFromUrl('https://emuready.com/listings?id=1')).toBe('https://emuready.com')
   })
 
-  it('returns null for invalid URLs', () => {
+  it('returns null for invalid URLs', async () => {
+    const { getOriginFromUrl } = await loadCors()
+
     expect(getOriginFromUrl('not a url')).toBeNull()
   })
 })
 
 describe('getAllowedOrigins', () => {
-  afterEach(() => {
-    vi.unstubAllEnvs()
+  it('allows localhost during CI E2E runs', async () => {
+    const { getAllowedOrigins } = await loadCors({
+      CI: 'true',
+      NODE_ENV: 'test',
+    })
+
+    expect(getAllowedOrigins()).toEqual(expect.arrayContaining(['http://localhost:3000']))
   })
 
-  it('allows localhost during CI E2E runs', () => {
-    vi.stubEnv('CI', 'true')
-    vi.stubEnv('NODE_ENV', 'test')
-    vi.stubEnv('NEXT_PUBLIC_ALLOWED_ORIGINS', '')
-    vi.stubEnv('ALLOWED_ORIGINS', '')
+  it('allows localhost for test app env even when Next runs a production build', async () => {
+    const { getAllowedOrigins } = await loadCors({
+      NODE_ENV: 'production',
+      NEXT_PUBLIC_APP_ENV: 'test',
+    })
 
     expect(getAllowedOrigins()).toEqual(expect.arrayContaining(['http://localhost:3000']))
   })
+
+  it('uses production origins for explicit production app env', async () => {
+    const { getAllowedOrigins } = await loadCors({
+      NODE_ENV: 'production',
+      NEXT_PUBLIC_APP_ENV: 'production',
+    })
+
+    const origins = getAllowedOrigins()
+    expect(origins).toEqual(expect.arrayContaining(['https://emuready.com']))
+    expect(origins).not.toContain('http://localhost:3000')
+  })
 })
 
 describe('isAllowedRequestOrigin', () => {
-  it('allows configured origins', () => {
+  it('allows configured origins', async () => {
+    const { isAllowedRequestOrigin } = await loadCors()
+
     expect(
       isAllowedRequestOrigin({
         allowedOrigins,
@@ -38,7 +80,9 @@ describe('isAllowedRequestOrigin', () => {
     ).toBe(true)
   })
 
-  it('allows request sources when their origin is configured', () => {
+  it('allows request sources when their origin is configured', async () => {
+    const { isAllowedRequestOrigin } = await loadCors()
+
     expect(
       isAllowedRequestOrigin({
         allowedOrigins,
@@ -47,7 +91,9 @@ describe('isAllowedRequestOrigin', () => {
     ).toBe(true)
   })
 
-  it('rejects request sources when their origin is not configured', () => {
+  it('rejects request sources when their origin is not configured', async () => {
+    const { isAllowedRequestOrigin } = await loadCors()
+
     expect(
       isAllowedRequestOrigin({
         allowedOrigins,
@@ -56,7 +102,9 @@ describe('isAllowedRequestOrigin', () => {
     ).toBe(false)
   })
 
-  it('rejects unconfigured cross-origin requests', () => {
+  it('rejects unconfigured cross-origin requests', async () => {
+    const { isAllowedRequestOrigin } = await loadCors()
+
     expect(
       isAllowedRequestOrigin({
         allowedOrigins,
@@ -65,7 +113,9 @@ describe('isAllowedRequestOrigin', () => {
     ).toBe(false)
   })
 
-  it('rejects missing request sources', () => {
+  it('rejects missing request sources', async () => {
+    const { isAllowedRequestOrigin } = await loadCors()
+
     expect(
       isAllowedRequestOrigin({
         allowedOrigins,
diff --git a/src/lib/cors.ts b/src/lib/cors.ts
index bfd8c1412..991003ea9 100644
--- a/src/lib/cors.ts
+++ b/src/lib/cors.ts
@@ -1,4 +1,5 @@
 import { type NextRequest } from 'next/server'
+import { env } from '@/lib/env'
 
 /**
  * Hardcoded allowed origins for production
@@ -33,6 +34,16 @@ function addMissingOrigins(origins: string[], additionalOrigins: string[]) {
   }
 }
 
+function isLocalAppEnvironment() {
+  return (
+    env.APP_ENV === 'local' ||
+    env.APP_ENV === 'test' ||
+    env.IS_DEVELOPMENT_BUILD ||
+    env.IS_TEST_BUILD ||
+    process.env.PLAYWRIGHT_TEST === 'true'
+  )
+}
+
 /**
  * Get allowed CORS origins from environment variables
  * This is the single source of truth for allowed origins
@@ -45,20 +56,17 @@ export function getAllowedOrigins(): string[] {
   if (envOrigins) {
     // Use environment variable origins if provided
     origins = envOrigins.split(',').map((origin) => origin.trim())
-  } else if (process.env.NODE_ENV === 'development') {
-    // Default to localhost for development
+  } else if (isLocalAppEnvironment()) {
     origins = [...LOCAL_TEST_ORIGINS]
   } else {
-    // In production without env vars, use hardcoded production origins
     origins = [...PRODUCTION_ORIGINS]
   }
 
-  if (process.env.CI === 'true' && process.env.NODE_ENV !== 'production') {
+  if (process.env.CI === 'true' || isLocalAppEnvironment()) {
     addMissingOrigins(origins, LOCAL_TEST_ORIGINS)
   }
 
-  // Always include production origins if we're in production
-  if (process.env.NODE_ENV === 'production') {
+  if (!isLocalAppEnvironment()) {
     addMissingOrigins(origins, PRODUCTION_ORIGINS)
   }
 
@@ -104,8 +112,7 @@ export function getCORSHeaders(request?: NextRequest): Record<string, string> {
   const allowedOrigins = getAllowedOrigins()
   const origin = request?.headers.get('origin') || ''
 
-  // In production, require explicit origin configuration
-  if (process.env.NODE_ENV === 'production' && allowedOrigins.length === 0) {
+  if (env.IS_PUBLIC_PRODUCTION && allowedOrigins.length === 0) {
     console.error('CORS Error: No allowed origins configured in production')
     return {
       'Access-Control-Allow-Origin': 'null',
diff --git a/src/lib/env.test.ts b/src/lib/env.test.ts
new file mode 100644
index 000000000..9541ea4bc
--- /dev/null
+++ b/src/lib/env.test.ts
@@ -0,0 +1,77 @@
+import { afterEach, describe, expect, it, vi } from 'vitest'
+
+async function loadEnv(overrides: Record<string, string>) {
+  vi.resetModules()
+  vi.unstubAllEnvs()
+
+  for (const [key, value] of Object.entries(overrides)) {
+    vi.stubEnv(key, value)
+  }
+
+  return import('./env')
+}
+
+afterEach(() => {
+  vi.unstubAllEnvs()
+  vi.resetModules()
+})
+
+describe('env', () => {
+  it('uses explicit public app env for public production checks', async () => {
+    const { env } = await loadEnv({
+      NODE_ENV: 'production',
+      NEXT_PUBLIC_APP_ENV: 'production',
+    })
+
+    expect(env.APP_ENV).toBe('production')
+    expect(env.IS_PUBLIC_PRODUCTION).toBe(true)
+    expect(env.IS_PRODUCTION_BUILD).toBe(true)
+  })
+
+  it('does not infer public production from a production build without explicit app env', async () => {
+    const { env } = await loadEnv({
+      NODE_ENV: 'production',
+      NEXT_PUBLIC_APP_ENV: '',
+    })
+
+    expect(env.APP_ENV).toBe('local')
+    expect(env.IS_PUBLIC_PRODUCTION).toBe(false)
+    expect(env.IS_PRODUCTION_BUILD).toBe(true)
+  })
+
+  it('uses explicit public preview app env for Vercel preview builds', async () => {
+    const { env } = await loadEnv({
+      NODE_ENV: 'production',
+      NEXT_PUBLIC_APP_ENV: 'preview',
+    })
+
+    expect(env.APP_ENV).toBe('preview')
+    expect(env.IS_PUBLIC_PRODUCTION).toBe(false)
+    expect(env.IS_PRODUCTION_BUILD).toBe(true)
+  })
+
+  it('falls back to test app env for test runs when app env is omitted', async () => {
+    const { env } = await loadEnv({
+      NODE_ENV: 'test',
+      NEXT_PUBLIC_APP_ENV: '',
+    })
+
+    expect(env.APP_ENV).toBe('test')
+    expect(env.IS_PUBLIC_PRODUCTION).toBe(false)
+    expect(env.IS_TEST_BUILD).toBe(true)
+  })
+
+  it('only enables optional browser services when their public flags are true', async () => {
+    const { env } = await loadEnv({
+      NODE_ENV: 'production',
+      NEXT_PUBLIC_APP_ENV: 'production',
+      NEXT_PUBLIC_ENABLE_ANALYTICS: 'true',
+      NEXT_PUBLIC_ENABLE_KOFI_WIDGET: 'false',
+      NEXT_PUBLIC_ENABLE_SENTRY: 'true',
+    })
+
+    expect(env.ENABLE_ANALYTICS).toBe(true)
+    expect(env.ENABLE_KOFI_WIDGET).toBe(false)
+    expect(env.ENABLE_SENTRY).toBe(true)
+  })
+})
diff --git a/src/lib/env.ts b/src/lib/env.ts
index bc2fa8439..23a21c923 100644
--- a/src/lib/env.ts
+++ b/src/lib/env.ts
@@ -1,3 +1,6 @@
+const APP_ENV_VALUES = ['local', 'test', 'preview', 'staging', 'production'] as const
+type AppEnv = (typeof APP_ENV_VALUES)[number]
+
 interface Env {
   EMUREADY_LITE_GITHUB_URL: string
   EMUREADY_BETA_URL: string
@@ -17,9 +20,14 @@ interface Env {
   ENABLE_SW: boolean
   VERCEL_ANALYTICS_ENABLED: boolean
   DISABLE_COOKIE_BANNER: boolean
-  IS_PROD: boolean
-  IS_DEV: boolean
-  IS_TEST: boolean
+  APP_ENV: AppEnv
+  IS_PUBLIC_PRODUCTION: boolean
+  IS_PRODUCTION_BUILD: boolean
+  IS_DEVELOPMENT_BUILD: boolean
+  IS_TEST_BUILD: boolean
+  ENABLE_ANALYTICS: boolean
+  ENABLE_KOFI_WIDGET: boolean
+  ENABLE_SENTRY: boolean
   ENABLE_V2_LISTINGS: boolean
   ENABLE_PATREON_VERIFICATION: boolean
   ENABLE_ANDROID_DOWNLOADS: boolean
@@ -28,6 +36,15 @@ interface Env {
   ANDROID_LATEST_APK_URL: string
 }
 
+function resolveAppEnv(): AppEnv {
+  const appEnv = process.env.NEXT_PUBLIC_APP_ENV
+  if (APP_ENV_VALUES.includes(appEnv as AppEnv)) return appEnv as AppEnv
+  if (process.env.NODE_ENV === 'test') return 'test'
+  if (process.env.NODE_ENV === 'development') return 'local'
+  return 'local'
+}
+
+const APP_ENV = resolveAppEnv()
 const GITHUB_URL = process.env.NEXT_PUBLIC_GITHUB_URL ?? 'https://github.com/Producdevity/EmuReady'
 export const env = {
   EMUREADY_LITE_GITHUB_URL:
@@ -66,9 +83,14 @@ export const env = {
 
   DISABLE_COOKIE_BANNER: process.env.NEXT_PUBLIC_DISABLE_COOKIE_BANNER === 'true',
 
-  IS_PROD: process.env.NODE_ENV === 'production',
-  IS_DEV: process.env.NODE_ENV === 'development',
-  IS_TEST: process.env.NODE_ENV === 'test',
+  APP_ENV,
+  IS_PUBLIC_PRODUCTION: APP_ENV === 'production',
+  IS_PRODUCTION_BUILD: process.env.NODE_ENV === 'production',
+  IS_DEVELOPMENT_BUILD: process.env.NODE_ENV === 'development',
+  IS_TEST_BUILD: process.env.NODE_ENV === 'test',
+  ENABLE_ANALYTICS: process.env.NEXT_PUBLIC_ENABLE_ANALYTICS === 'true',
+  ENABLE_KOFI_WIDGET: process.env.NEXT_PUBLIC_ENABLE_KOFI_WIDGET === 'true',
+  ENABLE_SENTRY: process.env.NEXT_PUBLIC_ENABLE_SENTRY === 'true',
 
   ENABLE_V2_LISTINGS: process.env.NEXT_PUBLIC_ENABLE_V2_LISTINGS === 'true',
   ENABLE_PATREON_VERIFICATION: process.env.NEXT_PUBLIC_ENABLE_PATREON_VERIFICATION === 'true',
diff --git a/src/lib/trust/service.test.ts b/src/lib/trust/service.test.ts
index c456fc8e7..82fc53b81 100644
--- a/src/lib/trust/service.test.ts
+++ b/src/lib/trust/service.test.ts
@@ -26,6 +26,35 @@ function createMockPrismaCtx() {
 
 type MockPrismaCtx = ReturnType<typeof createMockPrismaCtx>
 
+describe('canUserAutoApprove', () => {
+  let prismaCtx: MockPrismaCtx
+
+  beforeEach(() => {
+    vi.clearAllMocks()
+    prismaCtx = createMockPrismaCtx()
+  })
+
+  it('uses the provided prisma client when checking trust score', async () => {
+    prismaCtx.user.findUnique.mockResolvedValue({ trustScore: 250 })
+    const { canUserAutoApprove } = await import('./service')
+
+    const result = await canUserAutoApprove('user-1', prismaCtx as never)
+
+    expect(result).toBe(true)
+    expect(prismaCtx.user.findUnique).toHaveBeenCalledWith({
+      where: { id: 'user-1' },
+      select: { trustScore: true },
+    })
+  })
+
+  it('returns false when the user does not exist', async () => {
+    prismaCtx.user.findUnique.mockResolvedValue(null)
+    const { canUserAutoApprove } = await import('./service')
+
+    await expect(canUserAutoApprove('missing-user', prismaCtx as never)).resolves.toBe(false)
+  })
+})
+
 describe('TrustService.applyBulkManualAdjustments', () => {
   let prismaCtx: MockPrismaCtx
 
diff --git a/src/lib/trust/service.ts b/src/lib/trust/service.ts
index e80c51820..429f89fc0 100644
--- a/src/lib/trust/service.ts
+++ b/src/lib/trust/service.ts
@@ -29,6 +29,8 @@ interface ApplyTrustActionParams {
   context?: TrustActionContext
 }
 
+type PrismaClientOrTransaction = PrismaClient | Prisma.TransactionClient
+
 export async function applyTrustAction(params: ApplyTrustActionParams): Promise<void> {
   const { userId, action, context = {} } = params
 
@@ -160,8 +162,11 @@ export async function reverseTrustAction(params: {
   })
 }
 
-export async function canUserAutoApprove(userId: string): Promise<boolean> {
-  const user = await prisma.user.findUnique({
+export async function canUserAutoApprove(
+  userId: string,
+  prismaClient: PrismaClientOrTransaction = prisma,
+): Promise<boolean> {
+  const user = await prismaClient.user.findUnique({
     where: { id: userId },
     select: { trustScore: true },
   })
diff --git a/src/proxy.ts b/src/proxy.ts
index 5200f4975..216cc0955 100644
--- a/src/proxy.ts
+++ b/src/proxy.ts
@@ -1,6 +1,7 @@
 import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
 import { NextResponse } from 'next/server'
 import { getAllowedOrigins, getOriginFromUrl, isAllowedRequestOrigin } from '@/lib/cors'
+import { env } from '@/lib/env'
 import { ms } from '@/utils/time'
 import type { NextRequest, NextFetchEvent } from 'next/server'
 
@@ -8,7 +9,10 @@ import type { NextRequest, NextFetchEvent } from 'next/server'
 // TODO: For production abuse control, prefer provider/WAF rate limits before traffic reaches Next.
 const rateLimitMap = new Map<string, { count: number; resetTime: number }>()
 
-const RATE_LIMIT_REQUESTS = process.env.NODE_ENV === 'test' ? 10000 : 100
+const IS_AUTOMATED_TEST_ENVIRONMENT =
+  env.APP_ENV === 'test' || env.IS_TEST_BUILD || process.env.PLAYWRIGHT_TEST === 'true'
+const IS_LOCAL_DEVELOPMENT_ENVIRONMENT = env.APP_ENV === 'local' || env.IS_DEVELOPMENT_BUILD
+const RATE_LIMIT_REQUESTS = IS_AUTOMATED_TEST_ENVIRONMENT ? 10000 : 100
 const RATE_LIMIT_WINDOW = ms.minutes(3)
 const RATE_LIMIT_CLEANUP_SAMPLE_RATE = 0.01
 const DEV_NO_STORE_HOSTS = new Set(['dev.emuready.com'])
@@ -42,7 +46,7 @@ function getClientIdentifier(req: NextRequest): string {
 function shouldBypassRateLimit(identifier: string): boolean {
   if (process.env.DISABLE_RATE_LIMIT === 'true') return true
 
-  if (process.env.NODE_ENV !== 'test' && process.env.NODE_ENV !== 'development') return false
+  if (!IS_AUTOMATED_TEST_ENVIRONMENT && !IS_LOCAL_DEVELOPMENT_ENVIRONMENT) return false
 
   return LOCAL_RATE_LIMIT_BYPASS_IDENTIFIERS.has(identifier)
 }
@@ -142,7 +146,7 @@ function protectTRPCAPI(req: NextRequest): NextResponse | null {
     )
   }
 
-  if (process.env.NODE_ENV !== 'test' && !isValidOrigin(req)) {
+  if (!IS_AUTOMATED_TEST_ENVIRONMENT && !isValidOrigin(req)) {
     console.warn(
       `Invalid origin for client: ${clientId}, origin: ${req.headers.get('origin')}, referer: ${req.headers.get('referer')}, path: ${pathname}`,
     )
diff --git a/src/schemas/cpu.ts b/src/schemas/cpu.ts
index 482855a2e..21490f726 100644
--- a/src/schemas/cpu.ts
+++ b/src/schemas/cpu.ts
@@ -15,6 +15,15 @@ export const GetCpusSchema = z
   })
   .optional()
 
+export const GetCpuOptionsSchema = z
+  .object({
+    search: z.string().optional(),
+    brandId: z.string().uuid().optional(),
+    limit: z.number().int().min(1).max(10000).default(50),
+    offset: z.number().int().min(0).default(0),
+  })
+  .optional()
+
 export const GetCpuByIdSchema = z.object({ id: z.string().uuid() })
 export const GetCpusByIdsSchema = z.object({ ids: z.array(z.string().uuid()).min(1).max(100) })
 
@@ -33,6 +42,7 @@ export const DeleteCpuSchema = z.object({ id: z.string().uuid() })
 
 // Type exports for repository use
 export type GetCpusInput = z.input<typeof GetCpusSchema>
+export type GetCpuOptionsInput = z.input<typeof GetCpuOptionsSchema>
 export type CreateCpuInput = z.infer<typeof CreateCpuSchema>
 export type UpdateCpuInput = z.infer<typeof UpdateCpuSchema>
 export type GetCpusByIdsInput = z.infer<typeof GetCpusByIdsSchema>
diff --git a/src/schemas/device.ts b/src/schemas/device.ts
index 27503e03d..ffca2eb24 100644
--- a/src/schemas/device.ts
+++ b/src/schemas/device.ts
@@ -17,6 +17,16 @@ export const GetDevicesSchema = z
   })
   .optional()
 
+export const GetDeviceOptionsSchema = z
+  .object({
+    search: z.string().nullable().optional(),
+    brandId: z.string().uuid().nullable().optional(),
+    socId: z.string().uuid().nullable().optional(),
+    limit: z.number().int().min(1).max(10000).default(50),
+    offset: z.number().int().min(0).default(0),
+  })
+  .optional()
+
 export const GetDeviceByIdSchema = z.object({ id: z.string().uuid() })
 export const GetDevicesByIdsSchema = z.object({ ids: z.array(z.string().uuid()).min(1).max(100) })
 
@@ -41,6 +51,7 @@ export const GetTrendingDevicesSummarySchema = z.object({
 
 // Type exports for repository use
 export type GetDevicesInput = z.input<typeof GetDevicesSchema>
+export type GetDeviceOptionsInput = z.input<typeof GetDeviceOptionsSchema>
 export type CreateDeviceInput = z.infer<typeof CreateDeviceSchema>
 export type UpdateDeviceInput = z.infer<typeof UpdateDeviceSchema>
 export type GetDevicesByIdsInput = z.infer<typeof GetDevicesByIdsSchema>
diff --git a/src/schemas/gpu.ts b/src/schemas/gpu.ts
index 46886ed36..fa2ad1d3f 100644
--- a/src/schemas/gpu.ts
+++ b/src/schemas/gpu.ts
@@ -15,6 +15,15 @@ export const GetGpusSchema = z
   })
   .optional()
 
+export const GetGpuOptionsSchema = z
+  .object({
+    search: z.string().optional(),
+    brandId: z.string().uuid().optional(),
+    limit: z.number().int().min(1).max(10000).default(50),
+    offset: z.number().int().min(0).default(0),
+  })
+  .optional()
+
 export const GetGpuByIdSchema = z.object({ id: z.string().uuid() })
 export const GetGpusByIdsSchema = z.object({ ids: z.array(z.string().uuid()).min(1).max(100) })
 
@@ -36,6 +45,7 @@ export const DeleteGpuSchema = z.object({ id: z.string().uuid() })
 // Use z.output for types after defaults are applied (what you get out)
 // TODO: figure out why we use z.infer
 export type GetGpusInput = z.input<typeof GetGpusSchema>
+export type GetGpuOptionsInput = z.input<typeof GetGpuOptionsSchema>
 export type CreateGpuInput = z.infer<typeof CreateGpuSchema>
 export type UpdateGpuInput = z.infer<typeof UpdateGpuSchema>
 export type GetGpusByIdsInput = z.infer<typeof GetGpusByIdsSchema>
diff --git a/src/schemas/soc.ts b/src/schemas/soc.ts
index 20c3a59b4..0baa2c543 100644
--- a/src/schemas/soc.ts
+++ b/src/schemas/soc.ts
@@ -14,6 +14,14 @@ export const GetSoCsSchema = z
   })
   .optional()
 
+export const GetSoCOptionsSchema = z
+  .object({
+    search: z.string().optional(),
+    limit: z.number().int().min(1).max(10000).default(50),
+    offset: z.number().int().min(0).default(0),
+  })
+  .optional()
+
 export const GetSoCByIdSchema = z.object({
   id: z.string().uuid(),
 })
@@ -37,5 +45,6 @@ export const GetSoCsByIdsSchema = z.object({ ids: z.array(z.string().uuid()).min
 
 // Type exports for repository use
 export type GetSoCsInput = z.input<typeof GetSoCsSchema>
+export type GetSoCOptionsInput = z.input<typeof GetSoCOptionsSchema>
 export type CreateSoCInput = z.infer<typeof CreateSoCSchema>
 export type UpdateSoCInput = z.infer<typeof UpdateSoCSchema>
diff --git a/src/sentry-config.test.ts b/src/sentry-config.test.ts
new file mode 100644
index 000000000..9aebfa0f5
--- /dev/null
+++ b/src/sentry-config.test.ts
@@ -0,0 +1,52 @@
+import { afterEach, describe, expect, it, vi } from 'vitest'
+
+const sentry = vi.hoisted(() => ({
+  init: vi.fn(),
+}))
+
+vi.mock('@sentry/nextjs', () => sentry)
+
+async function loadSentryConfigs(overrides: Record<string, string>) {
+  vi.resetModules()
+  vi.unstubAllEnvs()
+
+  for (const [key, value] of Object.entries(overrides)) {
+    vi.stubEnv(key, value)
+  }
+
+  await import('../sentry.server.config')
+  await import('../sentry.edge.config')
+}
+
+afterEach(() => {
+  vi.unstubAllEnvs()
+  vi.resetModules()
+  vi.clearAllMocks()
+})
+
+describe('Sentry server and edge config', () => {
+  it('does not initialize when the Sentry flag is disabled', async () => {
+    await loadSentryConfigs({
+      NODE_ENV: 'production',
+      NEXT_PUBLIC_APP_ENV: 'production',
+      NEXT_PUBLIC_ENABLE_SENTRY: 'false',
+    })
+
+    expect(sentry.init).not.toHaveBeenCalled()
+  })
+
+  it('initializes server and edge Sentry when the Sentry flag is enabled', async () => {
+    await loadSentryConfigs({
+      NODE_ENV: 'production',
+      NEXT_PUBLIC_APP_ENV: 'production',
+      NEXT_PUBLIC_ENABLE_SENTRY: 'true',
+    })
+
+    expect(sentry.init).toHaveBeenCalledTimes(2)
+    expect(sentry.init).toHaveBeenCalledWith(
+      expect.objectContaining({
+        dsn: expect.stringContaining('ingest.us.sentry.io'),
+      }),
+    )
+  })
+})
diff --git a/src/server/api/routers/cpus.ts b/src/server/api/routers/cpus.ts
index eb31e5128..0c8d637eb 100644
--- a/src/server/api/routers/cpus.ts
+++ b/src/server/api/routers/cpus.ts
@@ -3,6 +3,7 @@ import {
   CreateCpuSchema,
   DeleteCpuSchema,
   GetCpuByIdSchema,
+  GetCpuOptionsSchema,
   GetCpusByIdsSchema,
   GetCpusSchema,
   UpdateCpuSchema,
@@ -21,6 +22,11 @@ export const cpusRouter = createTRPCRouter({
     return repository.list(input ?? {})
   }),
 
+  options: publicProcedure.input(GetCpuOptionsSchema).query(async ({ ctx, input }) => {
+    const repository = new CpusRepository(ctx.prisma)
+    return repository.options(input ?? {})
+  }),
+
   byId: publicProcedure.input(GetCpuByIdSchema).query(async ({ ctx, input }) => {
     const repository = new CpusRepository(ctx.prisma)
     const cpu = await repository.byIdWithCounts(input.id)
diff --git a/src/server/api/routers/devices.ts b/src/server/api/routers/devices.ts
index 224432ee2..259571fcb 100644
--- a/src/server/api/routers/devices.ts
+++ b/src/server/api/routers/devices.ts
@@ -3,6 +3,7 @@ import {
   CreateDeviceSchema,
   DeleteDeviceSchema,
   GetDeviceByIdSchema,
+  GetDeviceOptionsSchema,
   GetDevicesByIdsSchema,
   GetDevicesSchema,
   GetTrendingDevicesSummarySchema,
@@ -22,6 +23,11 @@ export const devicesRouter = createTRPCRouter({
     return repository.list(input ?? {})
   }),
 
+  options: publicProcedure.input(GetDeviceOptionsSchema).query(async ({ ctx, input }) => {
+    const repository = new DevicesRepository(ctx.prisma)
+    return repository.options(input ?? {})
+  }),
+
   byId: publicProcedure.input(GetDeviceByIdSchema).query(async ({ ctx, input }) => {
     const repository = new DevicesRepository(ctx.prisma)
     const device = await repository.byIdWithCounts(input.id)
@@ -37,8 +43,7 @@ export const devicesRouter = createTRPCRouter({
     const repository = new DevicesRepository(ctx.prisma)
 
     // Repository handles all validation (brand exists, SoC exists, no duplicates)
-    const created = await repository.create(input)
-    return repository.byIdWithCounts(created.id)
+    return repository.create(input)
   }),
 
   update: manageDevicesProcedure.input(UpdateDeviceSchema).mutation(async ({ ctx, input }) => {
@@ -46,20 +51,14 @@ export const devicesRouter = createTRPCRouter({
     const { id, ...data } = input
 
     // Repository handles all validation (device exists, brand exists, SoC exists, no duplicates)
-    const updated = await repository.update(id, data)
-    return repository.byIdWithCounts(updated.id)
+    return repository.update(id, data)
   }),
 
   delete: manageDevicesProcedure.input(DeleteDeviceSchema).mutation(async ({ ctx, input }) => {
     const repository = new DevicesRepository(ctx.prisma)
 
-    // Get device before deletion to return it
-    const existingDevice = await repository.byIdWithCounts(input.id)
-    if (!existingDevice) return ResourceError.device.notFound()
-
     // Repository handles validation (device exists, not in use)
-    await repository.delete(input.id)
-    return existingDevice
+    return repository.delete(input.id)
   }),
 
   stats: viewStatisticsProcedure.query(async ({ ctx }) => {
diff --git a/src/server/api/routers/gpus.ts b/src/server/api/routers/gpus.ts
index 8b000c235..0ec78de93 100644
--- a/src/server/api/routers/gpus.ts
+++ b/src/server/api/routers/gpus.ts
@@ -3,6 +3,7 @@ import {
   CreateGpuSchema,
   DeleteGpuSchema,
   GetGpuByIdSchema,
+  GetGpuOptionsSchema,
   GetGpusByIdsSchema,
   GetGpusSchema,
   UpdateGpuSchema,
@@ -21,6 +22,11 @@ export const gpusRouter = createTRPCRouter({
     return repository.list(input ?? {})
   }),
 
+  options: publicProcedure.input(GetGpuOptionsSchema).query(async ({ ctx, input }) => {
+    const repository = new GpusRepository(ctx.prisma)
+    return repository.options(input ?? {})
+  }),
+
   byId: publicProcedure.input(GetGpuByIdSchema).query(async ({ ctx, input }) => {
     const repository = new GpusRepository(ctx.prisma)
     const gpu = await repository.byIdWithCounts(input.id)
diff --git a/src/server/api/routers/socs.ts b/src/server/api/routers/socs.ts
index c88df5642..5d276b8bb 100644
--- a/src/server/api/routers/socs.ts
+++ b/src/server/api/routers/socs.ts
@@ -3,6 +3,7 @@ import {
   CreateSoCSchema,
   DeleteSoCSchema,
   GetSoCByIdSchema,
+  GetSoCOptionsSchema,
   GetSoCsSchema,
   UpdateSoCSchema,
   GetSoCsByIdsSchema,
@@ -33,7 +34,7 @@ export const socsRouter = createTRPCRouter({
     const pagination = paginate({
       total: total,
       page: page ?? Math.floor(actualOffset / limit) + 1,
-      limit: limit,
+      limit,
     })
 
     return {
@@ -42,6 +43,11 @@ export const socsRouter = createTRPCRouter({
     }
   }),
 
+  options: publicProcedure.input(GetSoCOptionsSchema).query(async ({ ctx, input }) => {
+    const repository = new SoCsRepository(ctx.prisma)
+    return repository.options(input ?? {})
+  }),
+
   byId: publicProcedure.input(GetSoCByIdSchema).query(async ({ ctx, input }) => {
     const repository = new SoCsRepository(ctx.prisma)
     const soc = await repository.byId(input.id)
diff --git a/src/server/repositories/cpus.repository.ts b/src/server/repositories/cpus.repository.ts
index fc9ab67e8..0246db9cd 100644
--- a/src/server/repositories/cpus.repository.ts
+++ b/src/server/repositories/cpus.repository.ts
@@ -3,7 +3,14 @@ import { ResourceError } from '@/lib/errors'
 import { type PaginationResult, paginate, calculateOffset } from '@/server/utils/pagination'
 import { Prisma } from '@orm/client'
 import { BaseRepository } from './base.repository'
-import type { GetCpusInput, CreateCpuInput, UpdateCpuInput } from '@/schemas/cpu'
+import type {
+  GetCpusInput,
+  GetCpuOptionsInput,
+  CreateCpuInput,
+  UpdateCpuInput,
+} from '@/schemas/cpu'
+
+type CpuOptionFilters = NonNullable<GetCpuOptionsInput>
 
 /**
  * Repository for CPU data access
@@ -30,6 +37,14 @@ export class CpusRepository extends BaseRepository {
     } satisfies Prisma.CpuInclude,
   } as const
 
+  static readonly selects = {
+    option: {
+      id: true,
+      modelName: true,
+      brand: { select: { id: true, name: true } },
+    } satisfies Prisma.CpuSelect,
+  } as const
+
   async byId(
     id: string,
     options: { limited?: boolean } = {},
@@ -84,11 +99,9 @@ export class CpusRepository extends BaseRepository {
     id: string,
     data: Partial<UpdateCpuInput>,
   ): Promise<Prisma.CpuGetPayload<{ include: typeof CpusRepository.includes.default }>> {
-    // Check if CPU exists
     const cpu = await this.byId(id)
     if (!cpu) throw ResourceError.cpu.notFound()
 
-    // Validate brand exists if being updated
     if (data.brandId) {
       const brand = await this.prisma.deviceBrand.findUnique({
         where: { id: data.brandId },
@@ -96,7 +109,6 @@ export class CpusRepository extends BaseRepository {
       if (!brand) throw ResourceError.deviceBrand.notFound()
     }
 
-    // Check for duplicate model name if being updated
     if (data.modelName) {
       const exists = await this.existsByModelName(data.modelName, id)
       if (exists) throw ResourceError.cpu.alreadyExists(data.modelName)
@@ -172,6 +184,26 @@ export class CpusRepository extends BaseRepository {
     })
   }
 
+  async options(filters: CpuOptionFilters = {}): Promise<{
+    cpus: Prisma.CpuGetPayload<{ select: typeof CpusRepository.selects.option }>[]
+    hasMore: boolean
+  }> {
+    const limit = filters.limit ?? 50
+    const offset = filters.offset ?? 0
+    const cpus = await this.prisma.cpu.findMany({
+      where: this.buildWhereClause(filters.search, filters.brandId),
+      select: CpusRepository.selects.option,
+      orderBy: [{ brand: { name: this.sortOrder } }, { modelName: this.sortOrder }],
+      take: limit + 1,
+      skip: offset,
+    })
+
+    return {
+      cpus: cpus.slice(0, limit),
+      hasMore: cpus.length > limit,
+    }
+  }
+
   /**
    * Get CPUs with pagination metadata
    * Supports both web and mobile usage via options
diff --git a/src/server/repositories/devices.repository.ts b/src/server/repositories/devices.repository.ts
index 9cd122275..defe8f61a 100644
--- a/src/server/repositories/devices.repository.ts
+++ b/src/server/repositories/devices.repository.ts
@@ -1,12 +1,19 @@
 import { startOfMonth, subDays } from 'date-fns'
+import { LRUCache } from 'lru-cache'
 import { HOME_PAGE_LIMITS } from '@/data/constants'
 import { ResourceError } from '@/lib/errors'
 import { type PaginationResult, paginate, calculateOffset } from '@/server/utils/pagination'
+import { TIME_CONSTANTS } from '@/utils/time'
 import { Prisma, ApprovalStatus } from '@orm/client'
 import { getTrendingDevices } from '@orm/sql'
 import { BaseRepository } from './base.repository'
 import type { TimeRangeId } from '@/app/home/components/TimeRangeTabs'
-import type { GetDevicesInput, CreateDeviceInput, UpdateDeviceInput } from '@/schemas/device'
+import type {
+  GetDevicesInput,
+  GetDeviceOptionsInput,
+  CreateDeviceInput,
+  UpdateDeviceInput,
+} from '@/schemas/device'
 
 export interface TrendingDevice {
   id: string
@@ -23,6 +30,14 @@ export interface TrendingDevicesSummary {
   thisWeek: TrendingDevice[]
 }
 
+const trendingDevicesSummaryCache = new LRUCache<string, TrendingDevicesSummary>({
+  ttl: TIME_CONSTANTS.SIX_HOURS,
+  max: 20,
+})
+
+type DeviceFilters = NonNullable<GetDevicesInput>
+type DeviceOptionFilters = NonNullable<GetDeviceOptionsInput>
+
 /**
  * Repository for Device data access
  */
@@ -57,6 +72,13 @@ export class DevicesRepository extends BaseRepository {
       brand: { select: { id: true, name: true } },
       soc: { select: { id: true, name: true, manufacturer: true } },
     } satisfies Prisma.DeviceSelect,
+
+    option: {
+      id: true,
+      modelName: true,
+      brand: { select: { id: true, name: true } },
+      soc: { select: { id: true, name: true, manufacturer: true } },
+    } satisfies Prisma.DeviceSelect,
   } as const
 
   async byId(id: string): Promise<Prisma.DeviceGetPayload<{
@@ -81,7 +103,7 @@ export class DevicesRepository extends BaseRepository {
 
   async create(data: CreateDeviceInput): Promise<
     Prisma.DeviceGetPayload<{
-      include: typeof DevicesRepository.includes.default
+      include: typeof DevicesRepository.includes.withCounts
     }>
   > {
     // Check for duplicate
@@ -102,7 +124,7 @@ export class DevicesRepository extends BaseRepository {
       () =>
         this.prisma.device.create({
           data: { ...data, socId: data.socId ?? null },
-          include: DevicesRepository.includes.default,
+          include: DevicesRepository.includes.withCounts,
         }),
       'Device',
     )
@@ -113,7 +135,7 @@ export class DevicesRepository extends BaseRepository {
     data: Partial<UpdateDeviceInput>,
   ): Promise<
     Prisma.DeviceGetPayload<{
-      include: typeof DevicesRepository.includes.default
+      include: typeof DevicesRepository.includes.withCounts
     }>
   > {
     // Check if device exists
@@ -145,17 +167,21 @@ export class DevicesRepository extends BaseRepository {
         this.prisma.device.update({
           where: { id },
           data: { ...data, socId: data.socId ?? undefined },
-          include: DevicesRepository.includes.default,
+          include: DevicesRepository.includes.withCounts,
         }),
       'Device',
     )
   }
 
-  async delete(id: string): Promise<void> {
+  async delete(id: string): Promise<
+    Prisma.DeviceGetPayload<{
+      include: typeof DevicesRepository.includes.withCounts
+    }>
+  > {
     // Check if device exists and has listings
     const device = await this.prisma.device.findUnique({
       where: { id },
-      include: { _count: { select: { listings: true } } },
+      include: DevicesRepository.includes.withCounts,
     })
 
     if (!device) throw ResourceError.device.notFound()
@@ -164,36 +190,63 @@ export class DevicesRepository extends BaseRepository {
     }
 
     await this.handleDatabaseOperation(() => this.prisma.device.delete({ where: { id } }), 'Device')
+    return device
   }
 
-  async list(input: GetDevicesInput = {}): Promise<{
-    devices: Prisma.DeviceGetPayload<{ include: typeof DevicesRepository.includes.withCounts }>[]
-    pagination: PaginationResult
-  }> {
-    const limit = input.limit ?? 20
-    const actualOffset = calculateOffset({ page: input.page, offset: input.offset }, limit)
-
+  private buildWhere(input: DeviceFilters | DeviceOptionFilters = {}): Prisma.DeviceWhereInput {
     const where: Prisma.DeviceWhereInput = {}
 
     if (input.brandId) where.brandId = input.brandId
+    if (input.socId) where.socId = input.socId
     if (input.search) {
-      const searchWords = input.search.trim().split(/\s+/)
+      const searchWords = input.search.trim().split(/\s+/).filter(Boolean)
+
+      if (searchWords.length > 0) {
+        // Each word must appear in either brand or model name
+        where.AND = searchWords.map((word) => ({
+          OR: [
+            { modelName: { contains: word, mode: this.mode } },
+            { brand: { name: { contains: word, mode: this.mode } } },
+          ],
+        }))
+      }
+    }
 
-      // Each word must appear in either brand or model name
-      where.AND = searchWords.map((word) => ({
-        OR: [
-          { modelName: { contains: word, mode: this.mode } },
-          { brand: { name: { contains: word, mode: this.mode } } },
-        ],
-      }))
+    return where
+  }
+
+  private buildOrderBy(input: DeviceFilters = {}): Prisma.DeviceOrderByWithRelationInput[] {
+    const direction = input.sortDirection ?? this.sortOrder
+
+    switch (input.sortField) {
+      case 'modelName':
+        return [{ modelName: direction }]
+      case 'soc':
+        return [{ soc: { name: direction } }, { brand: { name: this.sortOrder } }]
+      case 'listings':
+        return [{ listings: { _count: direction } }, { brand: { name: this.sortOrder } }]
+      case 'brand':
+      default:
+        return [{ brand: { name: direction } }, { modelName: this.sortOrder }]
     }
+  }
+
+  async list(input: DeviceFilters = {}): Promise<{
+    devices: Prisma.DeviceGetPayload<{ include: typeof DevicesRepository.includes.withCounts }>[]
+    pagination: PaginationResult
+  }> {
+    const limit = input.limit ?? 20
+    const actualOffset = calculateOffset({ page: input.page, offset: input.offset }, limit)
+
+    const where = this.buildWhere(input)
+    const orderBy = this.buildOrderBy(input)
 
     const [total, devices] = await Promise.all([
       this.prisma.device.count({ where }),
       this.prisma.device.findMany({
         where,
         include: DevicesRepository.includes.withCounts,
-        orderBy: [{ brand: { name: this.sortOrder } }, { modelName: this.sortOrder }],
+        orderBy,
         skip: actualOffset,
         take: limit,
       }),
@@ -208,6 +261,26 @@ export class DevicesRepository extends BaseRepository {
     return { devices, pagination }
   }
 
+  async options(input: DeviceOptionFilters = {}): Promise<{
+    devices: Prisma.DeviceGetPayload<{ select: typeof DevicesRepository.selects.option }>[]
+    hasMore: boolean
+  }> {
+    const limit = input.limit ?? 50
+    const offset = input.offset ?? 0
+    const devices = await this.prisma.device.findMany({
+      where: this.buildWhere(input),
+      select: DevicesRepository.selects.option,
+      orderBy: [{ brand: { name: this.sortOrder } }, { modelName: this.sortOrder }],
+      skip: offset,
+      take: limit + 1,
+    })
+
+    return {
+      devices: devices.slice(0, limit),
+      hasMore: devices.length > limit,
+    }
+  }
+
   async existsByModelAndBrand(modelName: string, brandId: string): Promise<boolean | string> {
     const device = await this.prisma.device.findFirst({
       where: { modelName, brandId },
@@ -256,7 +329,7 @@ export class DevicesRepository extends BaseRepository {
     })
   }
 
-  async listMobile(filters: GetDevicesInput = {}): Promise<{
+  async listMobile(filters: DeviceFilters = {}): Promise<{
     devices: {
       id: string
       modelName: string
@@ -270,19 +343,7 @@ export class DevicesRepository extends BaseRepository {
     const page = filters.page ?? 1
     const actualOffset = calculateOffset({ page }, limit)
 
-    const where: Prisma.DeviceWhereInput = {}
-
-    if (filters.brandId) where.brandId = filters.brandId
-    if (filters.search) {
-      const searchWords = filters.search.trim().split(/\s+/)
-
-      where.AND = searchWords.map((word) => ({
-        OR: [
-          { modelName: { contains: word, mode: this.mode } },
-          { brand: { name: { contains: word, mode: this.mode } } },
-        ],
-      }))
-    }
+    const where = this.buildWhere(filters)
 
     const [devices, total] = await Promise.all([
       this.prisma.device.findMany({
@@ -389,6 +450,10 @@ export class DevicesRepository extends BaseRepository {
   async getTrendingDevicesSummary(
     limit: number = HOME_PAGE_LIMITS.TRENDING_DEVICES,
   ): Promise<TrendingDevicesSummary> {
+    const cacheKey = `limit:${limit}`
+    const cached = trendingDevicesSummaryCache.get(cacheKey)
+    if (cached) return cached
+
     const timeRanges: TimeRangeId[] = ['allTime', 'thisMonth', 'thisWeek']
 
     const results = await Promise.all(
@@ -396,6 +461,8 @@ export class DevicesRepository extends BaseRepository {
     )
     const [allTime, thisMonth, thisWeek] = results
 
-    return { allTime, thisMonth, thisWeek }
+    const summary = { allTime, thisMonth, thisWeek }
+    trendingDevicesSummaryCache.set(cacheKey, summary)
+    return summary
   }
 }
diff --git a/src/server/repositories/gpus.repository.ts b/src/server/repositories/gpus.repository.ts
index 8228144ea..6bcc092bc 100644
--- a/src/server/repositories/gpus.repository.ts
+++ b/src/server/repositories/gpus.repository.ts
@@ -3,7 +3,14 @@ import { ResourceError } from '@/lib/errors'
 import { type PaginationResult, paginate, calculateOffset } from '@/server/utils/pagination'
 import { Prisma } from '@orm/client'
 import { BaseRepository } from './base.repository'
-import type { GetGpusInput, CreateGpuInput, UpdateGpuInput } from '@/schemas/gpu'
+import type {
+  GetGpusInput,
+  GetGpuOptionsInput,
+  CreateGpuInput,
+  UpdateGpuInput,
+} from '@/schemas/gpu'
+
+type GpuOptionFilters = NonNullable<GetGpuOptionsInput>
 
 /**
  * Repository for GPU data access
@@ -34,6 +41,14 @@ export class GpusRepository extends BaseRepository {
     } satisfies Prisma.GpuInclude,
   } as const
 
+  static readonly selects = {
+    option: {
+      id: true,
+      modelName: true,
+      brand: { select: { id: true, name: true } },
+    } satisfies Prisma.GpuSelect,
+  } as const
+
   async byId(
     id: string,
     options: { limited?: boolean } = {},
@@ -167,6 +182,26 @@ export class GpusRepository extends BaseRepository {
     })
   }
 
+  async options(filters: GpuOptionFilters = {}): Promise<{
+    gpus: Prisma.GpuGetPayload<{ select: typeof GpusRepository.selects.option }>[]
+    hasMore: boolean
+  }> {
+    const limit = filters.limit ?? 50
+    const offset = filters.offset ?? 0
+    const gpus = await this.prisma.gpu.findMany({
+      where: this.buildWhereClause(filters.search, filters.brandId),
+      select: GpusRepository.selects.option,
+      orderBy: [{ brand: { name: this.sortOrder } }, { modelName: this.sortOrder }],
+      take: limit + 1,
+      skip: offset,
+    })
+
+    return {
+      gpus: gpus.slice(0, limit),
+      hasMore: gpus.length > limit,
+    }
+  }
+
   /**
    * Get GPUs with pagination metadata
    * Supports both web and mobile usage via options
diff --git a/src/server/repositories/listings.repository.ts b/src/server/repositories/listings.repository.ts
index 11bb8e314..467978e43 100644
--- a/src/server/repositories/listings.repository.ts
+++ b/src/server/repositories/listings.repository.ts
@@ -791,7 +791,7 @@ export class ListingsRepository extends BaseRepository {
         throw AppError.badRequest("The selected emulator does not support this game's system")
 
       // Determine approval
-      const canAutoApprove = await canUserAutoApprove(authorId)
+      const canAutoApprove = await canUserAutoApprove(authorId, tx)
       const isAuthorOrHigher = roleIncludesRole(userRole, Role.AUTHOR)
       const status: ApprovalStatus =
         canAutoApprove || isAuthorOrHigher ? ApprovalStatus.APPROVED : ApprovalStatus.PENDING
diff --git a/src/server/repositories/pc-listings.repository.ts b/src/server/repositories/pc-listings.repository.ts
index 0dff245a4..e29d62b56 100644
--- a/src/server/repositories/pc-listings.repository.ts
+++ b/src/server/repositories/pc-listings.repository.ts
@@ -704,7 +704,7 @@ export class PcListingsRepository extends BaseRepository {
     if (!isSystemCompatible)
       return AppError.badRequest("The selected emulator does not support this game's system")
 
-    const canAutoApprove = await canUserAutoApprove(authorId)
+    const canAutoApprove = await canUserAutoApprove(authorId, this.prisma)
     const isAuthorOrHigher = roleIncludesRole(userRole, Role.AUTHOR)
     const status: ApprovalStatus =
       canAutoApprove || isAuthorOrHigher ? ApprovalStatus.APPROVED : ApprovalStatus.PENDING
diff --git a/src/server/repositories/socs.repository.ts b/src/server/repositories/socs.repository.ts
index d2352772a..4e072bd47 100644
--- a/src/server/repositories/socs.repository.ts
+++ b/src/server/repositories/socs.repository.ts
@@ -2,7 +2,14 @@ import { PAGINATION } from '@/data/constants'
 import { ResourceError } from '@/lib/errors'
 import { Prisma, type SoC } from '@orm/client'
 import { BaseRepository } from './base.repository'
-import type { GetSoCsInput, CreateSoCInput, UpdateSoCInput } from '@/schemas/soc'
+import type {
+  GetSoCsInput,
+  GetSoCOptionsInput,
+  CreateSoCInput,
+  UpdateSoCInput,
+} from '@/schemas/soc'
+
+type SoCOptionFilters = NonNullable<GetSoCOptionsInput>
 
 /**
  * Repository for SoC (System on Chip) data access
@@ -52,6 +59,35 @@ export class SoCsRepository extends BaseRepository {
     })
   }
 
+  async options(filters: SoCOptionFilters = {}): Promise<{
+    socs: Pick<SoC, 'id' | 'name' | 'manufacturer'>[]
+    hasMore: boolean
+  }> {
+    const limit = filters.limit ?? 50
+    const offset = filters.offset ?? 0
+    const where: Prisma.SoCWhereInput = {
+      ...(filters.search && {
+        OR: [
+          { name: { contains: filters.search, mode: this.mode } },
+          { manufacturer: { contains: filters.search, mode: this.mode } },
+        ],
+      }),
+    }
+
+    const socs = await this.prisma.soC.findMany({
+      where,
+      select: { id: true, name: true, manufacturer: true },
+      orderBy: [{ manufacturer: this.sortOrder }, { name: this.sortOrder }],
+      take: limit + 1,
+      skip: offset,
+    })
+
+    return {
+      socs: socs.slice(0, limit),
+      hasMore: socs.length > limit,
+    }
+  }
+
   /**
    * Get SoCs by a list of IDs (basic fields)
    */
diff --git a/tests/filtering.spec.ts b/tests/filtering.spec.ts
index 1e8a32fd1..3f09f9624 100644
--- a/tests/filtering.spec.ts
+++ b/tests/filtering.spec.ts
@@ -2,14 +2,49 @@ import { test, expect } from './fixtures'
 import { ListingsPage } from './pages/ListingsPage'
 import type { Locator, Page } from '@playwright/test'
 
-async function selectFirstFilterOption(page: Page, filterButton: Locator, optionText?: string) {
-  await filterButton.click()
+async function waitForListingsTableIdle(page: Page) {
+  const tableBody = page.locator('table tbody').first()
+  const firstRow = page.locator('table tbody tr').first()
+  const noListingsMessage = page.getByText(/no listings found|no results|empty|nothing found/i)
+
+  await expect(firstRow.or(noListingsMessage)).toBeVisible()
+  if (await tableBody.isVisible()) {
+    await expect(tableBody).not.toHaveClass(/opacity-50/)
+  }
+}
+
+async function openFilterDropdown(filterButton: Locator) {
+  await filterButton.scrollIntoViewIfNeeded()
+  let opened = false
+  for (let attempt = 0; attempt < 3; attempt += 1) {
+    await filterButton.click()
+    try {
+      await expect(filterButton).toHaveAttribute('aria-expanded', 'true', { timeout: 750 })
+      opened = true
+      break
+    } catch {
+      // Retry because the filter button can re-render while URL-backed state updates.
+    }
+  }
+  expect(opened).toBe(true)
+  await expect(filterButton).toHaveAttribute('aria-expanded', 'true')
+}
+
+async function selectFirstFilterOption(
+  page: Page,
+  filterButton: Locator,
+  expectedParam: string,
+  optionText?: string,
+) {
+  await openFilterDropdown(filterButton)
   const options = page.locator('label:has(input[type="checkbox"])')
-  const firstOption = optionText ? options.filter({ hasText: optionText }).first() : options.first()
-  await expect(firstOption).toBeVisible()
-  await firstOption.click()
+  const option = optionText ? options.filter({ hasText: optionText }).first() : options.first()
+  await expect(option).toBeVisible()
+  await option.click()
+  await expect(page).toHaveURL(new RegExp(`[?&]${expectedParam}=`))
   await filterButton.click()
-  await expect(firstOption).toBeHidden()
+  await expect(filterButton).toHaveAttribute('aria-expanded', 'false')
+  await waitForListingsTableIdle(page)
 }
 
 test.describe('Filtering Tests', () => {
@@ -17,8 +52,9 @@ test.describe('Filtering Tests', () => {
     const listingsPage = new ListingsPage(page)
     await listingsPage.goto()
     await listingsPage.verifyPageLoaded()
+    await expect(listingsPage.listingItems.first()).toBeVisible()
 
-    await listingsPage.deviceFilter.click()
+    await openFilterDropdown(listingsPage.deviceFilter)
 
     const firstOption = page.locator('label:has(input[type="checkbox"])').first()
     await expect(firstOption).toBeVisible()
@@ -53,7 +89,8 @@ test.describe('Filtering Tests', () => {
     await listingsPage.verifyPageLoaded()
     await expect(listingsPage.listingItems.first()).toBeVisible()
 
-    await selectFirstFilterOption(page, listingsPage.deviceFilter)
+    const deviceName = await listingsPage.getFirstListingDeviceName()
+    await selectFirstFilterOption(page, listingsPage.deviceFilter, 'deviceIds', deviceName)
 
     await expect(page).toHaveURL(/[?&]deviceIds=/)
     await expect(listingsPage.listingItems.first().or(listingsPage.noListingsMessage)).toBeVisible()
@@ -65,7 +102,7 @@ test.describe('Filtering Tests', () => {
     await listingsPage.verifyPageLoaded()
     await expect(listingsPage.listingItems.first()).toBeVisible()
 
-    await selectFirstFilterOption(page, listingsPage.emulatorFilter)
+    await selectFirstFilterOption(page, listingsPage.emulatorFilter, 'emulatorIds')
 
     await expect(page).toHaveURL(/[?&]emulatorIds=/)
     await expect(listingsPage.listingItems.first().or(listingsPage.noListingsMessage)).toBeVisible()
@@ -77,7 +114,7 @@ test.describe('Filtering Tests', () => {
     await listingsPage.verifyPageLoaded()
     await expect(listingsPage.listingItems.first()).toBeVisible()
 
-    await selectFirstFilterOption(page, listingsPage.performanceFilter)
+    await selectFirstFilterOption(page, listingsPage.performanceFilter, 'performanceIds')
 
     await expect(page).toHaveURL(/[?&]performanceIds=/)
     await expect(listingsPage.listingItems.first().or(listingsPage.noListingsMessage)).toBeVisible()
@@ -89,12 +126,13 @@ test.describe('Filtering Tests', () => {
     await listingsPage.verifyPageLoaded()
     await expect(listingsPage.listingItems.first()).toBeVisible()
 
-    await selectFirstFilterOption(page, listingsPage.deviceFilter)
-    await expect(page).toHaveURL(/[?&]deviceIds=/)
-
-    await selectFirstFilterOption(page, listingsPage.emulatorFilter)
+    const deviceName = await listingsPage.getFirstListingDeviceName()
+    await selectFirstFilterOption(page, listingsPage.emulatorFilter, 'emulatorIds')
     await expect(page).toHaveURL(/[?&]emulatorIds=/)
 
+    await selectFirstFilterOption(page, listingsPage.deviceFilter, 'deviceIds', deviceName)
+    await expect(page).toHaveURL(/[?&]deviceIds=/)
+
     await expect(listingsPage.clearFiltersButton).toBeVisible()
     await listingsPage.clearFiltersButton.click()
 
@@ -109,7 +147,8 @@ test.describe('Filtering Tests', () => {
     await listingsPage.verifyPageLoaded()
     await expect(listingsPage.listingItems.first()).toBeVisible()
 
-    await selectFirstFilterOption(page, listingsPage.deviceFilter)
+    const deviceName = await listingsPage.getFirstListingDeviceName()
+    await selectFirstFilterOption(page, listingsPage.deviceFilter, 'deviceIds', deviceName)
 
     await expect(page).toHaveURL(/[?&]deviceIds=/)
   })
@@ -121,7 +160,7 @@ test.describe('Filtering Tests', () => {
     await expect(listingsPage.listingItems.first()).toBeVisible()
 
     const deviceName = await listingsPage.getFirstListingDeviceName()
-    await selectFirstFilterOption(page, listingsPage.deviceFilter, deviceName)
+    await selectFirstFilterOption(page, listingsPage.deviceFilter, 'deviceIds', deviceName)
     await expect(page).toHaveURL(/[?&]deviceIds=/)
 
     await listingsPage.clickFirstListing()
diff --git a/tests/pages/ListingsPage.ts b/tests/pages/ListingsPage.ts
index 4d570183c..3d2639cf9 100644
--- a/tests/pages/ListingsPage.ts
+++ b/tests/pages/ListingsPage.ts
@@ -69,8 +69,12 @@ export class ListingsPage extends BasePage {
   async clickFirstListing() {
     const firstRow = this.listingItems.first()
     await expect(firstRow).toBeVisible()
-    const link = firstRow.locator('a[href*="/listings/"]').first()
-    await link.click()
+    const link = firstRow.locator('td').first().getByRole('link').first()
+    await expect(link).toBeVisible()
+    const href = await link.getAttribute('href')
+    if (!href) throw new Error('Expected first listing link to have an href')
+    expect(href).toMatch(/^\/listings\/[^/]+$/)
+    await this.page.goto(href)
     await expect(this.page).toHaveURL(/\/listings\/[^/]+/)
   }
 
diff --git a/tests/third-party-services.spec.ts b/tests/third-party-services.spec.ts
new file mode 100644
index 000000000..1e630a5aa
--- /dev/null
+++ b/tests/third-party-services.spec.ts
@@ -0,0 +1,30 @@
+import { test, expect } from './fixtures'
+
+const OPTIONAL_SERVICE_REQUEST_PATTERNS = [
+  'storage.ko-fi.com',
+  'googletagmanager.com',
+  'google-analytics.com',
+  '_vercel/insights',
+  '_vercel/speed-insights',
+  'ingest.us.sentry.io',
+] as const
+
+test.describe('Optional browser services', () => {
+  test('does not request disabled analytics, Ko-fi, or Sentry scripts in test builds', async ({
+    page,
+  }) => {
+    const optionalServiceRequests: string[] = []
+
+    page.on('request', (request) => {
+      const url = request.url()
+      if (OPTIONAL_SERVICE_REQUEST_PATTERNS.some((pattern) => url.includes(pattern))) {
+        optionalServiceRequests.push(url)
+      }
+    })
+
+    await page.goto('/')
+    await page.waitForLoadState('domcontentloaded')
+
+    expect(optionalServiceRequests).toEqual([])
+  })
+})

From 95f1f3f7d9cff9c3c4cabc5a0ccb20b39c329502 Mon Sep 17 00:00:00 2001
From: Producdevity <y.gherbi.dev@gmail.com>
Date: Thu, 28 May 2026 14:00:54 +0200
Subject: [PATCH 09/35] refactor: update AsyncSocFilterSelect tests to use
 constants for options and labels

---
 .../filters/AsyncSocFilterSelect.test.tsx     | 28 ++++++++++++++-----
 1 file changed, 21 insertions(+), 7 deletions(-)

diff --git a/src/app/listings/components/filters/AsyncSocFilterSelect.test.tsx b/src/app/listings/components/filters/AsyncSocFilterSelect.test.tsx
index dc2d90412..db38e04a1 100644
--- a/src/app/listings/components/filters/AsyncSocFilterSelect.test.tsx
+++ b/src/app/listings/components/filters/AsyncSocFilterSelect.test.tsx
@@ -18,6 +18,20 @@ vi.mock('@/lib/api', () => ({
 
 let AsyncSocFilterSelect: typeof AsyncSocFilterSelectComponent
 
+const OPTION_SOC = {
+  id: 'soc-1',
+  name: 'Snapdragon 8 Gen 2',
+  manufacturer: 'Qualcomm',
+} as const
+const SELECTED_SOC = {
+  id: 'soc-selected',
+  name: 'Dimensity 1100',
+  manufacturer: 'MediaTek',
+} as const
+const SOC_LABEL = 'SoCs'
+const OPTION_SOC_LABEL = `${OPTION_SOC.manufacturer} ${OPTION_SOC.name}`
+const SELECTED_SOC_LABEL = `${SELECTED_SOC.manufacturer} ${SELECTED_SOC.name}`
+
 interface IdsInput {
   ids: string[]
 }
@@ -49,7 +63,7 @@ function setupApiMocks() {
 
       return descriptors.map(() => ({
         data: {
-          socs: [{ id: 'soc-1', name: 'Snapdragon 8 Gen 2', manufacturer: 'Qualcomm' }],
+          socs: [OPTION_SOC],
           hasMore: false,
         },
         isFetching: false,
@@ -59,8 +73,8 @@ function setupApiMocks() {
   apiMocks.socsGetByIdsUseQuery.mockImplementation((input: IdsInput) => ({
     data: input.ids.map((id) => ({
       id,
-      name: 'Dimensity 1100',
-      manufacturer: 'MediaTek',
+      name: SELECTED_SOC.name,
+      manufacturer: SELECTED_SOC.manufacturer,
     })),
   }))
 }
@@ -76,10 +90,10 @@ describe('AsyncSocFilterSelect', () => {
   })
 
   it('maps SoC option and selected labels', () => {
-    render(<AsyncSocFilterSelect label="SoCs" value={['soc-selected']} onChange={vi.fn()} />)
+    render(<AsyncSocFilterSelect label={SOC_LABEL} value={[SELECTED_SOC.id]} onChange={vi.fn()} />)
 
-    expect(screen.getByText('MediaTek Dimensity 1100')).toBeInTheDocument()
-    fireEvent.click(screen.getByRole('button', { name: 'SoCs multi-select' }))
-    expect(screen.getByText('Qualcomm Snapdragon 8 Gen 2')).toBeInTheDocument()
+    expect(screen.getByText(SELECTED_SOC_LABEL)).toBeInTheDocument()
+    fireEvent.click(screen.getByRole('button', { name: `${SOC_LABEL} multi-select` }))
+    expect(screen.getByText(OPTION_SOC_LABEL)).toBeInTheDocument()
   })
 })

From 7cd1846752e8da074f75c609f0624f106f22567d Mon Sep 17 00:00:00 2001
From: Producdevity <y.gherbi.dev@gmail.com>
Date: Thu, 28 May 2026 14:02:21 +0200
Subject: [PATCH 10/35] refactor: update service and tests to use
 TrustPrismaTransaction type for improved type safety

---
 src/lib/trust/service.test.ts | 35 +++++++++++++++++--------------
 src/lib/trust/service.ts      | 39 +++++++++++++++++++++--------------
 2 files changed, 43 insertions(+), 31 deletions(-)

diff --git a/src/lib/trust/service.test.ts b/src/lib/trust/service.test.ts
index 82fc53b81..7be791cca 100644
--- a/src/lib/trust/service.test.ts
+++ b/src/lib/trust/service.test.ts
@@ -1,5 +1,6 @@
 import { describe, expect, it, beforeEach, vi } from 'vitest'
 import { TrustAction } from '@orm'
+import type { TrustPrismaTransaction } from './service'
 
 vi.mock('@/lib/analytics', () => ({
   default: {
@@ -38,7 +39,7 @@ describe('canUserAutoApprove', () => {
     prismaCtx.user.findUnique.mockResolvedValue({ trustScore: 250 })
     const { canUserAutoApprove } = await import('./service')
 
-    const result = await canUserAutoApprove('user-1', prismaCtx as never)
+    const result = await canUserAutoApprove('user-1', prismaCtx)
 
     expect(result).toBe(true)
     expect(prismaCtx.user.findUnique).toHaveBeenCalledWith({
@@ -51,7 +52,7 @@ describe('canUserAutoApprove', () => {
     prismaCtx.user.findUnique.mockResolvedValue(null)
     const { canUserAutoApprove } = await import('./service')
 
-    await expect(canUserAutoApprove('missing-user', prismaCtx as never)).resolves.toBe(false)
+    await expect(canUserAutoApprove('missing-user', prismaCtx)).resolves.toBe(false)
   })
 })
 
@@ -65,7 +66,7 @@ describe('TrustService.applyBulkManualAdjustments', () => {
 
   async function createService() {
     const { TrustService } = await import('./service')
-    return new TrustService(prismaCtx as never)
+    return new TrustService(prismaCtx)
   }
 
   it('returns 0 and makes no DB calls for an empty map', async () => {
@@ -275,9 +276,11 @@ describe('TrustService.applyBulkManualAdjustments', () => {
   })
 
   it('wraps in $transaction when prisma has it', async () => {
-    const mockTransaction = vi.fn(async (fn: (ctx: MockPrismaCtx) => Promise<number>) =>
-      fn(prismaCtx),
-    )
+    let transactionCallCount = 0
+    const mockTransaction = async <T>(fn: (ctx: TrustPrismaTransaction) => Promise<T>) => {
+      transactionCallCount++
+      return fn(prismaCtx)
+    }
     const prismaWithTx = {
       ...prismaCtx,
       $transaction: mockTransaction,
@@ -287,7 +290,7 @@ describe('TrustService.applyBulkManualAdjustments', () => {
     prismaCtx.user.findUnique.mockResolvedValue({ name: 'Admin', email: 'admin@test.com' })
 
     const { TrustService } = await import('./service')
-    const service = new TrustService(prismaWithTx as never)
+    const service = new TrustService(prismaWithTx)
 
     await service.applyBulkManualAdjustments({
       adjustments: new Map([['user-1', 5]]),
@@ -295,7 +298,7 @@ describe('TrustService.applyBulkManualAdjustments', () => {
       adminUserId: 'admin-1',
     })
 
-    expect(mockTransaction).toHaveBeenCalledTimes(1)
+    expect(transactionCallCount).toBe(1)
   })
 
   it('calls execute directly when prisma is a transaction client (no $transaction)', async () => {
@@ -379,7 +382,7 @@ describe('TrustService.applyManualAdjustment', () => {
 
   async function createService() {
     const { TrustService } = await import('./service')
-    return new TrustService(prismaCtx as never)
+    return new TrustService(prismaCtx)
   }
 
   it('throws when adjustment is zero', async () => {
@@ -468,7 +471,7 @@ describe('TrustService.reverseLogAction', () => {
 
   async function createService() {
     const { TrustService } = await import('./service')
-    return new TrustService(prismaCtx as never)
+    return new TrustService(prismaCtx)
   }
 
   it('negates positive action weight (LISTING_RECEIVED_UPVOTE: +2 → -2)', async () => {
@@ -570,14 +573,16 @@ describe('TrustService.reverseLogAction', () => {
   })
 
   it('wraps in $transaction when prisma has it', async () => {
-    const mockTransaction = vi.fn(async (fn: (ctx: MockPrismaCtx) => Promise<void>) =>
-      fn(prismaCtx),
-    )
+    let transactionCallCount = 0
+    const mockTransaction = async <T>(fn: (ctx: TrustPrismaTransaction) => Promise<T>) => {
+      transactionCallCount++
+      return fn(prismaCtx)
+    }
     const prismaWithTx = { ...prismaCtx, $transaction: mockTransaction }
     prismaCtx.user.findUnique.mockResolvedValue({ trustScore: 10 })
 
     const { TrustService } = await import('./service')
-    const service = new TrustService(prismaWithTx as never)
+    const service = new TrustService(prismaWithTx)
 
     await service.reverseLogAction({
       userId: 'user-1',
@@ -585,7 +590,7 @@ describe('TrustService.reverseLogAction', () => {
       metadata: {},
     })
 
-    expect(mockTransaction).toHaveBeenCalledTimes(1)
+    expect(transactionCallCount).toBe(1)
   })
 
   it('calls execute directly when prisma is a transaction client (no $transaction)', async () => {
diff --git a/src/lib/trust/service.ts b/src/lib/trust/service.ts
index 429f89fc0..287fce81b 100644
--- a/src/lib/trust/service.ts
+++ b/src/lib/trust/service.ts
@@ -3,7 +3,7 @@ import analytics from '@/lib/analytics'
 import { ResourceError } from '@/lib/errors'
 import { prisma } from '@/server/db'
 import { validateData } from '@/server/utils/validation'
-import { TrustAction, type Prisma, type PrismaClient } from '@orm/client'
+import { TrustAction, type Prisma } from '@orm/client'
 import { TRUST_ACTIONS, TRUST_CONFIG, getTrustLevel, hasTrustLevel } from './config'
 
 const UNKNOWN_TRUST_LEVEL_NAME = 'Unranked'
@@ -29,7 +29,9 @@ interface ApplyTrustActionParams {
   context?: TrustActionContext
 }
 
-type PrismaClientOrTransaction = PrismaClient | Prisma.TransactionClient
+interface AutoApprovalPrismaClient {
+  user: Pick<Prisma.TransactionClient['user'], 'findUnique'>
+}
 
 export async function applyTrustAction(params: ApplyTrustActionParams): Promise<void> {
   const { userId, action, context = {} } = params
@@ -92,6 +94,7 @@ export async function applyTrustAction(params: ApplyTrustActionParams): Promise<
 /**
  * Reverses the trust impact of a previously applied trust action.
  * Applies the negative of the original action's weight.
+ * TODO: figure out if we should use this
  */
 export async function reverseTrustAction(params: {
   userId: string
@@ -164,7 +167,7 @@ export async function reverseTrustAction(params: {
 
 export async function canUserAutoApprove(
   userId: string,
-  prismaClient: PrismaClientOrTransaction = prisma,
+  prismaClient: AutoApprovalPrismaClient = prisma,
 ): Promise<boolean> {
   const user = await prismaClient.user.findUnique({
     where: { id: userId },
@@ -236,10 +239,14 @@ export async function applyMonthlyActiveBonus(): Promise<{
   return { processedUsers, errors }
 }
 
-type PrismaTransaction = Omit<
-  PrismaClient,
-  '$connect' | '$disconnect' | '$on' | '$transaction' | '$use' | '$extends'
->
+export interface TrustPrismaTransaction {
+  user: Pick<Prisma.TransactionClient['user'], 'findUnique' | 'findMany' | 'update'>
+  trustActionLog: Pick<Prisma.TransactionClient['trustActionLog'], 'create' | 'createMany'>
+}
+
+export type TrustPrismaClient = TrustPrismaTransaction & {
+  $transaction?<T>(fn: (tx: TrustPrismaTransaction) => Promise<T>): Promise<T>
+}
 
 /**
  * TrustService class for managing trust actions.
@@ -247,7 +254,7 @@ type PrismaTransaction = Omit<
  * TransactionClient (participates in an outer transaction).
  */
 export class TrustService {
-  constructor(private readonly prisma: PrismaClient | PrismaTransaction) {}
+  constructor(private readonly prisma: TrustPrismaClient) {}
 
   async logAction(params: {
     userId: string
@@ -263,7 +270,7 @@ export class TrustService {
 
     const weight = TRUST_ACTIONS[action].weight
 
-    const executeInTransaction = async (prismaCtx: PrismaTransaction) => {
+    const executeInTransaction = async (prismaCtx: TrustPrismaTransaction) => {
       const currentUser = await prismaCtx.user.findUnique({
         where: { id: userId },
         select: { trustScore: true },
@@ -311,7 +318,7 @@ export class TrustService {
       }
     }
 
-    if ('$transaction' in this.prisma) {
+    if (this.prisma.$transaction) {
       await this.prisma.$transaction(executeInTransaction)
     } else {
       await executeInTransaction(this.prisma)
@@ -333,7 +340,7 @@ export class TrustService {
     const reversalWeight = -TRUST_ACTIONS[originalAction].weight
     if (reversalWeight === 0) return
 
-    const executeInTransaction = async (prismaCtx: PrismaTransaction) => {
+    const executeInTransaction = async (prismaCtx: TrustPrismaTransaction) => {
       const currentUser = await prismaCtx.user.findUnique({
         where: { id: userId },
         select: { trustScore: true },
@@ -381,7 +388,7 @@ export class TrustService {
       }
     }
 
-    if ('$transaction' in this.prisma) {
+    if (this.prisma.$transaction) {
       await this.prisma.$transaction(executeInTransaction)
     } else {
       await executeInTransaction(this.prisma)
@@ -400,7 +407,7 @@ export class TrustService {
       throw ResourceError.trust.adjustmentCannotBeZero()
     }
 
-    const executeAdjustment = async (prismaCtx: PrismaTransaction) => {
+    const executeAdjustment = async (prismaCtx: TrustPrismaTransaction) => {
       const currentUser = await prismaCtx.user.findUnique({
         where: { id: userId },
         select: { trustScore: true, name: true, email: true },
@@ -464,7 +471,7 @@ export class TrustService {
       }
     }
 
-    if ('$transaction' in this.prisma) {
+    if (this.prisma.$transaction) {
       await this.prisma.$transaction(executeAdjustment)
     } else {
       await executeAdjustment(this.prisma)
@@ -483,7 +490,7 @@ export class TrustService {
 
     const userIds = nonZeroEntries.map(([id]) => id)
 
-    const executeBulk = async (prismaCtx: PrismaTransaction) => {
+    const executeBulk = async (prismaCtx: TrustPrismaTransaction) => {
       const users = await prismaCtx.user.findMany({
         where: { id: { in: userIds } },
         select: { id: true, trustScore: true },
@@ -571,7 +578,7 @@ export class TrustService {
       return foundEntries.length
     }
 
-    if ('$transaction' in this.prisma) {
+    if (this.prisma.$transaction) {
       return this.prisma.$transaction(executeBulk)
     }
     return executeBulk(this.prisma)

From 6acb6aacbdb603e3c89a65dce9153e6099e59d86 Mon Sep 17 00:00:00 2001
From: Producdevity <y.gherbi.dev@gmail.com>
Date: Thu, 28 May 2026 14:05:49 +0200
Subject: [PATCH 11/35] refactor: update service and tests to use
 TrustPrismaTransaction type for improved type safety

---
 src/lib/env.test.ts                | 52 ++++++++++++++++++++----------
 tests/helpers/external-services.ts |  6 ++--
 2 files changed, 38 insertions(+), 20 deletions(-)

diff --git a/src/lib/env.test.ts b/src/lib/env.test.ts
index 9541ea4bc..e4064e4ff 100644
--- a/src/lib/env.test.ts
+++ b/src/lib/env.test.ts
@@ -1,5 +1,23 @@
 import { afterEach, describe, expect, it, vi } from 'vitest'
 
+const ENV_KEYS = {
+  nodeEnv: 'NODE_ENV',
+  appEnv: 'NEXT_PUBLIC_APP_ENV',
+  enableAnalytics: 'NEXT_PUBLIC_ENABLE_ANALYTICS',
+  enableKofiWidget: 'NEXT_PUBLIC_ENABLE_KOFI_WIDGET',
+  enableSentry: 'NEXT_PUBLIC_ENABLE_SENTRY',
+} as const
+
+const ENV_VALUES = {
+  empty: '',
+  false: 'false',
+  local: 'local',
+  preview: 'preview',
+  production: 'production',
+  test: 'test',
+  true: 'true',
+} as const
+
 async function loadEnv(overrides: Record<string, string>) {
   vi.resetModules()
   vi.unstubAllEnvs()
@@ -19,55 +37,55 @@ afterEach(() => {
 describe('env', () => {
   it('uses explicit public app env for public production checks', async () => {
     const { env } = await loadEnv({
-      NODE_ENV: 'production',
-      NEXT_PUBLIC_APP_ENV: 'production',
+      [ENV_KEYS.nodeEnv]: ENV_VALUES.production,
+      [ENV_KEYS.appEnv]: ENV_VALUES.production,
     })
 
-    expect(env.APP_ENV).toBe('production')
+    expect(env.APP_ENV).toBe(ENV_VALUES.production)
     expect(env.IS_PUBLIC_PRODUCTION).toBe(true)
     expect(env.IS_PRODUCTION_BUILD).toBe(true)
   })
 
   it('does not infer public production from a production build without explicit app env', async () => {
     const { env } = await loadEnv({
-      NODE_ENV: 'production',
-      NEXT_PUBLIC_APP_ENV: '',
+      [ENV_KEYS.nodeEnv]: ENV_VALUES.production,
+      [ENV_KEYS.appEnv]: ENV_VALUES.empty,
     })
 
-    expect(env.APP_ENV).toBe('local')
+    expect(env.APP_ENV).toBe(ENV_VALUES.local)
     expect(env.IS_PUBLIC_PRODUCTION).toBe(false)
     expect(env.IS_PRODUCTION_BUILD).toBe(true)
   })
 
   it('uses explicit public preview app env for Vercel preview builds', async () => {
     const { env } = await loadEnv({
-      NODE_ENV: 'production',
-      NEXT_PUBLIC_APP_ENV: 'preview',
+      [ENV_KEYS.nodeEnv]: ENV_VALUES.production,
+      [ENV_KEYS.appEnv]: ENV_VALUES.preview,
     })
 
-    expect(env.APP_ENV).toBe('preview')
+    expect(env.APP_ENV).toBe(ENV_VALUES.preview)
     expect(env.IS_PUBLIC_PRODUCTION).toBe(false)
     expect(env.IS_PRODUCTION_BUILD).toBe(true)
   })
 
   it('falls back to test app env for test runs when app env is omitted', async () => {
     const { env } = await loadEnv({
-      NODE_ENV: 'test',
-      NEXT_PUBLIC_APP_ENV: '',
+      [ENV_KEYS.nodeEnv]: ENV_VALUES.test,
+      [ENV_KEYS.appEnv]: ENV_VALUES.empty,
     })
 
-    expect(env.APP_ENV).toBe('test')
+    expect(env.APP_ENV).toBe(ENV_VALUES.test)
     expect(env.IS_PUBLIC_PRODUCTION).toBe(false)
     expect(env.IS_TEST_BUILD).toBe(true)
   })
 
   it('only enables optional browser services when their public flags are true', async () => {
     const { env } = await loadEnv({
-      NODE_ENV: 'production',
-      NEXT_PUBLIC_APP_ENV: 'production',
-      NEXT_PUBLIC_ENABLE_ANALYTICS: 'true',
-      NEXT_PUBLIC_ENABLE_KOFI_WIDGET: 'false',
-      NEXT_PUBLIC_ENABLE_SENTRY: 'true',
+      [ENV_KEYS.nodeEnv]: ENV_VALUES.production,
+      [ENV_KEYS.appEnv]: ENV_VALUES.production,
+      [ENV_KEYS.enableAnalytics]: ENV_VALUES.true,
+      [ENV_KEYS.enableKofiWidget]: ENV_VALUES.false,
+      [ENV_KEYS.enableSentry]: ENV_VALUES.true,
     })
 
     expect(env.ENABLE_ANALYTICS).toBe(true)
diff --git a/tests/helpers/external-services.ts b/tests/helpers/external-services.ts
index a373396c7..3d69adcf8 100644
--- a/tests/helpers/external-services.ts
+++ b/tests/helpers/external-services.ts
@@ -6,7 +6,7 @@ const transparentPng = Buffer.from(
 )
 
 export async function registerExternalServiceMocks(page: Page) {
-  await page.route('**/_vercel/speed-insights/script.js', async (route) => {
+  await page.route('**/_vercel/speed-insights/script.js*', async (route) => {
     await route.fulfill({
       status: 200,
       contentType: 'application/javascript',
@@ -15,7 +15,7 @@ export async function registerExternalServiceMocks(page: Page) {
   })
 
   await page.route(
-    'https://storage.ko-fi.com/cdn/scripts/floating-chat-wrapper.css',
+    'https://storage.ko-fi.com/cdn/scripts/floating-chat-wrapper.css*',
     async (route) => {
       await route.fulfill({
         status: 200,
@@ -25,7 +25,7 @@ export async function registerExternalServiceMocks(page: Page) {
     },
   )
 
-  await page.route('https://storage.ko-fi.com/cdn/scripts/overlay-widget.js', async (route) => {
+  await page.route('https://storage.ko-fi.com/cdn/scripts/overlay-widget.js*', async (route) => {
     await route.fulfill({
       status: 200,
       contentType: 'application/javascript',

From 40d79ad25392d0fb827b180360ad74279f4b74ad Mon Sep 17 00:00:00 2001
From: Producdevity <y.gherbi.dev@gmail.com>
Date: Thu, 28 May 2026 14:09:27 +0200
Subject: [PATCH 12/35] refactor: update form event types from FormEvent to
 SubmitEvent for consistency

---
 src/app/admin/cpus/components/CpuModal.tsx             | 4 ++--
 src/app/admin/reports/components/ReportStatusModal.tsx | 6 +++---
 src/app/profile/components/PcPresetModal.tsx           | 4 ++--
 3 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/src/app/admin/cpus/components/CpuModal.tsx b/src/app/admin/cpus/components/CpuModal.tsx
index 40133c086..062ae1737 100644
--- a/src/app/admin/cpus/components/CpuModal.tsx
+++ b/src/app/admin/cpus/components/CpuModal.tsx
@@ -1,6 +1,6 @@
 'use client'
 
-import { useState, useEffect, type FormEvent } from 'react'
+import { useState, useEffect, type SubmitEvent } from 'react'
 import { Button, Input, Modal, Autocomplete } from '@/components/ui'
 import { api } from '@/lib/api'
 import { type RouterInput, type RouterOutput } from '@/types/trpc'
@@ -39,7 +39,7 @@ function CpuModal(props: Props) {
     setSuccess('')
   }, [props.cpuData, props.isOpen])
 
-  const handleSubmit = async (ev: FormEvent) => {
+  const handleSubmit = async (ev: SubmitEvent) => {
     ev.preventDefault()
     setError('')
     setSuccess('')
diff --git a/src/app/admin/reports/components/ReportStatusModal.tsx b/src/app/admin/reports/components/ReportStatusModal.tsx
index 056a169e8..f877e9aaf 100644
--- a/src/app/admin/reports/components/ReportStatusModal.tsx
+++ b/src/app/admin/reports/components/ReportStatusModal.tsx
@@ -1,6 +1,6 @@
 'use client'
 
-import { useState, useEffect, type FormEvent, type ChangeEvent } from 'react'
+import { useState, useEffect, type SubmitEvent, type ChangeEvent } from 'react'
 import { Button, Input, Modal } from '@/components/ui'
 import { api } from '@/lib/api'
 import { type ReportStatusType } from '@/schemas/listingReport'
@@ -49,8 +49,8 @@ function ReportStatusModal(props: Props) {
     }
   }, [props.isOpen, props.report])
 
-  const handleSubmit = async (e: FormEvent) => {
-    e.preventDefault()
+  const handleSubmit = async (ev: SubmitEvent) => {
+    ev.preventDefault()
     if (!props.report) return
 
     setError('')
diff --git a/src/app/profile/components/PcPresetModal.tsx b/src/app/profile/components/PcPresetModal.tsx
index e7c330c57..35ae0553a 100644
--- a/src/app/profile/components/PcPresetModal.tsx
+++ b/src/app/profile/components/PcPresetModal.tsx
@@ -1,6 +1,6 @@
 'use client'
 
-import { useCallback, useState, useEffect, type FormEvent } from 'react'
+import { useCallback, useState, useEffect, type SubmitEvent } from 'react'
 import { Button, Input, Modal, Autocomplete, SelectInput } from '@/components/ui'
 import { PC_OS_OPTIONS } from '@/data/pc-os'
 import { api } from '@/lib/api'
@@ -101,7 +101,7 @@ function PcPresetModal(props: Props) {
     setSuccess('')
   }, [props.preset, props.isOpen])
 
-  const handleSubmit = async (ev: FormEvent) => {
+  const handleSubmit = async (ev: SubmitEvent) => {
     ev.preventDefault()
     setError('')
     setSuccess('')

From 26e4a60b433fde799bf4a4b702258f9fdf8e3fc2 Mon Sep 17 00:00:00 2001
From: Producdevity <y.gherbi.dev@gmail.com>
Date: Thu, 28 May 2026 15:30:08 +0200
Subject: [PATCH 13/35] refactor: add internal-regex setting and adjust path
 groups for ESLint import rules

---
 eslint.config.mjs | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/eslint.config.mjs b/eslint.config.mjs
index 64b1f9ba1..758ce6075 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -24,7 +24,6 @@ const eslintConfig = [
     ignores: [
       '*.log',
       '*.tsbuildinfo',
-      '.claude/**',
       '.clerk/**',
       '.husky/**',
       '.idea/**',
@@ -37,7 +36,6 @@ const eslintConfig = [
       'next-env.d.ts',
       'node_modules/**',
       'notes/**',
-      'notes/**',
       'out/**',
       'prisma/generated/**',
       'public/**',
@@ -112,6 +110,7 @@ const eslintConfig = [
   {
     files: ['**/*.{js,jsx,ts,tsx}'],
     settings: {
+      'import/internal-regex': '^(@/|~/|@orm(?:/.*)?$)',
       'import/resolver': {
         typescript: { project: './tsconfig.json' },
         node: { extensions: ['.js', '.jsx', '.ts', '.tsx'] },
@@ -128,6 +127,8 @@ const eslintConfig = [
           pathGroups: [
             { pattern: '@/**', group: 'internal', position: 'before' },
             { pattern: '~/**', group: 'internal', position: 'before' },
+            { pattern: '@orm', group: 'internal', position: 'after' },
+            { pattern: '@orm/**', group: 'internal', position: 'after' },
             { pattern: './**.module.css', group: 'sibling', position: 'after' },
             { pattern: './**.css', group: 'sibling', position: 'after' },
           ],

From 5d744d2826326fe2539cd6bf881cbf34c69da09c Mon Sep 17 00:00:00 2001
From: Producdevity <y.gherbi.dev@gmail.com>
Date: Thu, 28 May 2026 15:44:46 +0200
Subject: [PATCH 14/35] fix: address review and own-report handling

---
 .../listings/[id]/components/ListingDetailsClient.tsx  |  1 +
 .../listings/[id]/components/ReportListingButton.tsx   | 10 +++-------
 .../[id]/components/PcListingDetailsClient.tsx         |  1 +
 .../[id]/components/PcReportListingButton.tsx          | 10 +++-------
 src/lib/trust/service.ts                               |  4 ++--
 5 files changed, 10 insertions(+), 16 deletions(-)

diff --git a/src/app/listings/[id]/components/ListingDetailsClient.tsx b/src/app/listings/[id]/components/ListingDetailsClient.tsx
index 4842bba22..4abf7f117 100644
--- a/src/app/listings/[id]/components/ListingDetailsClient.tsx
+++ b/src/app/listings/[id]/components/ListingDetailsClient.tsx
@@ -150,6 +150,7 @@ function ListingDetailsClient(props: Props) {
                 <ReportListingButton
                   listingId={props.listing.id}
                   authorId={props.listing.authorId}
+                  currentUserId={currentUserQuery.data?.id}
                   onSuccess={refreshData}
                 />
                 <VerifyListingButton
diff --git a/src/app/listings/[id]/components/ReportListingButton.tsx b/src/app/listings/[id]/components/ReportListingButton.tsx
index f20f09253..3f423a5bc 100644
--- a/src/app/listings/[id]/components/ReportListingButton.tsx
+++ b/src/app/listings/[id]/components/ReportListingButton.tsx
@@ -1,6 +1,5 @@
 'use client'
 
-import { useUser } from '@clerk/nextjs'
 import { Flag } from 'lucide-react'
 import { useState } from 'react'
 import { Button } from '@/components/ui'
@@ -9,18 +8,15 @@ import ReportListingModal from './ReportListingModal'
 interface Props {
   listingId: string
   authorId: string
+  currentUserId?: string | null
   onSuccess?: () => void
 }
 
 function ReportListingButton(props: Props) {
-  const { user } = useUser()
   const [isModalOpen, setIsModalOpen] = useState(false)
 
-  // Don't show report button if user is not logged in
-  if (!user?.id) return null
-
-  // Don't show report button if the user is the author
-  if (user.id === props.authorId) return null
+  if (!props.currentUserId) return null
+  if (props.currentUserId === props.authorId) return null
 
   const handleSuccess = () => {
     setIsModalOpen(false)
diff --git a/src/app/pc-listings/[id]/components/PcListingDetailsClient.tsx b/src/app/pc-listings/[id]/components/PcListingDetailsClient.tsx
index 0cb59432c..148cf0242 100644
--- a/src/app/pc-listings/[id]/components/PcListingDetailsClient.tsx
+++ b/src/app/pc-listings/[id]/components/PcListingDetailsClient.tsx
@@ -215,6 +215,7 @@ function PcListingDetailsClient(props: Props) {
                 <PcReportListingButton
                   pcListingId={props.pcListing.id}
                   authorId={props.pcListing.authorId}
+                  currentUserId={currentUserQuery.data?.id}
                   onSuccess={refreshData}
                 />
                 <VerifyPcListingButton
diff --git a/src/app/pc-listings/[id]/components/PcReportListingButton.tsx b/src/app/pc-listings/[id]/components/PcReportListingButton.tsx
index bcd3356d6..96459bc7b 100644
--- a/src/app/pc-listings/[id]/components/PcReportListingButton.tsx
+++ b/src/app/pc-listings/[id]/components/PcReportListingButton.tsx
@@ -1,6 +1,5 @@
 'use client'
 
-import { useUser } from '@clerk/nextjs'
 import { Flag } from 'lucide-react'
 import { useState } from 'react'
 import { Button } from '@/components/ui'
@@ -9,18 +8,15 @@ import PcReportListingModal from './PcReportListingModal'
 interface Props {
   pcListingId: string
   authorId: string
+  currentUserId?: string | null
   onSuccess?: () => void
 }
 
 function PcReportListingButton(props: Props) {
-  const { user } = useUser()
   const [isModalOpen, setIsModalOpen] = useState(false)
 
-  // Don't show report button if user is not logged in
-  if (!user?.id) return null
-
-  // Don't show report button if the user is the author
-  if (user.id === props.authorId) return null
+  if (!props.currentUserId) return null
+  if (props.currentUserId === props.authorId) return null
 
   const handleSuccess = () => {
     setIsModalOpen(false)
diff --git a/src/lib/trust/service.ts b/src/lib/trust/service.ts
index 287fce81b..df8127689 100644
--- a/src/lib/trust/service.ts
+++ b/src/lib/trust/service.ts
@@ -3,7 +3,8 @@ import analytics from '@/lib/analytics'
 import { ResourceError } from '@/lib/errors'
 import { prisma } from '@/server/db'
 import { validateData } from '@/server/utils/validation'
-import { TrustAction, type Prisma } from '@orm/client'
+import { TrustAction } from '@orm'
+import { type Prisma } from '@orm/client'
 import { TRUST_ACTIONS, TRUST_CONFIG, getTrustLevel, hasTrustLevel } from './config'
 
 const UNKNOWN_TRUST_LEVEL_NAME = 'Unranked'
@@ -94,7 +95,6 @@ export async function applyTrustAction(params: ApplyTrustActionParams): Promise<
 /**
  * Reverses the trust impact of a previously applied trust action.
  * Applies the negative of the original action's weight.
- * TODO: figure out if we should use this
  */
 export async function reverseTrustAction(params: {
   userId: string

From 7b024bff786949f756541c07fad3e421ca5047f9 Mon Sep 17 00:00:00 2001
From: Producdevity <y.gherbi.dev@gmail.com>
Date: Thu, 28 May 2026 16:11:57 +0200
Subject: [PATCH 15/35] docs: clarify deployment and commit permissions in
 AGENTS.md

---
 AGENTS.md | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/AGENTS.md b/AGENTS.md
index e0c538a40..27a6093d4 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -15,7 +15,9 @@ This file is the source of working guidance for AI coding agents in this reposit
   - `pnpm prisma validate` validates the schema.
   - Database-backed Prisma commands must use the project scripts that wrap `scripts/db-cmd.sh` when available.
 - Do not run `pnpm dev`, `pnpm build`, `pnpm start`, `pnpm run deploy`, migrations, seeds, or data scripts unless the user explicitly asks.
-- Never deploy, commit, or push unless the user explicitly asks.
+- Never deploy, commit, or push unless the user explicitly asks for that exact
+  action. Requests to fix a PR, update a PR, address review comments, check CI,
+  or prepare changes do not imply permission to commit or push.
 - Use the current git user as commit author; never add Codex/AI authorship or AI-themed branch names.
 
 ## Domain Rules
@@ -60,6 +62,8 @@ This file is the source of working guidance for AI coding agents in this reposit
 - Use generated Prisma types where appropriate.
 - Do not add unused functions, exports, or speculative helpers.
 - Remove dead code when refactoring.
+- Do not remove or rewrite existing TODO comments unless the user explicitly
+  asks, or unless the TODO is directly made obsolete by the code change.
 - Prefer function declarations for top-level functions/components.
 - Component props interfaces should be named `Props`.
 - Do not destructure component props in function parameters; use `props.foo`.

From de43ea45a49a212b91f28b603ad1e21f6c9ba4ba Mon Sep 17 00:00:00 2001
From: Producdevity <y.gherbi.dev@gmail.com>
Date: Thu, 28 May 2026 16:25:24 +0200
Subject: [PATCH 16/35] refactor: remove reverseTrustAction function

---
 src/lib/trust/service.ts | 73 ----------------------------------------
 1 file changed, 73 deletions(-)

diff --git a/src/lib/trust/service.ts b/src/lib/trust/service.ts
index df8127689..7056f30de 100644
--- a/src/lib/trust/service.ts
+++ b/src/lib/trust/service.ts
@@ -92,79 +92,6 @@ export async function applyTrustAction(params: ApplyTrustActionParams): Promise<
   })
 }
 
-/**
- * Reverses the trust impact of a previously applied trust action.
- * Applies the negative of the original action's weight.
- */
-export async function reverseTrustAction(params: {
-  userId: string
-  originalAction: TrustAction
-  context?: TrustActionContext
-}): Promise<void> {
-  const { userId, originalAction, context = {} } = params
-
-  if (!TRUST_ACTIONS[originalAction]) {
-    throw new Error(`Invalid trust action: ${originalAction}`)
-  }
-
-  const originalWeight = TRUST_ACTIONS[originalAction].weight
-  if (originalWeight === 0) return
-
-  const reversalWeight = -originalWeight
-
-  await prisma.$transaction(async (tx) => {
-    const currentUser = await tx.user.findUnique({
-      where: { id: userId },
-      select: { trustScore: true },
-    })
-
-    const currentTrustLevel = currentUser ? getTrustLevel(currentUser.trustScore) : null
-    const newTrustScore = (currentUser?.trustScore ?? 0) + reversalWeight
-    const newTrustLevel = getTrustLevel(newTrustScore)
-
-    await tx.user.update({
-      where: { id: userId },
-      data: {
-        trustScore: newTrustScore,
-        lastActiveAt: new Date(),
-      },
-    })
-
-    await tx.trustActionLog.create({
-      data: {
-        userId,
-        action: TrustAction.VOTE_NULLIFICATION_REVERSAL,
-        weight: reversalWeight,
-        metadata: validateData(z.record(z.unknown()), {
-          ...context,
-          originalAction,
-          reversed: true,
-        }) as Prisma.InputJsonValue,
-      },
-    })
-
-    analytics.trust.trustScoreChanged({
-      userId,
-      oldScore: currentUser?.trustScore || 0,
-      newScore: newTrustScore,
-      action: TrustAction.VOTE_NULLIFICATION_REVERSAL,
-      weight: reversalWeight,
-    })
-
-    const previousLevel = currentTrustLevel?.name ?? null
-    const nextLevel = newTrustLevel.name ?? null
-
-    if (previousLevel !== nextLevel) {
-      analytics.trust.trustLevelChanged({
-        userId,
-        oldLevel: resolveTrustLevelName(currentTrustLevel),
-        newLevel: resolveTrustLevelName(newTrustLevel),
-        score: newTrustScore,
-      })
-    }
-  })
-}
-
 export async function canUserAutoApprove(
   userId: string,
   prismaClient: AutoApprovalPrismaClient = prisma,

From 65e1a190e4dbca836766bb16385807d57390e7b0 Mon Sep 17 00:00:00 2001
From: Producdevity <y.gherbi.dev@gmail.com>
Date: Thu, 28 May 2026 16:26:02 +0200
Subject: [PATCH 17/35] docs: filters and selects documentation for clarity

---
 AGENTS.md | 23 ++++++++---------------
 1 file changed, 8 insertions(+), 15 deletions(-)

diff --git a/AGENTS.md b/AGENTS.md
index 27a6093d4..68bd0e07d 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -85,21 +85,14 @@ This file is the source of working guidance for AI coding agents in this reposit
 - Never use `window.confirm()`. Use `useConfirmDialog` from `@/components/ui`.
 - Keep admin pages consistent: table controls, search/filtering, pagination, statistics, and bulk actions should follow existing admin patterns.
 
-## Filters
-
-- Controllers own filter behavior: interactions, analytics, collapsed badges, active summaries, and calls into presentational content.
-- Content components should only render fields and call handlers passed by controllers.
-- URL/state hooks own URL sync and local UI state; they must not emit per-filter analytics.
-- Filter analytics should be emitted once from controllers, using `filterAnalytics` and `selectedLabels`.
-- Call `onChange` before emitting analytics.
-- Use shared filter UI pieces: `FilterSidebarShell`, `CollapsedBadges`, `ActiveFiltersSummary`, and `MobileFilterSheet`.
-- Use shared option mappers from `src/utils/options.ts`.
-
-## Async Multi-Selects
-
-- Use `src/components/ui/form/async-multi-select/AsyncMultiSelect.tsx` as the base.
-- Entity wrappers such as CPU, GPU, Device, and SoC selects should stay thin: call TRPC, map to `Option[]`, manage pagination state, and pass data to the base component.
-- Selected chips must persist by deriving them from `options` plus `selectedByIds`.
+## Filters And Selects
+
+- Filter controllers own behavior and analytics: interaction handlers, collapsed badges, active summaries, and calls into presentational content.
+- Filter content components should stay presentational. URL/state hooks own URL sync and local UI state, and must not emit per-filter analytics.
+- Emit filter analytics once from controllers after calling `onChange`, using `filterAnalytics` and `selectedLabels`.
+- Reuse shared filter primitives (`FilterSidebarShell`, `CollapsedBadges`, `ActiveFiltersSummary`, `MobileFilterSheet`) and option mappers from `src/utils/options.ts`.
+- Async entity filters must wrap `src/components/ui/form/async-multi-select/AsyncMultiSelect.tsx`. CPU, GPU, Device, and SoC wrappers should only query, map options, manage pagination, and pass data to the base component.
+- Selected async chips must derive from `options` plus `selectedByIds` so URL-loaded selections survive when their option is not on the current page.
 
 ## Security
 

From f6f058edbab463cb0dda878d502ff27d9940fa9b Mon Sep 17 00:00:00 2001
From: Producdevity <y.gherbi.dev@gmail.com>
Date: Thu, 28 May 2026 17:53:31 +0200
Subject: [PATCH 18/35] fix: include index in Autocomplete option IDs to ensure
 uniqueness

---
 src/components/ui/form/Autocomplete.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/ui/form/Autocomplete.tsx b/src/components/ui/form/Autocomplete.tsx
index 7fb0c98da..6dc7c1659 100644
--- a/src/components/ui/form/Autocomplete.tsx
+++ b/src/components/ui/form/Autocomplete.tsx
@@ -587,7 +587,7 @@ export function Autocomplete<T extends AutocompleteOptionBase>({
             !showMinCharsMessage &&
             suggestions.map((item, idx) => {
               const itemValue = optionToValue(item)
-              const safeId = `option-${String(itemValue).replace(/[^A-Za-z0-9_-]/g, '_')}`
+              const safeId = `option-${idx}-${String(itemValue).replace(/[^A-Za-z0-9_-]/g, '_')}`
               const isHighlighted = idx === highlightedIndex
               return (
                 <li

From d470445a281a7bc40315c7b6b730da09f19cc262 Mon Sep 17 00:00:00 2001
From: Producdevity <y.gherbi.dev@gmail.com>
Date: Fri, 29 May 2026 09:58:01 +0200
Subject: [PATCH 19/35] fix: Turnstile configuration check to require both keys

---
 .../human-verification/server/providers/turnstile.test.ts | 8 ++++++++
 .../human-verification/server/providers/turnstile.ts      | 4 +++-
 2 files changed, 11 insertions(+), 1 deletion(-)

diff --git a/src/features/human-verification/server/providers/turnstile.test.ts b/src/features/human-verification/server/providers/turnstile.test.ts
index 672b127d2..a26e2cc36 100644
--- a/src/features/human-verification/server/providers/turnstile.test.ts
+++ b/src/features/human-verification/server/providers/turnstile.test.ts
@@ -12,6 +12,14 @@ afterEach(() => {
 describe('turnstile provider', () => {
   it('reports whether Turnstile is configured', () => {
     vi.stubEnv('TURNSTILE_SECRET_KEY', '')
+    vi.stubEnv('NEXT_PUBLIC_TURNSTILE_SITE_KEY', '')
+    expect(isTurnstileConfigured()).toBe(false)
+
+    vi.stubEnv('TURNSTILE_SECRET_KEY', 'secret')
+    expect(isTurnstileConfigured()).toBe(false)
+
+    vi.stubEnv('TURNSTILE_SECRET_KEY', '')
+    vi.stubEnv('NEXT_PUBLIC_TURNSTILE_SITE_KEY', 'site-key')
     expect(isTurnstileConfigured()).toBe(false)
 
     vi.stubEnv('TURNSTILE_SECRET_KEY', 'secret')
diff --git a/src/features/human-verification/server/providers/turnstile.ts b/src/features/human-verification/server/providers/turnstile.ts
index a359d8a93..941fc647f 100644
--- a/src/features/human-verification/server/providers/turnstile.ts
+++ b/src/features/human-verification/server/providers/turnstile.ts
@@ -21,7 +21,9 @@ export interface TurnstileVerificationResult {
 }
 
 export function isTurnstileConfigured(): boolean {
-  return Boolean(process.env.TURNSTILE_SECRET_KEY?.trim())
+  return Boolean(
+    process.env.TURNSTILE_SECRET_KEY?.trim() && process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY?.trim(),
+  )
 }
 
 export function getRequestIp(headers?: Headers): string | undefined {

From 5efc41d5e7aa1e76273023c66d4d28083ff1675f Mon Sep 17 00:00:00 2001
From: Producdevity <y.gherbi.dev@gmail.com>
Date: Fri, 29 May 2026 09:59:06 +0200
Subject: [PATCH 20/35] fix: prevent impossible challenge when human
 verification is not configured

---
 src/server/utils/spam-check.test.ts | 29 +++++++++++++++++++++++++++++
 src/server/utils/spam-check.ts      |  4 ++--
 2 files changed, 31 insertions(+), 2 deletions(-)

diff --git a/src/server/utils/spam-check.test.ts b/src/server/utils/spam-check.test.ts
index 03bebe905..f57457ad6 100644
--- a/src/server/utils/spam-check.test.ts
+++ b/src/server/utils/spam-check.test.ts
@@ -292,4 +292,33 @@ describe('checkSpamContent', () => {
       )
     }
   })
+
+  it('does not issue an impossible challenge when human verification is not configured', async () => {
+    vi.spyOn(SpamDetectionService.prototype, 'detectSpam').mockResolvedValue({
+      isSpam: true,
+      confidence: 0.85,
+      method: 'pattern_matching',
+      reason: 'Spam pattern matched',
+    })
+    vi.mocked(isTurnstileConfigured).mockReturnValue(false)
+
+    try {
+      await checkSpamContent({
+        prisma: mockPrisma,
+        userId: USER_ID,
+        content: 'Flagged content',
+        entityType: 'comment',
+        challengeMode: 'challenge',
+      })
+      throw new Error('Expected checkSpamContent to throw')
+    } catch (error) {
+      expect(error).toBeInstanceOf(TRPCError)
+      expect((error as TRPCError).code).toBe('INTERNAL_SERVER_ERROR')
+      expect((error as TRPCError).cause).toEqual(
+        expect.objectContaining({
+          code: HUMAN_VERIFICATION_ERROR_CODES.UNAVAILABLE,
+        }),
+      )
+    }
+  })
 })
diff --git a/src/server/utils/spam-check.ts b/src/server/utils/spam-check.ts
index f417a7331..ab2254c97 100644
--- a/src/server/utils/spam-check.ts
+++ b/src/server/utils/spam-check.ts
@@ -63,14 +63,14 @@ async function enforceHumanVerification(
   token: string | null | undefined,
   headers: Headers | undefined,
 ): Promise<void> {
+  if (!isTurnstileConfigured()) AppError.humanVerificationUnavailable()
+
   if (!token) AppError.humanVerificationRequired()
 
   if (token.length > HUMAN_VERIFICATION_TOKEN_MAX_LENGTH) {
     AppError.humanVerificationFailed()
   }
 
-  if (!isTurnstileConfigured()) AppError.humanVerificationUnavailable()
-
   const result = await verifyTurnstileToken({
     token,
     remoteIp: getRequestIp(headers),

From 9f664cc620399d1887649a2ad4354b849e9b3ebc Mon Sep 17 00:00:00 2001
From: Producdevity <y.gherbi.dev@gmail.com>
Date: Fri, 29 May 2026 10:01:12 +0200
Subject: [PATCH 21/35] fix: disable retry for mutations in TRPC query
 configuration

---
 src/lib/api.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/lib/api.tsx b/src/lib/api.tsx
index 6e337b3d8..385da0237 100644
--- a/src/lib/api.tsx
+++ b/src/lib/api.tsx
@@ -24,7 +24,7 @@ export function TRPCProvider(props: PropsWithChildren) {
             retry: shouldRetryTRPCQuery,
             retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
           },
-          mutations: { retry: 1 },
+          mutations: { retry: false },
         },
       }),
   )

From 561952ac93e855f7010d0aefc7c04cc0506bdad8 Mon Sep 17 00:00:00 2001
From: Producdevity <y.gherbi.dev@gmail.com>
Date: Fri, 29 May 2026 10:27:28 +0200
Subject: [PATCH 22/35] fix: refactor safe ID generation for Autocomplete
 options

---
 src/components/ui/form/Autocomplete.tsx | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/src/components/ui/form/Autocomplete.tsx b/src/components/ui/form/Autocomplete.tsx
index 6dc7c1659..82a319124 100644
--- a/src/components/ui/form/Autocomplete.tsx
+++ b/src/components/ui/form/Autocomplete.tsx
@@ -490,6 +490,9 @@ export function Autocomplete<T extends AutocompleteOptionBase>({
   const dropdownVisible =
     isOpen && (suggestions.length > 0 || showNoResults || showMinCharsMessage || isLoading)
 
+  const createSafeId = (itemValue: string, idx: number) =>
+    `option-${idx}-${String(itemValue).replace(/[^A-Za-z0-9_-]/g, '_')}`
+
   return (
     <div className={cn('relative', className)}>
       {label && (
@@ -531,10 +534,7 @@ export function Autocomplete<T extends AutocompleteOptionBase>({
           aria-controls={isOpen ? 'autocomplete-list' : undefined}
           aria-activedescendant={
             highlightedIndex >= 0 && suggestions[highlightedIndex]
-              ? `option-${String(optionToValue(suggestions[highlightedIndex])).replace(
-                  /[^A-Za-z0-9_-]/g,
-                  '_',
-                )}`
+              ? createSafeId(optionToValue(suggestions[highlightedIndex]), highlightedIndex)
               : undefined
           }
         />
@@ -587,7 +587,7 @@ export function Autocomplete<T extends AutocompleteOptionBase>({
             !showMinCharsMessage &&
             suggestions.map((item, idx) => {
               const itemValue = optionToValue(item)
-              const safeId = `option-${idx}-${String(itemValue).replace(/[^A-Za-z0-9_-]/g, '_')}`
+              const safeId = createSafeId(itemValue, idx)
               const isHighlighted = idx === highlightedIndex
               return (
                 <li

From 8326ddafba0484a2be51d5ecb0232617c708009f Mon Sep 17 00:00:00 2001
From: Producdevity <y.gherbi.dev@gmail.com>
Date: Fri, 29 May 2026 11:35:26 +0200
Subject: [PATCH 23/35] test: add structured error handling tests and improve
 error serialization consistency

---
 docs/ERROR_HANDLING.md                        | 20 ++++++
 .../search/utils/gameCreationErrors.test.ts   | 53 ++++++++++++++
 .../new/search/utils/gameCreationErrors.ts    | 19 +++--
 .../utils/getHumanVerificationRequest.test.ts | 41 +++++++----
 .../utils/getHumanVerificationRequest.ts      | 27 +++----
 src/lib/app-error-cause.test.ts               | 70 +++++++++++++++++++
 src/lib/app-error-cause.ts                    |  3 +-
 src/lib/trpc-client-errors.ts                 |  5 ++
 8 files changed, 199 insertions(+), 39 deletions(-)
 create mode 100644 src/app/games/new/search/utils/gameCreationErrors.test.ts
 create mode 100644 src/lib/app-error-cause.test.ts

diff --git a/docs/ERROR_HANDLING.md b/docs/ERROR_HANDLING.md
index cbb89b80a..3f1f30771 100644
--- a/docs/ERROR_HANDLING.md
+++ b/docs/ERROR_HANDLING.md
@@ -329,3 +329,23 @@ All error methods throw `TRPCError` instances with the appropriate HTTP status c
 - `INTERNAL_SERVER_ERROR` → 500
 
 The errors are automatically handled by TRPC and sent to the client with proper HTTP status codes and error messages.
+
+## Structured Error Data on the Client
+
+Pass a plain object as the error `cause` to send machine-readable data alongside the message, and read it on the client with `getAppErrorData` (`@/lib/trpc-client-errors`):
+
+```typescript
+// Server
+AppError.conflict('Game already exists', {
+  code: APP_ERROR_CODES.GAME_ALREADY_EXISTS,
+  existingGameId,
+})
+
+// Client
+const appError = getAppErrorData(error)
+if (appError?.code === APP_ERROR_CODES.GAME_ALREADY_EXISTS) {
+  // ...use appError.existingGameId
+}
+```
+
+`cause` fields must be plain JSON values (no nested `Error` instances).
diff --git a/src/app/games/new/search/utils/gameCreationErrors.test.ts b/src/app/games/new/search/utils/gameCreationErrors.test.ts
new file mode 100644
index 000000000..ec7bffa41
--- /dev/null
+++ b/src/app/games/new/search/utils/gameCreationErrors.test.ts
@@ -0,0 +1,53 @@
+import { TRPCClientError } from '@trpc/client'
+import { describe, expect, it, vi } from 'vitest'
+import { APP_ERROR_CODES } from '@/lib/errors'
+import { handleGameCreationError } from './gameCreationErrors'
+
+vi.mock('sonner', () => ({
+  toast: { error: vi.fn(), info: vi.fn() },
+}))
+
+function trpcClientError(appError: unknown): unknown {
+  return TRPCClientError.from({
+    error: { code: -32600, message: 'game error', data: { appError } },
+  })
+}
+
+describe('handleGameCreationError', () => {
+  it('detects a duplicate game with an existing id', () => {
+    const error = trpcClientError({
+      code: APP_ERROR_CODES.GAME_ALREADY_EXISTS,
+      existingGameId: 'game-123',
+    })
+
+    expect(handleGameCreationError(error)).toEqual({
+      type: 'duplicate',
+      existingGameId: 'game-123',
+    })
+  })
+
+  it('detects a duplicate game without an existing id', () => {
+    const error = trpcClientError({ code: APP_ERROR_CODES.GAME_ALREADY_EXISTS })
+
+    expect(handleGameCreationError(error)).toEqual({ type: 'duplicateNoId' })
+  })
+
+  it('detects a submission-limit error and surfaces reportsNeeded', () => {
+    const error = trpcClientError({
+      code: APP_ERROR_CODES.GAME_SUBMISSION_LIMIT_EXCEEDED,
+      reportsNeeded: 3,
+    })
+
+    expect(handleGameCreationError(error)).toEqual({
+      type: 'submissionLimit',
+      reportsNeeded: 3,
+    })
+  })
+
+  it('falls back to a generic result for unrecognized errors', () => {
+    expect(handleGameCreationError(new Error('boom'))).toEqual({ type: 'generic' })
+    expect(handleGameCreationError(trpcClientError({ code: 'SOMETHING_ELSE' }))).toEqual({
+      type: 'generic',
+    })
+  })
+})
diff --git a/src/app/games/new/search/utils/gameCreationErrors.ts b/src/app/games/new/search/utils/gameCreationErrors.ts
index 44ce3c360..c681158cf 100644
--- a/src/app/games/new/search/utils/gameCreationErrors.ts
+++ b/src/app/games/new/search/utils/gameCreationErrors.ts
@@ -1,5 +1,6 @@
 import { toast } from 'sonner'
 import { APP_ERROR_CODES, type AppErrorCode } from '@/lib/errors'
+import { getAppErrorData } from '@/lib/trpc-client-errors'
 import getErrorMessage from '@/utils/getErrorMessage'
 
 export type GameCreationErrorResult =
@@ -14,13 +15,19 @@ interface ErrorCause {
   reportsNeeded?: number
 }
 
-/**
- * Extracts the error cause from a tRPC error.
- * tRPC wraps errors, so we need to check both direct and nested cause.
- */
+function isAppErrorCode(value: unknown): value is AppErrorCode {
+  return typeof value === 'string' && Object.values(APP_ERROR_CODES).some((code) => code === value)
+}
+
 function getErrorCause(error: unknown): ErrorCause | undefined {
-  const err = error as { cause?: ErrorCause; data?: { cause?: ErrorCause } }
-  return err?.cause ?? err?.data?.cause
+  const payload = getAppErrorData(error)
+  if (!payload) return undefined
+
+  return {
+    code: isAppErrorCode(payload.code) ? payload.code : undefined,
+    existingGameId: typeof payload.existingGameId === 'string' ? payload.existingGameId : undefined,
+    reportsNeeded: typeof payload.reportsNeeded === 'number' ? payload.reportsNeeded : undefined,
+  }
 }
 
 /**
diff --git a/src/features/human-verification/client/utils/getHumanVerificationRequest.test.ts b/src/features/human-verification/client/utils/getHumanVerificationRequest.test.ts
index 484022c5e..74864726c 100644
--- a/src/features/human-verification/client/utils/getHumanVerificationRequest.test.ts
+++ b/src/features/human-verification/client/utils/getHumanVerificationRequest.test.ts
@@ -1,27 +1,42 @@
+import { TRPCClientError } from '@trpc/client'
 import { describe, expect, it } from 'vitest'
 import { getHumanVerificationRequest } from './getHumanVerificationRequest'
 import { HUMAN_VERIFICATION_ACTION, HUMAN_VERIFICATION_ERROR_CODES } from '../../shared/constants'
 
+function trpcClientError(data: unknown): unknown {
+  return TRPCClientError.from({ error: { code: -32600, message: 'verification', data } })
+}
+
 describe('getHumanVerificationRequest', () => {
-  it('extracts the challenge request from tRPC app error data', () => {
-    expect(
-      getHumanVerificationRequest({
-        data: {
-          appError: {
-            code: HUMAN_VERIFICATION_ERROR_CODES.REQUIRED,
-            provider: 'turnstile',
-            action: HUMAN_VERIFICATION_ACTION,
-          },
-        },
-      }),
-    ).toEqual({
+  it('extracts the challenge request from a tRPC client error carrying the verification app error', () => {
+    const error = trpcClientError({
+      appError: {
+        code: HUMAN_VERIFICATION_ERROR_CODES.REQUIRED,
+        provider: 'turnstile',
+        action: HUMAN_VERIFICATION_ACTION,
+      },
+    })
+
+    expect(getHumanVerificationRequest(error)).toEqual({
       code: HUMAN_VERIFICATION_ERROR_CODES.REQUIRED,
       provider: 'turnstile',
       action: HUMAN_VERIFICATION_ACTION,
     })
   })
 
-  it('ignores non-challenge errors', () => {
+  it('ignores a tRPC client error carrying a different app error', () => {
+    const error = trpcClientError({ appError: { code: 'GAME_ALREADY_EXISTS' } })
+
+    expect(getHumanVerificationRequest(error)).toBeNull()
+  })
+
+  it('ignores tRPC client errors with no app-error payload', () => {
+    expect(getHumanVerificationRequest(trpcClientError({ appError: null }))).toBeNull()
+    expect(getHumanVerificationRequest(trpcClientError({}))).toBeNull()
+  })
+
+  it('ignores errors that are not tRPC client errors', () => {
     expect(getHumanVerificationRequest(new Error('nope'))).toBeNull()
+    expect(getHumanVerificationRequest(null)).toBeNull()
   })
 })
diff --git a/src/features/human-verification/client/utils/getHumanVerificationRequest.ts b/src/features/human-verification/client/utils/getHumanVerificationRequest.ts
index 43248d0be..bc6cf6975 100644
--- a/src/features/human-verification/client/utils/getHumanVerificationRequest.ts
+++ b/src/features/human-verification/client/utils/getHumanVerificationRequest.ts
@@ -1,34 +1,23 @@
+import { getAppErrorData } from '@/lib/trpc-client-errors'
 import {
   HUMAN_VERIFICATION_ACTION,
   HUMAN_VERIFICATION_ERROR_CODES,
   type HumanVerificationRequiredData,
 } from '../../shared/constants'
 
-function getErrorPayload(error: unknown): unknown {
-  if (!error || typeof error !== 'object') return null
-
-  const candidate = error as {
-    cause?: unknown
-    data?: { appError?: unknown; cause?: unknown }
-  }
-
-  return candidate.data?.appError ?? candidate.data?.cause ?? candidate.cause ?? null
-}
-
 export function getHumanVerificationRequest(error: unknown): HumanVerificationRequiredData | null {
-  const payload = getErrorPayload(error)
-  if (!payload || typeof payload !== 'object') return null
+  const payload = getAppErrorData(error)
+  if (!payload) return null
 
-  const data = payload as Partial<HumanVerificationRequiredData>
   if (
-    data.code === HUMAN_VERIFICATION_ERROR_CODES.REQUIRED &&
-    data.provider === 'turnstile' &&
-    data.action === HUMAN_VERIFICATION_ACTION
+    payload.code === HUMAN_VERIFICATION_ERROR_CODES.REQUIRED &&
+    payload.provider === 'turnstile' &&
+    payload.action === HUMAN_VERIFICATION_ACTION
   ) {
     return {
       code: HUMAN_VERIFICATION_ERROR_CODES.REQUIRED,
-      provider: data.provider,
-      action: data.action,
+      provider: 'turnstile',
+      action: HUMAN_VERIFICATION_ACTION,
     }
   }
 
diff --git a/src/lib/app-error-cause.test.ts b/src/lib/app-error-cause.test.ts
new file mode 100644
index 000000000..e5e15b513
--- /dev/null
+++ b/src/lib/app-error-cause.test.ts
@@ -0,0 +1,70 @@
+import { TRPCError } from '@trpc/server'
+import superjson from 'superjson'
+import { describe, expect, it } from 'vitest'
+import {
+  HUMAN_VERIFICATION_ACTION,
+  HUMAN_VERIFICATION_ERROR_CODES,
+} from '@/features/human-verification/shared/constants'
+import { APP_ERROR_CODES, AppError, ResourceError } from '@/lib/errors'
+import { getSerializableAppError } from './app-error-cause'
+
+function captureTrpcError(throwing: () => void): TRPCError {
+  try {
+    throwing()
+  } catch (error) {
+    if (error instanceof TRPCError) return error
+    throw error
+  }
+  throw new Error('Expected the callback to throw a TRPCError')
+}
+
+function roundTripAppError(error: TRPCError): unknown {
+  const errorData = { appError: getSerializableAppError(error.cause) }
+  return superjson.deserialize<{ appError: unknown }>(superjson.serialize(errorData)).appError
+}
+
+describe('getSerializableAppError', () => {
+  it('returns a plain-object copy of the structured cause', () => {
+    const error = captureTrpcError(() => AppError.humanVerificationRequired())
+
+    expect(getSerializableAppError(error.cause)).toEqual({
+      code: HUMAN_VERIFICATION_ERROR_CODES.REQUIRED,
+      provider: 'turnstile',
+      action: HUMAN_VERIFICATION_ACTION,
+    })
+  })
+
+  it('does not return an Error instance, so superjson cannot strip the custom fields', () => {
+    const error = captureTrpcError(() => AppError.humanVerificationRequired())
+
+    expect(getSerializableAppError(error.cause)).not.toBeInstanceOf(Error)
+  })
+
+  it('preserves human-verification fields through the superjson round-trip the tRPC transformer applies', () => {
+    const error = captureTrpcError(() => AppError.humanVerificationRequired())
+
+    expect(roundTripAppError(error)).toEqual({
+      code: HUMAN_VERIFICATION_ERROR_CODES.REQUIRED,
+      provider: 'turnstile',
+      action: HUMAN_VERIFICATION_ACTION,
+    })
+  })
+
+  it('preserves game-error metadata through the superjson round-trip', () => {
+    const error = captureTrpcError(() =>
+      ResourceError.game.alreadyExists('Tetris', 'Game Boy', 'game-123'),
+    )
+
+    expect(roundTripAppError(error)).toMatchObject({
+      code: APP_ERROR_CODES.GAME_ALREADY_EXISTS,
+      existingGameId: 'game-123',
+    })
+  })
+
+  it('ignores causes that do not carry a structured app-error code', () => {
+    expect(getSerializableAppError(new Error('boom'))).toBeNull()
+    expect(getSerializableAppError(null)).toBeNull()
+    expect(getSerializableAppError('nope')).toBeNull()
+    expect(getSerializableAppError(42)).toBeNull()
+  })
+})
diff --git a/src/lib/app-error-cause.ts b/src/lib/app-error-cause.ts
index b6a9d68ea..b8cddb991 100644
--- a/src/lib/app-error-cause.ts
+++ b/src/lib/app-error-cause.ts
@@ -2,5 +2,6 @@ export function getSerializableAppError(cause: unknown): Record<string, unknown>
   if (!cause || typeof cause !== 'object') return null
 
   const payload = cause as Record<string, unknown>
-  return typeof payload.code === 'string' ? payload : null
+  // tRPC wraps `cause` in an Error and superjson strips custom props off Errors; copy to a plain object.
+  return typeof payload.code === 'string' ? { ...payload } : null
 }
diff --git a/src/lib/trpc-client-errors.ts b/src/lib/trpc-client-errors.ts
index 95ee52db3..9fd59feea 100644
--- a/src/lib/trpc-client-errors.ts
+++ b/src/lib/trpc-client-errors.ts
@@ -11,3 +11,8 @@ export function isTRPCNotFoundError(error: unknown): boolean {
 export function shouldRetryTRPCQuery(failureCount: number, error: Error): boolean {
   return !isTRPCNotFoundError(error) && failureCount < MAX_QUERY_RETRIES
 }
+
+export function getAppErrorData(error: unknown): Record<string, unknown> | null {
+  if (!isTRPCClientError<AppRouter>(error)) return null
+  return error.data?.appError ?? null
+}

From a3fa69c43b4203cb4034a4da286fad44d8f930eb Mon Sep 17 00:00:00 2001
From: Producdevity <y.gherbi.dev@gmail.com>
Date: Fri, 29 May 2026 11:39:05 +0200
Subject: [PATCH 24/35] docs: streamline error handling documentation and
 update usage examples

---
 docs/ERROR_HANDLING.md | 355 ++++-------------------------------------
 1 file changed, 33 insertions(+), 322 deletions(-)

diff --git a/docs/ERROR_HANDLING.md b/docs/ERROR_HANDLING.md
index 3f1f30771..db9e61dde 100644
--- a/docs/ERROR_HANDLING.md
+++ b/docs/ERROR_HANDLING.md
@@ -1,351 +1,62 @@
-# Error Handling System
+# Error Handling
 
-This document describes the centralized error handling system for the EmuReady application, which provides type-safe, consistent error throwing for TRPC procedures.
+Server procedures throw typed errors via helpers in `src/lib/errors.ts`. The tRPC error formatter serializes them, and the client reads them back — keeping codes and messages consistent and machine-readable.
 
-## Overview
+## Throwing errors (server)
 
-The error handling system replaces the previous pattern of manually throwing `TRPCError` instances throughout the codebase with a more maintainable, type-safe approach.
-
-### Before (❌ Not Recommended)
-
-```typescript
-// Old pattern - error-prone and inconsistent
-throw new TRPCError({
-  code: 'NOT_FOUND',
-  message: 'Device brand not found',
-})
-
-throw new TRPCError({
-  code: 'CONFLICT',
-  message: `Brand "${input.name}" already exists`,
-})
-```
-
-### After (✅ Recommended)
+Use `AppError`, `ResourceError`, and `ValidationError` from `src/lib/errors.ts` instead of a raw `TRPCError`. That file is the source of truth for every helper and for the `ERROR_CODES` / `APP_ERROR_CODES` constants; add new ones there.
 
 ```typescript
-// New pattern - type-safe and consistent
-import { ResourceError } from '@/lib/errors'
+import { AppError, ResourceError } from '@/lib/errors'
 
-ResourceError.deviceBrand.notFound()
-ResourceError.deviceBrand.alreadyExists(input.name)
+if (!user) ResourceError.user.notFound()
+if (existing) ResourceError.deviceBrand.alreadyExists(input.name)
+if (!allowed) AppError.forbidden()
 ```
 
-## Benefits
-
-1. **Type Safety**: Error codes are constants, not strings
-2. **Consistency**: Standardized error messages across the application
-3. **Maintainability**: Centralized error handling logic
-4. **Developer Experience**: Autocomplete and IntelliSense support
-5. **Testability**: Easy to test error scenarios
-
-## Core Classes
-
-### `AppError`
-
-General-purpose error methods for common scenarios.
-
-```typescript
-import { AppError } from '@/lib/errors'
-
-// Authentication & Authorization
-AppError.unauthorized()
-AppError.forbidden()
-AppError.insufficientRole('ADMIN')
-
-// Resource errors
-AppError.notFound('User')
-AppError.alreadyExists('User', 'email "test@example.com"')
-AppError.conflict('Custom conflict message')
-
-// Validation errors
-AppError.badRequest('Invalid input')
-AppError.invalidInput('email')
-AppError.missingRequiredField('name')
-
-// Business logic errors
-AppError.resourceInUse('user', 5)
-AppError.operationNotAllowed('delete')
-
-// System errors
-AppError.internalError()
-AppError.databaseError('user creation')
-
-// Custom errors
-AppError.custom('BAD_REQUEST', 'Custom message')
-```
-
-### `ResourceError`
-
-Resource-specific error methods with contextual messages.
-
-```typescript
-import { ResourceError } from '@/lib/errors'
-
-// Device Brands
-ResourceError.deviceBrand.notFound()
-ResourceError.deviceBrand.alreadyExists('Apple')
-ResourceError.deviceBrand.inUse(3)
-
-// Devices
-ResourceError.device.notFound()
-ResourceError.device.alreadyExists('iPhone 15')
-ResourceError.device.inUse(2)
+Each helper throws a `TRPCError` with the appropriate HTTP status, so routers stay free of manual error construction.
 
-// Systems
-ResourceError.system.notFound()
-ResourceError.system.alreadyExists('Nintendo Switch')
-ResourceError.system.hasGames(10)
-
-// Games
-ResourceError.game.notFound()
-ResourceError.game.inUse(5)
-
-// Emulators
-ResourceError.emulator.notFound()
-ResourceError.emulator.alreadyExists('Dolphin')
-ResourceError.emulator.inUse(8)
-
-// Listings
-ResourceError.listing.notFound()
-ResourceError.listing.alreadyExists()
-ResourceError.listing.notPending()
-
-// Custom Fields
-ResourceError.customField.notFound()
-ResourceError.customField.alreadyExists('driver_version')
-ResourceError.customField.invalidForEmulator('field-123', 'emulator-456')
-
-// Performance Scales
-ResourceError.performanceScale.notFound()
-ResourceError.performanceScale.labelExists('Excellent')
-ResourceError.performanceScale.rankExists(1)
-ResourceError.performanceScale.inUse(15)
-
-// Users
-ResourceError.user.notFound()
-ResourceError.user.emailExists()
-ResourceError.user.cannotDeleteSelf()
-ResourceError.user.cannotDemoteSelf()
-ResourceError.user.notInDatabase('user-123')
-
-// Comments
-ResourceError.comment.notFound()
-ResourceError.comment.parentNotFound()
-ResourceError.comment.alreadyDeleted()
-ResourceError.comment.cannotEditDeleted()
-```
-
-### `ValidationError`
-
-Specialized validation errors for custom fields and form validation.
-
-```typescript
-import { ValidationError } from '@/lib/errors'
-
-ValidationError.requiresOptions('SELECT')
-ValidationError.optionsNotAllowed('TEXT')
-ValidationError.emptyOptions('SELECT')
-ValidationError.invalidOptions('SELECT')
-```
-
-## Migration Guide
-
-### Step 1: Import the Error Classes
-
-```typescript
-// Replace TRPCError import
-- import { TRPCError } from '@trpc/server'
-
-// Add error handling imports
-+ import { AppError, ResourceError } from '@/lib/errors'
-```
-
-### Step 2: Replace Error Throwing Patterns
-
-#### Not Found Errors
-
-```typescript
-// Before
-- if (!user) {
--   throw new TRPCError({
--     code: 'NOT_FOUND',
--     message: 'User not found',
--   })
-- }
-
-// After
-+ if (!user) {
-+   ResourceError.user.notFound()
-+ }
-```
+## Structured error data
 
-#### Already Exists Errors
+To send machine-readable data (not just a message), attach a plain object as the `cause`. The error formatter (`src/server/api/trpc.ts`, mirrored in `src/server/api/mobileContext.ts`) exposes it to the client at `data.appError` via `getSerializableAppError` (`src/lib/app-error-cause.ts`).
 
 ```typescript
-// Before
-- if (existingUser) {
--   throw new TRPCError({
--     code: 'CONFLICT',
--     message: `User with email "${email}" already exists`,
--   })
-- }
-
-// After
-+ if (existingUser) {
-+   ResourceError.user.emailExists()
-+ }
-```
-
-#### Permission Errors
-
-```typescript
-// Before
-- if (!hasPermission(user.role, Role.ADMIN)) {
--   throw new TRPCError({
--     code: 'FORBIDDEN',
--     message: 'You need admin permissions',
--   })
-- }
-
-// After
-+ if (!hasPermission(user.role, Role.ADMIN)) {
-+   AppError.insufficientRole('ADMIN')
-+ }
-```
-
-#### Resource In Use Errors
-
-```typescript
-// Before
-- if (listingsCount > 0) {
--   throw new TRPCError({
--     code: 'BAD_REQUEST',
--     message: `Cannot delete device that is used in ${listingsCount} listings`,
--   })
-- }
-
-// After
-+ if (listingsCount > 0) {
-+   ResourceError.device.inUse(listingsCount)
-+ }
-```
-
-## Error Codes
-
-The system uses type-safe error codes defined as constants:
-
-```typescript
-export const ERROR_CODES = {
-  UNAUTHORIZED: 'UNAUTHORIZED',
-  FORBIDDEN: 'FORBIDDEN',
-  NOT_FOUND: 'NOT_FOUND',
-  CONFLICT: 'CONFLICT',
-  BAD_REQUEST: 'BAD_REQUEST',
-  INTERNAL_SERVER_ERROR: 'INTERNAL_SERVER_ERROR',
-} as const
-```
-
-## Adding New Error Types
-
-### Adding a New Resource Error
-
-```typescript
-// In src/lib/errors.ts
-export class ResourceError {
-  // ... existing resources ...
-
-  static newResource = {
-    notFound: () => AppError.notFound('New Resource'),
-    alreadyExists: (name: string) =>
-      AppError.alreadyExists('New Resource', `name "${name}"`),
-    inUse: (count: number) => AppError.resourceInUse('new resource', count),
-  }
-}
+AppError.conflict('Game already exists', {
+  code: APP_ERROR_CODES.GAME_ALREADY_EXISTS,
+  existingGameId,
+})
 ```
 
-### Adding a New General Error
-
-```typescript
-// In src/lib/errors.ts
-export class AppError {
-  // ... existing methods ...
-
-  static newErrorType(customParam?: string): never {
-    const message = customParam
-      ? `Custom error with ${customParam}`
-      : 'Default custom error message'
-
-    throw new TRPCError({
-      code: ERROR_CODES.BAD_REQUEST,
-      message,
-    })
-  }
-}
-```
+`cause` fields must be plain JSON values (no nested `Error` instances).
 
-## Testing Error Handling
+## Reading errors (client)
 
-The error handling system is fully testable:
+Most call sites only display the message:
 
 ```typescript
-import { describe, it, expect } from 'vitest'
-import { ResourceError } from '@/lib/errors'
+import getErrorMessage from '@/utils/getErrorMessage'
 
-describe('My Router', () => {
-  it('should throw not found error for missing user', () => {
-    expect(() => ResourceError.user.notFound()).toThrow('User not found')
-  })
-})
+toast.error(getErrorMessage(error))
 ```
 
-## Best Practices
-
-1. **Use Specific Resource Errors**: Prefer `ResourceError.user.notFound()` over `AppError.notFound('User')`
-2. **Provide Context**: Include relevant identifiers in error messages when available
-3. **Be Consistent**: Use the same error type for the same scenario across different routers
-4. **Test Error Scenarios**: Write tests for both success and error cases
-5. **Document Custom Errors**: If you add new error types, document them in this file
-
-## Error Message Consistency
-
-The system ensures consistent error messages across the application:
-
-- **Not Found**: `"{Resource} not found"`
-- **Already Exists**: `"{Resource} with {identifier} already exists"`
-- **In Use**: `"Cannot delete {resource} that is used in {count} records"`
-- **Unauthorized**: `"You must be logged in to perform this action"`
-- **Forbidden**: `"You do not have permission to perform this action"`
-
-## Integration with TRPC
-
-All error methods throw `TRPCError` instances with the appropriate HTTP status codes:
-
-- `UNAUTHORIZED` → 401
-- `FORBIDDEN` → 403
-- `NOT_FOUND` → 404
-- `CONFLICT` → 409
-- `BAD_REQUEST` → 400
-- `INTERNAL_SERVER_ERROR` → 500
-
-The errors are automatically handled by TRPC and sent to the client with proper HTTP status codes and error messages.
-
-## Structured Error Data on the Client
-
-Pass a plain object as the error `cause` to send machine-readable data alongside the message, and read it on the client with `getAppErrorData` (`@/lib/trpc-client-errors`):
+When the client must _act_ on a specific error rather than just notify, branch on the structured code with `getAppErrorData` (`src/lib/trpc-client-errors.ts`). See `handleGameCreationError` (`src/app/games/new/search/utils/gameCreationErrors.ts`), which deep-links to the existing game on a duplicate and shows a tailored CTA on the submission limit:
 
 ```typescript
-// Server
-AppError.conflict('Game already exists', {
-  code: APP_ERROR_CODES.GAME_ALREADY_EXISTS,
-  existingGameId,
-})
+import { getAppErrorData } from '@/lib/trpc-client-errors'
 
-// Client
 const appError = getAppErrorData(error)
 if (appError?.code === APP_ERROR_CODES.GAME_ALREADY_EXISTS) {
-  // ...use appError.existingGameId
+  router.push(`/listings/new?gameId=${appError.existingGameId}`)
 }
 ```
 
-`cause` fields must be plain JSON values (no nested `Error` instances).
+## Key files
+
+| File | Responsibility |
+| --- | --- |
+| `src/lib/errors.ts` | `AppError` / `ResourceError` / `ValidationError` helpers; `ERROR_CODES`, `APP_ERROR_CODES` |
+| `src/lib/app-error-cause.ts` | Server: turn a `TRPCError` `cause` into a serializable `appError` payload |
+| `src/lib/trpc-client-errors.ts` | Client: `getAppErrorData`, `isTRPCNotFoundError`, query retry policy |
+| `src/server/api/trpc.ts` · `mobileContext.ts` | `errorFormatter` attaching `appError` / `zodError` to the client error shape |
+
+Errors are unit-testable by asserting the thrown `TRPCError` (see the sibling `*.test.ts` files).

From d6378d2f765b2cdac2d2e7f8c31768f1b41a498a Mon Sep 17 00:00:00 2001
From: Producdevity <y.gherbi.dev@gmail.com>
Date: Sat, 30 May 2026 14:07:31 +0200
Subject: [PATCH 25/35] fix: handle Turnstile script readiness and widget
 initialization more robustly

---
 .../HumanVerificationProvider.test.tsx        | 73 ++++++++++++++++++-
 .../components/HumanVerificationProvider.tsx  | 48 ++++++------
 2 files changed, 97 insertions(+), 24 deletions(-)

diff --git a/src/features/human-verification/client/components/HumanVerificationProvider.test.tsx b/src/features/human-verification/client/components/HumanVerificationProvider.test.tsx
index cc74de7de..c34312115 100644
--- a/src/features/human-verification/client/components/HumanVerificationProvider.test.tsx
+++ b/src/features/human-verification/client/components/HumanVerificationProvider.test.tsx
@@ -1,4 +1,4 @@
-import { fireEvent, render, screen } from '@testing-library/react'
+import { act, fireEvent, render, screen } from '@testing-library/react'
 import { afterEach, describe, expect, it, vi } from 'vitest'
 import { HumanVerificationProvider, useHumanVerification } from './HumanVerificationProvider'
 import { HUMAN_VERIFICATION_ACTION } from '../../shared/constants'
@@ -34,8 +34,30 @@ function CaptureButton(props: CaptureButtonProps) {
   )
 }
 
+function createTurnstileStub() {
+  const renderWidget = vi.fn<(container: HTMLElement, options: Record<string, unknown>) => string>(
+    () => 'widget-id',
+  )
+  const ready = vi.fn(() => {
+    throw new Error('turnstile.ready() must not be called when api.js is loaded async/defer')
+  })
+
+  return {
+    renderWidget,
+    ready,
+    api: {
+      ready,
+      render: renderWidget,
+      remove: vi.fn(),
+      reset: vi.fn(),
+    },
+  }
+}
+
 describe('HumanVerificationProvider', () => {
   afterEach(() => {
+    delete window.turnstile
+    delete window.onloadTurnstileCallback
     vi.unstubAllEnvs()
   })
 
@@ -55,4 +77,53 @@ describe('HumanVerificationProvider', () => {
     expect(requests).toHaveLength(2)
     await expect(requests[1]).rejects.toThrow(/already in progress/i)
   })
+
+  it('becomes ready via onloadTurnstileCallback, renders the widget directly, then resolves with the token', async () => {
+    const stub = createTurnstileStub()
+    const requests: Promise<string>[] = []
+
+    render(
+      <HumanVerificationProvider>
+        <CaptureButton onRequest={(promise) => requests.push(promise)} />
+      </HumanVerificationProvider>,
+    )
+
+    expect(typeof window.onloadTurnstileCallback).toBe('function')
+
+    window.turnstile = stub.api
+    act(() => window.onloadTurnstileCallback?.())
+
+    fireEvent.click(screen.getByRole('button', { name: /request verification/i }))
+
+    expect(stub.renderWidget).toHaveBeenCalledTimes(1)
+    expect(stub.ready).not.toHaveBeenCalled()
+
+    const options = stub.renderWidget.mock.calls[0][1]
+    expect(options.sitekey).toBe('site-key')
+    expect(options.action).toBe(HUMAN_VERIFICATION_ACTION)
+
+    const onToken = options.callback
+    if (typeof onToken !== 'function') throw new Error('callback option is not a function')
+    act(() => onToken('verification-token'))
+
+    await expect(requests[0]).resolves.toBe('verification-token')
+  })
+
+  it('treats an already-loaded Turnstile script as ready without registering an onload callback', () => {
+    const stub = createTurnstileStub()
+    window.turnstile = stub.api
+
+    render(
+      <HumanVerificationProvider>
+        <CaptureButton onRequest={() => {}} />
+      </HumanVerificationProvider>,
+    )
+
+    expect(window.onloadTurnstileCallback).toBeUndefined()
+
+    fireEvent.click(screen.getByRole('button', { name: /request verification/i }))
+
+    expect(stub.renderWidget).toHaveBeenCalledTimes(1)
+    expect(stub.ready).not.toHaveBeenCalled()
+  })
 })
diff --git a/src/features/human-verification/client/components/HumanVerificationProvider.tsx b/src/features/human-verification/client/components/HumanVerificationProvider.tsx
index 6b8b3895c..7e09cebd3 100644
--- a/src/features/human-verification/client/components/HumanVerificationProvider.tsx
+++ b/src/features/human-verification/client/components/HumanVerificationProvider.tsx
@@ -34,6 +34,7 @@ interface TurnstileApi {
 declare global {
   interface Window {
     turnstile?: TurnstileApi
+    onloadTurnstileCallback?: () => void
   }
 }
 
@@ -54,8 +55,10 @@ interface PendingRequest {
 
 export function HumanVerificationProvider(props: PropsWithChildren) {
   const [pendingRequest, setPendingRequest] = useState<PendingRequest | null>(null)
-  const [scriptStatus, setScriptStatus] = useState<ScriptStatus>('idle')
-  const widgetContainerRef = useRef<HTMLDivElement | null>(null)
+  const [scriptStatus, setScriptStatus] = useState<ScriptStatus>(() =>
+    typeof window !== 'undefined' && window.turnstile ? 'ready' : 'idle',
+  )
+  const [widgetContainer, setWidgetContainer] = useState<HTMLDivElement | null>(null)
   const widgetIdRef = useRef<TurnstileWidgetId | null>(null)
   const pendingRequestRef = useRef<PendingRequest | null>(null)
   const turnstileSiteKey = env.TURNSTILE_SITE_KEY
@@ -84,30 +87,23 @@ export function HumanVerificationProvider(props: PropsWithChildren) {
   )
 
   const closeDialog = useCallback(() => {
-    if (widgetIdRef.current && window.turnstile) {
-      window.turnstile.remove(widgetIdRef.current)
-      widgetIdRef.current = null
-    }
     pendingRequestRef.current = null
     setPendingRequest(null)
   }, [])
 
   useEffect(() => {
-    if (
-      !pendingRequest ||
-      scriptStatus !== 'ready' ||
-      !widgetContainerRef.current ||
-      !window.turnstile
-    ) {
-      return
+    if (window.turnstile) return
+    window.onloadTurnstileCallback = () => setScriptStatus('ready')
+    return () => {
+      delete window.onloadTurnstileCallback
     }
+  }, [])
 
-    if (widgetIdRef.current) {
-      window.turnstile.remove(widgetIdRef.current)
-      widgetIdRef.current = null
-    }
+  useEffect(() => {
+    const turnstile = window.turnstile
+    if (!pendingRequest || scriptStatus !== 'ready' || !widgetContainer || !turnstile) return
 
-    const widgetId = window.turnstile.render(widgetContainerRef.current, {
+    const widgetId = turnstile.render(widgetContainer, {
       sitekey: turnstileSiteKey,
       action: pendingRequest.action,
       theme: 'auto',
@@ -127,7 +123,14 @@ export function HumanVerificationProvider(props: PropsWithChildren) {
     })
 
     widgetIdRef.current = widgetId ?? null
-  }, [closeDialog, pendingRequest, scriptStatus, turnstileSiteKey])
+
+    return () => {
+      if (widgetIdRef.current && window.turnstile) {
+        window.turnstile.remove(widgetIdRef.current)
+        widgetIdRef.current = null
+      }
+    }
+  }, [closeDialog, pendingRequest, scriptStatus, turnstileSiteKey, widgetContainer])
 
   const handleOpenChange = (open: boolean) => {
     if (open || !pendingRequest) return
@@ -139,9 +142,8 @@ export function HumanVerificationProvider(props: PropsWithChildren) {
     <HumanVerificationContext.Provider value={requestVerification}>
       {turnstileSiteKey && (
         <Script
-          src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit"
+          src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit&onload=onloadTurnstileCallback"
           strategy="afterInteractive"
-          onReady={() => setScriptStatus('ready')}
           onError={() => {
             setScriptStatus('failed')
             pendingRequestRef.current?.reject(new Error('Human verification failed to load.'))
@@ -151,7 +153,7 @@ export function HumanVerificationProvider(props: PropsWithChildren) {
       )}
       {props.children}
       <Dialog open={!!pendingRequest} onOpenChange={handleOpenChange}>
-        <DialogContent title="Human verification" className="sm:max-w-md">
+        <DialogContent className="sm:max-w-md">
           <DialogHeader>
             <DialogTitle>Human verification</DialogTitle>
             <DialogDescription>
@@ -162,7 +164,7 @@ export function HumanVerificationProvider(props: PropsWithChildren) {
             {scriptStatus !== 'ready' && (
               <p className="text-sm text-gray-500 dark:text-gray-400">Loading verification...</p>
             )}
-            <div ref={widgetContainerRef} />
+            <div ref={setWidgetContainer} />
           </div>
         </DialogContent>
       </Dialog>

From 458c917fa190ba4adb474dd15a2a8dcfe86f900b Mon Sep 17 00:00:00 2001
From: Producdevity <y.gherbi.dev@gmail.com>
Date: Sat, 30 May 2026 14:24:38 +0200
Subject: [PATCH 26/35] fix: remove unused Google Analytics script

---
 src/app/layout.tsx | 10 ----------
 1 file changed, 10 deletions(-)

diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index abbaa4ef6..6a4b15df3 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -6,7 +6,6 @@ import { Analytics } from '@vercel/analytics/next'
 import { SpeedInsights } from '@vercel/speed-insights/next'
 import { type Metadata, type Viewport } from 'next'
 import { Inter } from 'next/font/google'
-import Script from 'next/script'
 import { connection } from 'next/server'
 import { Suspense, type PropsWithChildren } from 'react'
 import { Toaster } from 'sonner'
@@ -37,15 +36,6 @@ export default function RootLayout(props: PropsWithChildren) {
   return (
     <html lang="en" suppressHydrationWarning>
       <body className={cn(inter.className, 'min-h-screen bg-background font-sans antialiased')}>
-        {env.ENABLE_ANALYTICS && env.GA_ID && (
-          <Script id="google-analytics-dataLayer" strategy="beforeInteractive">
-            {`
-                window.dataLayer = window.dataLayer || [];
-                function gtag(){dataLayer.push(arguments);}
-                gtag('js', new Date());
-              `}
-          </Script>
-        )}
         <ServiceWorkerRegistrar enabled={env.ENABLE_SW} />
         <ClerkBoundary>
           <Providers>

From f6aec3ef707948a8a685659bed4154279ffb7199 Mon Sep 17 00:00:00 2001
From: Producdevity <y.gherbi.dev@gmail.com>
Date: Mon, 1 Jun 2026 19:27:44 +0200
Subject: [PATCH 27/35] fix: update Turnstile keys in environment example files

---
 .env.docker.example | 4 ++--
 .env.example        | 4 ++--
 .env.test.example   | 4 ++--
 3 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/.env.docker.example b/.env.docker.example
index 79fea89fb..8cfd56ce2 100644
--- a/.env.docker.example
+++ b/.env.docker.example
@@ -38,8 +38,8 @@ TUNNEL_TOKEN="your_cloudflare_tunnel_token_here"
 # CLOUDFLARE TURNSTILE (Optional)
 # ===========================================
 # Human verification
-NEXT_PUBLIC_TURNSTILE_SITE_KEY="your_turnstile_site_key_here"
-TURNSTILE_SECRET_KEY="your_turnstile_secret_key_here"
+NEXT_PUBLIC_TURNSTILE_SITE_KEY=1x00000000000000000000AA
+TURNSTILE_SECRET_KEY=1x0000000000000000000000000000000AA
 
 # ===========================================
 # EMAIL SERVICES (Optional)
diff --git a/.env.example b/.env.example
index 7fe59b0d7..5e80fcf9f 100644
--- a/.env.example
+++ b/.env.example
@@ -17,8 +17,8 @@ NEXT_PUBLIC_IGDB_CLIENT_ID="IGDB-Client-ID"
 IGDB_CLIENT_KEY="IGDB-Client-Secret"
 
 # Cloudflare Turnstile
-NEXT_PUBLIC_TURNSTILE_SITE_KEY=0x4XXXXXXXXXXXXXXXXXXXXX
-TURNSTILE_SECRET_KEY=0x4XXXXXXXXXXXXXXXXXXXXXXXX-XXXXXXX
+NEXT_PUBLIC_TURNSTILE_SITE_KEY=1x00000000000000000000AA
+TURNSTILE_SECRET_KEY=1x0000000000000000000000000000000AA
 
 # Email Providers (For the future)
 EMAIL_ENABLED=false
diff --git a/.env.test.example b/.env.test.example
index 093105bf0..9dc7a9b4a 100644
--- a/.env.test.example
+++ b/.env.test.example
@@ -7,8 +7,8 @@ NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY="pk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
 CLERK_SECRET_KEY="sk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
 
 # Cloudflare Turnstile
-NEXT_PUBLIC_TURNSTILE_SITE_KEY=
-TURNSTILE_SECRET_KEY=
+NEXT_PUBLIC_TURNSTILE_SITE_KEY=1x00000000000000000000AA
+TURNSTILE_SECRET_KEY=1x0000000000000000000000000000000AA
 
 # Email (For the future)
 EMAIL_ENABLED=false

From ea06e2b8b095606997f1e7d4338baa402684fb9d Mon Sep 17 00:00:00 2001
From: Producdevity <y.gherbi.dev@gmail.com>
Date: Mon, 1 Jun 2026 19:38:15 +0200
Subject: [PATCH 28/35] fix: implement rate limits and duplicate detection for
 game submissions

---
 src/app/games/new/NewGamePage.tsx             |  17 ++-
 src/app/games/new/search/GameSearchPage.tsx   |  33 +++--
 .../games/new/search/v2/IGDBSearchPage.tsx    |  37 +++--
 src/schemas/game.ts                           |   2 +
 src/server/api/routers/games.ts               |  36 ++++-
 src/server/utils/spam-check.ts                |   5 +-
 src/server/utils/spamDetection.test.ts        |  86 ++++++++++-
 src/server/utils/spamDetection.ts             | 137 ++++++++++++++++--
 8 files changed, 294 insertions(+), 59 deletions(-)

diff --git a/src/app/games/new/NewGamePage.tsx b/src/app/games/new/NewGamePage.tsx
index 176b584ff..96c834032 100644
--- a/src/app/games/new/NewGamePage.tsx
+++ b/src/app/games/new/NewGamePage.tsx
@@ -13,6 +13,7 @@ import {
   Autocomplete,
   type AutocompleteOptionBase,
 } from '@/components/ui'
+import { useSubmitWithHumanVerification } from '@/features/human-verification/client'
 import { api } from '@/lib/api'
 import { cn } from '@/lib/utils'
 import getErrorMessage from '@/utils/getErrorMessage'
@@ -39,6 +40,7 @@ function AddGamePage() {
   const utils = api.useUtils()
   const systemsQuery = api.systems.get.useQuery()
   const createGame = api.games.create.useMutation()
+  const submitWithHumanVerification = useSubmitWithHumanVerification()
 
   const userQuery = api.users.me.useQuery(undefined, { enabled: !!user })
 
@@ -81,12 +83,15 @@ function AddGamePage() {
     if (!title || !systemId) return setError('Please fill in all required fields.')
 
     try {
-      const result = await createGame.mutateAsync({
-        title,
-        systemId,
-        imageUrl: imageUrl || undefined,
-        isErotic,
-      })
+      const result = await submitWithHumanVerification((humanVerificationToken) =>
+        createGame.mutateAsync({
+          title,
+          systemId,
+          imageUrl: imageUrl || undefined,
+          isErotic,
+          humanVerificationToken,
+        }),
+      )
 
       await utils.games.get.invalidate()
       await utils.games.checkExistingByTgdbIds.invalidate()
diff --git a/src/app/games/new/search/GameSearchPage.tsx b/src/app/games/new/search/GameSearchPage.tsx
index 45154c50d..367b06d39 100644
--- a/src/app/games/new/search/GameSearchPage.tsx
+++ b/src/app/games/new/search/GameSearchPage.tsx
@@ -11,7 +11,9 @@ import {
   type BaseGameResult,
 } from '@/components/game-search'
 import { LoadingSpinner } from '@/components/ui'
+import { useSubmitWithHumanVerification } from '@/features/human-verification/client'
 import { api } from '@/lib/api'
+import { logger } from '@/lib/logger'
 import getErrorMessage from '@/utils/getErrorMessage'
 import { hasRolePermission } from '@/utils/permissions'
 import { Role } from '@orm'
@@ -22,7 +24,6 @@ import { handleGameCreationError } from './utils/gameCreationErrors'
 import NotSignedInMessage from '../components/NotSignedInMessage'
 import type { TGDBGame, TGDBGamesByNameResponse } from '@/types/tgdb'
 
-// Extended TGDB game result that extends BaseGameResult
 interface TGDBGameResult extends BaseGameResult {
   id: string
   overview?: string
@@ -64,7 +65,6 @@ function TGDBSearchContent() {
   const searchParams = useSearchParams()
   const { user, isLoaded } = useUser()
 
-  // Get values directly from URL
   const urlQuery = searchParams.get('q') ?? ''
   const urlSystemId = searchParams.get('system') ?? ''
 
@@ -83,6 +83,7 @@ function TGDBSearchContent() {
     enabled: !!user,
   })
   const createGame = api.games.create.useMutation()
+  const submitWithHumanVerification = useSubmitWithHumanVerification()
   const systemsQuery = api.systems.get.useQuery()
 
   const { existingGames, updateSearchParams } = useGameSearch({
@@ -94,10 +95,8 @@ function TGDBSearchContent() {
     async (query: string, _platformId: number | null, systemId: string | null) => {
       setIsSearching(true)
 
-      // Update URL with search parameters
       updateSearchParams(query, systemId)
 
-      // Get system key from systemId
       const selectedSystem = systemId ? systemsQuery.data?.find((s) => s.id === systemId) : null
 
       try {
@@ -111,7 +110,6 @@ function TGDBSearchContent() {
         // Map TGDB games to our unified format
         const gameData = results.data?.games ?? []
         const mappedGames = gameData.map((game) => {
-          // Extract boxart URL from the search response
           const boxartUrl = extractBoxartUrl(game, results)
 
           const enrichedGame: TGDBGameWithBoxart = {
@@ -132,7 +130,7 @@ function TGDBSearchContent() {
           count: results.data?.count ?? 0,
         })
       } catch (error) {
-        console.error('Search error:', error)
+        logger.error('Search error:', error)
         toast.error(getErrorMessage(error, 'Failed to search games'))
       } finally {
         setIsSearching(false)
@@ -184,15 +182,18 @@ function TGDBSearchContent() {
 
       setIsSelecting(true)
       try {
-        const newGame = await createGame.mutateAsync({
-          title: game.game_title,
-          systemId,
-          imageUrl: game.boxart ?? null,
-          boxartUrl: game.boxart ?? null,
-          bannerUrl: null,
-          isErotic: false,
-          tgdbGameId: game.id,
-        })
+        const newGame = await submitWithHumanVerification((humanVerificationToken) =>
+          createGame.mutateAsync({
+            title: game.game_title,
+            systemId,
+            imageUrl: game.boxart ?? null,
+            boxartUrl: game.boxart ?? null,
+            bannerUrl: null,
+            isErotic: false,
+            tgdbGameId: game.id,
+            humanVerificationToken,
+          }),
+        )
 
         // Invalidate the existing games cache to ensure the new game is reflected
         await utils.games.checkExistingByNamesAndSystems.invalidate()
@@ -208,7 +209,7 @@ function TGDBSearchContent() {
         setIsSelecting(false)
       }
     },
-    [user, createGame, router, utils],
+    [user, submitWithHumanVerification, createGame, router, utils],
   )
 
   const isModeratorOrHigher = useMemo(() => {
diff --git a/src/app/games/new/search/v2/IGDBSearchPage.tsx b/src/app/games/new/search/v2/IGDBSearchPage.tsx
index 7b49f10bd..7f5336536 100644
--- a/src/app/games/new/search/v2/IGDBSearchPage.tsx
+++ b/src/app/games/new/search/v2/IGDBSearchPage.tsx
@@ -12,7 +12,9 @@ import {
   type BaseGameResult,
 } from '@/components/game-search'
 import { LoadingSpinner, useConfirmDialog } from '@/components/ui'
+import { useSubmitWithHumanVerification } from '@/features/human-verification/client'
 import { api } from '@/lib/api'
+import { logger } from '@/lib/logger'
 import getErrorMessage from '@/utils/getErrorMessage'
 import { hasRolePermission } from '@/utils/permissions'
 import { getIGDBPlatformId } from '@/utils/system-platform-mapping'
@@ -56,6 +58,7 @@ function IGDBSearchContent() {
     enabled: !!user,
   })
   const createGame = api.games.create.useMutation()
+  const submitWithHumanVerification = useSubmitWithHumanVerification()
   const systemsQuery = api.systems.get.useQuery()
   const confirm = useConfirmDialog()
 
@@ -66,7 +69,6 @@ function IGDBSearchContent() {
 
   const handleSearch = useCallback(
     async (query: string, platformId: number | null, systemId: string | null) => {
-      // Require system selection for game creation
       if (!systemId) {
         toast.warning('Please select a system before searching for games')
         return
@@ -82,7 +84,6 @@ function IGDBSearchContent() {
           platformId,
           limit: 20,
         })
-        // Map the results to ensure consistent types
         setSearchResults({
           games: results.games.map((game) => ({
             ...game,
@@ -91,7 +92,7 @@ function IGDBSearchContent() {
           count: results.count,
         })
       } catch (error) {
-        console.error('Search error:', error)
+        logger.error('Search error:', error)
         toast.error(getErrorMessage(error, 'Failed to search games'))
       } finally {
         setIsSearching(false)
@@ -144,15 +145,18 @@ function IGDBSearchContent() {
 
       setIsSelecting(true)
       try {
-        const newGame = await createGame.mutateAsync({
-          title: selectedGame.name,
-          systemId,
-          imageUrl: selectedGame.imageUrl ?? null,
-          boxartUrl: selectedGame.boxartUrl ?? null,
-          bannerUrl: selectedGame.bannerUrl ?? null,
-          isErotic: selectedGame.isErotic ?? false,
-          igdbGameId: Number(selectedGame.id),
-        })
+        const newGame = await submitWithHumanVerification((humanVerificationToken) =>
+          createGame.mutateAsync({
+            title: selectedGame.name,
+            systemId,
+            imageUrl: selectedGame.imageUrl ?? null,
+            boxartUrl: selectedGame.boxartUrl ?? null,
+            bannerUrl: selectedGame.bannerUrl ?? null,
+            isErotic: selectedGame.isErotic ?? false,
+            igdbGameId: Number(selectedGame.id),
+            humanVerificationToken,
+          }),
+        )
 
         await utils.games.checkExistingByNamesAndSystems.invalidate()
 
@@ -167,7 +171,14 @@ function IGDBSearchContent() {
         setIsSelecting(false)
       }
     },
-    [selectedGame, user, createGame, utils, showGameCreatedConfirmation],
+    [
+      selectedGame,
+      user,
+      submitWithHumanVerification,
+      createGame,
+      utils,
+      showGameCreatedConfirmation,
+    ],
   )
 
   const isModeratorOrHigher = hasRolePermission(userQuery?.data?.role, Role.MODERATOR)
diff --git a/src/schemas/game.ts b/src/schemas/game.ts
index 212c7d9a5..74e62059d 100644
--- a/src/schemas/game.ts
+++ b/src/schemas/game.ts
@@ -1,4 +1,5 @@
 import { z } from 'zod'
+import { HumanVerificationTokenSchema } from '@/features/human-verification/shared/schema'
 import { ApprovalStatus } from '@orm'
 
 export const GameSortField = z.enum([
@@ -72,6 +73,7 @@ export const CheckExistingByNamesAndSystemsSchema = z.object({
 export const CreateGameSchema = z.object({
   title: z.string().min(1),
   systemId: z.string().uuid(),
+  humanVerificationToken: HumanVerificationTokenSchema.optional(),
   imageUrl: z.string().nullable().optional(),
   boxartUrl: z.string().nullable().optional(),
   bannerUrl: z.string().nullable().optional(),
diff --git a/src/server/api/routers/games.ts b/src/server/api/routers/games.ts
index 2b16e4d7f..41617942a 100644
--- a/src/server/api/routers/games.ts
+++ b/src/server/api/routers/games.ts
@@ -50,6 +50,7 @@ import {
   sanitizeInput,
   validateNonEmptyArray,
 } from '@/server/utils/security-validation'
+import { checkSpamContent } from '@/server/utils/spam-check'
 import { getSubmissionBalance } from '@/server/utils/submission-balance'
 import {
   findTitleIdForGameName,
@@ -292,8 +293,19 @@ export const gamesRouter = createTRPCRouter({
     }),
 
   create: protectedProcedure.input(CreateGameSchema).mutation(async ({ ctx, input }) => {
+    const gameInput = {
+      title: input.title,
+      systemId: input.systemId,
+      imageUrl: input.imageUrl,
+      boxartUrl: input.boxartUrl,
+      bannerUrl: input.bannerUrl,
+      tgdbGameId: input.tgdbGameId,
+      igdbGameId: input.igdbGameId,
+      isErotic: input.isErotic,
+    }
+
     const system = await ctx.prisma.system.findUnique({
-      where: { id: input.systemId },
+      where: { id: gameInput.systemId },
     })
 
     if (!system) return ResourceError.system.notFound()
@@ -314,15 +326,27 @@ export const gamesRouter = createTRPCRouter({
 
     // Check if game with same title already exists for this system
     const existingGame = await ctx.prisma.game.findFirst({
-      where: { title: input.title, systemId: input.systemId },
+      where: { title: gameInput.title, systemId: gameInput.systemId },
     })
 
     if (existingGame) {
-      return ResourceError.game.alreadyExists(input.title, system.name, existingGame.id)
+      return ResourceError.game.alreadyExists(gameInput.title, system.name, existingGame.id)
     }
 
+    await checkSpamContent({
+      prisma: ctx.prisma,
+      userId: ctx.session.user.id,
+      content: gameInput.title,
+      entityType: 'game',
+      challengeMode: 'challenge',
+      enableRateLimiting: false,
+      enableDuplicateDetection: false,
+      humanVerificationToken: input.humanVerificationToken,
+      headers: ctx.headers,
+    })
+
     try {
-      const { igdbGameId, ...gameData } = input
+      const { igdbGameId, ...gameData } = gameInput
 
       // Build metadata object for external provider IDs
       const metadata = createGameMetadata({
@@ -357,7 +381,7 @@ export const gamesRouter = createTRPCRouter({
           invalidateListPages(),
           invalidateSitemap(),
           revalidateByTag('games'),
-          revalidateByTag(`system-${input.systemId}`),
+          revalidateByTag(`system-${gameInput.systemId}`),
         ]).catch((error) => {
           logger.error('[Games Router] Cache invalidation failed:', error)
         })
@@ -366,7 +390,7 @@ export const gamesRouter = createTRPCRouter({
       return result
     } catch (error) {
       if (isPrismaError(error, PRISMA_ERROR_CODES.UNIQUE_CONSTRAINT_VIOLATION)) {
-        return ResourceError.game.alreadyExists(input.title, system.name)
+        return ResourceError.game.alreadyExists(gameInput.title, system.name)
       }
       throw error
     }
diff --git a/src/server/utils/spam-check.ts b/src/server/utils/spam-check.ts
index ab2254c97..6ae5a1ad0 100644
--- a/src/server/utils/spam-check.ts
+++ b/src/server/utils/spam-check.ts
@@ -17,13 +17,16 @@ interface CheckSpamContentParams {
   content: string
   entityType: SpamEntityType
   challengeMode?: 'block' | 'challenge'
+  enableRateLimiting?: boolean
+  enableDuplicateDetection?: boolean
   humanVerificationToken?: string | null
   headers?: Headers
 }
 
 export async function checkSpamContent(params: CheckSpamContentParams): Promise<void> {
   const detector = new SpamDetectionService(params.prisma, {
-    enableRateLimiting: process.env.DISABLE_RATE_LIMIT !== 'true',
+    enableRateLimiting: params.enableRateLimiting ?? process.env.DISABLE_RATE_LIMIT !== 'true',
+    enableDuplicateDetection: params.enableDuplicateDetection ?? true,
   })
   const result = await detector.detectSpam({
     userId: params.userId,
diff --git a/src/server/utils/spamDetection.test.ts b/src/server/utils/spamDetection.test.ts
index 6b5a00803..96967c4aa 100644
--- a/src/server/utils/spamDetection.test.ts
+++ b/src/server/utils/spamDetection.test.ts
@@ -1,8 +1,11 @@
 import { describe, it, expect, beforeEach, vi } from 'vitest'
-import { type PrismaClient } from '@orm/client'
+import { Role, type PrismaClient } from '@orm/client'
 import { SpamDetectionService } from './spamDetection'
 
 const mockPrisma = {
+  user: {
+    findUnique: vi.fn(),
+  },
   listing: {
     count: vi.fn(),
     findMany: vi.fn(),
@@ -19,17 +22,28 @@ const mockPrisma = {
     count: vi.fn(),
     findMany: vi.fn(),
   },
+  game: {
+    count: vi.fn(),
+    findMany: vi.fn(),
+  },
 }
 
 function mockCleanRecentActivity() {
+  mockPrisma.user.findUnique.mockResolvedValue({
+    createdAt: new Date(),
+    role: Role.USER,
+    trustScore: 0,
+  })
   mockPrisma.listing.count.mockResolvedValue(0)
   mockPrisma.pcListing.count.mockResolvedValue(0)
   mockPrisma.comment.count.mockResolvedValue(0)
   mockPrisma.pcListingComment.count.mockResolvedValue(0)
+  mockPrisma.game.count.mockResolvedValue(0)
   mockPrisma.listing.findMany.mockResolvedValue([])
   mockPrisma.pcListing.findMany.mockResolvedValue([])
   mockPrisma.comment.findMany.mockResolvedValue([])
   mockPrisma.pcListingComment.findMany.mockResolvedValue([])
+  mockPrisma.game.findMany.mockResolvedValue([])
 }
 
 describe('SpamDetectionService', () => {
@@ -99,6 +113,57 @@ describe('SpamDetectionService', () => {
       expect(result.isSpam).toBe(true)
       expect(result.method).toBe('rate_limiting')
     })
+
+    it('raises the report limit for established accounts', async () => {
+      vi.mocked(mockPrisma.user.findUnique).mockResolvedValue({
+        createdAt: new Date(Date.now() - 31 * 24 * 60 * 60 * 1000),
+        role: Role.USER,
+        trustScore: 0,
+      })
+      vi.mocked(mockPrisma.listing.count).mockResolvedValue(5)
+
+      const result = await service.detectSpam({
+        userId: 'user-123',
+        content: 'Normal content',
+        entityType: 'listing',
+      })
+
+      expect(result.isSpam).toBe(false)
+    })
+
+    it('raises the report limit for trusted accounts while keeping a cap', async () => {
+      vi.mocked(mockPrisma.user.findUnique).mockResolvedValue({
+        createdAt: new Date(),
+        role: Role.USER,
+        trustScore: 250,
+      })
+      vi.mocked(mockPrisma.listing.count).mockResolvedValue(40)
+
+      const result = await service.detectSpam({
+        userId: 'user-123',
+        content: 'Normal content',
+        entityType: 'listing',
+      })
+
+      expect(result.isSpam).toBe(true)
+      expect(result.method).toBe('rate_limiting')
+    })
+
+    it('uses a dedicated rate limit bucket for games', async () => {
+      vi.mocked(mockPrisma.game.count).mockResolvedValue(10)
+
+      const result = await service.detectSpam({
+        userId: 'user-123',
+        content: 'Normal game title',
+        entityType: 'game',
+      })
+
+      expect(result.isSpam).toBe(true)
+      expect(result.method).toBe('rate_limiting')
+      expect(result.reason).toContain('Too many recent games')
+      expect(mockPrisma.comment.count).not.toHaveBeenCalled()
+      expect(mockPrisma.pcListingComment.count).not.toHaveBeenCalled()
+    })
   })
 
   describe('Duplicate Content Detection', () => {
@@ -173,6 +238,25 @@ describe('SpamDetectionService', () => {
       expect(result.method).toBe('duplicate_detection')
     })
 
+    it('uses game titles for duplicate game detection', async () => {
+      vi.mocked(mockPrisma.game.findMany).mockResolvedValue([
+        { title: 'Duplicate Game' },
+        { title: 'Duplicate Game' },
+        { title: 'Duplicate Game' },
+      ])
+
+      const result = await service.detectSpam({
+        userId: 'user-123',
+        content: 'Duplicate Game',
+        entityType: 'game',
+      })
+
+      expect(result.isSpam).toBe(true)
+      expect(result.method).toBe('duplicate_detection')
+      expect(mockPrisma.comment.findMany).not.toHaveBeenCalled()
+      expect(mockPrisma.pcListingComment.findMany).not.toHaveBeenCalled()
+    })
+
     it('should NOT detect spam for slightly different content', async () => {
       vi.mocked(mockPrisma.listing.findMany).mockResolvedValue([
         { notes: 'Game runs great on my device' },
diff --git a/src/server/utils/spamDetection.ts b/src/server/utils/spamDetection.ts
index 4e9938ecc..1a217ad45 100644
--- a/src/server/utils/spamDetection.ts
+++ b/src/server/utils/spamDetection.ts
@@ -1,4 +1,4 @@
-import { type PrismaClient } from '@orm/client'
+import { Role, type PrismaClient } from '@orm/client'
 
 export interface SpamDetectionResult {
   isSpam: boolean
@@ -7,15 +7,21 @@ export interface SpamDetectionResult {
   reason?: string
 }
 
-export type SpamEntityType = 'listing' | 'pcListing' | 'comment' | 'pcComment'
+export type SpamEntityType = 'listing' | 'pcListing' | 'comment' | 'pcComment' | 'game'
 
-type SpamEntityCategory = 'report' | 'comment'
+type SpamEntityCategory = 'report' | 'comment' | 'game'
 
 interface RateLimitRule {
   windowMinutes: number
   maxItems: number
 }
 
+interface UserRateLimitProfile {
+  createdAt: Date
+  role: Role
+  trustScore: number
+}
+
 export interface SpamDetectionConfig {
   enableRateLimiting?: boolean
   enableContentAnalysis?: boolean
@@ -52,8 +58,23 @@ const DEFAULT_RATE_LIMITS: Record<SpamEntityCategory, RateLimitRule> = {
     windowMinutes: 5,
     maxItems: 10,
   },
+  game: {
+    windowMinutes: 15,
+    maxItems: 10,
+  },
 }
 
+const REPORT_RATE_LIMIT_TIERS = {
+  ESTABLISHED_ACCOUNT_DAYS: 30,
+  CONTRIBUTOR_TRUST_SCORE: 100,
+  TRUSTED_TRUST_SCORE: 250,
+  ESTABLISHED_MAX_ITEMS: 20,
+  TRUSTED_MAX_ITEMS: 40,
+  STAFF_MAX_ITEMS: 100,
+} as const
+
+const STAFF_ROLES = new Set<Role>([Role.MODERATOR, Role.ADMIN, Role.SUPER_ADMIN])
+
 interface NormalizedSpamDetectionConfig {
   enableRateLimiting: boolean
   enableContentAnalysis: boolean
@@ -73,17 +94,40 @@ const DEFAULT_CONFIG: NormalizedSpamDetectionConfig = {
 }
 
 function getEntityCategory(entityType: SpamEntityType): SpamEntityCategory {
-  return entityType === 'listing' || entityType === 'pcListing' ? 'report' : 'comment'
+  if (entityType === 'listing' || entityType === 'pcListing') return 'report'
+  if (entityType === 'game') return 'game'
+  return 'comment'
 }
 
 function formatEntityCategory(category: SpamEntityCategory): string {
-  return category === 'report' ? 'reports' : 'comments'
+  if (category === 'report') return 'reports'
+  if (category === 'game') return 'games'
+  return 'comments'
 }
 
 function isNonEmptyString(value: string | null | undefined): value is string {
   return typeof value === 'string' && value.trim().length > 0
 }
 
+function getAccountAgeDays(createdAt: Date): number {
+  return (Date.now() - createdAt.getTime()) / (24 * 60 * 60 * 1000)
+}
+
+function getReportRateLimitMax(profile: UserRateLimitProfile): number {
+  if (STAFF_ROLES.has(profile.role)) return REPORT_RATE_LIMIT_TIERS.STAFF_MAX_ITEMS
+  if (profile.trustScore >= REPORT_RATE_LIMIT_TIERS.TRUSTED_TRUST_SCORE) {
+    return REPORT_RATE_LIMIT_TIERS.TRUSTED_MAX_ITEMS
+  }
+  if (
+    profile.trustScore >= REPORT_RATE_LIMIT_TIERS.CONTRIBUTOR_TRUST_SCORE ||
+    getAccountAgeDays(profile.createdAt) >= REPORT_RATE_LIMIT_TIERS.ESTABLISHED_ACCOUNT_DAYS
+  ) {
+    return REPORT_RATE_LIMIT_TIERS.ESTABLISHED_MAX_ITEMS
+  }
+
+  return DEFAULT_RATE_LIMITS.report.maxItems
+}
+
 export class SpamDetectionService {
   private readonly config: NormalizedSpamDetectionConfig
 
@@ -103,6 +147,10 @@ export class SpamDetectionService {
           ...DEFAULT_CONFIG.rateLimits.comment,
           ...config.rateLimits?.comment,
         },
+        game: {
+          ...DEFAULT_CONFIG.rateLimits.game,
+          ...config.rateLimits?.game,
+        },
       },
     }
   }
@@ -163,13 +211,10 @@ export class SpamDetectionService {
     entityType: SpamEntityType,
   ): Promise<SpamDetectionResult> {
     const category = getEntityCategory(entityType)
-    const rule = this.getRateLimitRule(category)
+    const rule = await this.getRateLimitRule(category, userId)
     const windowStart = new Date(Date.now() - rule.windowMinutes * 60 * 1000)
 
-    const count =
-      category === 'report'
-        ? await this.countRecentReports(userId, windowStart)
-        : await this.countRecentComments(userId, windowStart)
+    const count = await this.countRecentContent(category, userId, windowStart)
 
     if (count >= rule.maxItems) {
       return {
@@ -183,12 +228,42 @@ export class SpamDetectionService {
     return { isSpam: false, confidence: 0, method: 'rate_limiting' }
   }
 
-  private getRateLimitRule(category: SpamEntityCategory): RateLimitRule {
+  private async getRateLimitRule(
+    category: SpamEntityCategory,
+    userId: string,
+  ): Promise<RateLimitRule> {
     const configured = this.config.rateLimits[category]
-    return {
+    const baseRule = {
       windowMinutes: this.config.rateLimitWindow ?? configured.windowMinutes,
       maxItems: this.config.rateLimitMax ?? configured.maxItems,
     }
+
+    if (category !== 'report' || this.config.rateLimitMax !== undefined) return baseRule
+
+    const profile = await this.getUserRateLimitProfile(userId)
+    if (!profile) return baseRule
+
+    return {
+      ...baseRule,
+      maxItems: Math.max(baseRule.maxItems, getReportRateLimitMax(profile)),
+    }
+  }
+
+  private async getUserRateLimitProfile(userId: string): Promise<UserRateLimitProfile | null> {
+    return await this.prisma.user.findUnique({
+      where: { id: userId },
+      select: { createdAt: true, role: true, trustScore: true },
+    })
+  }
+
+  private async countRecentContent(
+    category: SpamEntityCategory,
+    userId: string,
+    windowStart: Date,
+  ): Promise<number> {
+    if (category === 'report') return await this.countRecentReports(userId, windowStart)
+    if (category === 'game') return await this.countRecentGames(userId, windowStart)
+    return await this.countRecentComments(userId, windowStart)
   }
 
   private async countRecentReports(userId: string, windowStart: Date): Promise<number> {
@@ -210,6 +285,15 @@ export class SpamDetectionService {
     return handheldCount + pcCount
   }
 
+  private async countRecentGames(userId: string, windowStart: Date): Promise<number> {
+    return await this.prisma.game.count({
+      where: {
+        submittedBy: userId,
+        submittedAt: { gte: windowStart },
+      },
+    })
+  }
+
   private async countRecentComments(userId: string, windowStart: Date): Promise<number> {
     const [handheldCount, pcCount] = await Promise.all([
       this.prisma.comment.count({
@@ -241,10 +325,7 @@ export class SpamDetectionService {
     if (!normalizedContent) return { isSpam: false, confidence: 0, method: 'duplicate_detection' }
 
     const category = getEntityCategory(entityType)
-    const recentContent =
-      category === 'report'
-        ? await this.getRecentReportContent(userId, recentTimeWindow)
-        : await this.getRecentCommentContent(userId, recentTimeWindow)
+    const recentContent = await this.getRecentContent(category, userId, recentTimeWindow)
 
     const duplicates = recentContent.filter((recent) => {
       const normalized = this.normalizeContent(recent)
@@ -266,6 +347,16 @@ export class SpamDetectionService {
     return { isSpam: false, confidence: 0, method: 'duplicate_detection' }
   }
 
+  private async getRecentContent(
+    category: SpamEntityCategory,
+    userId: string,
+    recentTimeWindow: Date,
+  ): Promise<string[]> {
+    if (category === 'report') return await this.getRecentReportContent(userId, recentTimeWindow)
+    if (category === 'game') return await this.getRecentGameContent(userId, recentTimeWindow)
+    return await this.getRecentCommentContent(userId, recentTimeWindow)
+  }
+
   private async getRecentReportContent(userId: string, recentTimeWindow: Date): Promise<string[]> {
     const [recentListings, recentPcListings] = await Promise.all([
       this.prisma.listing.findMany({
@@ -295,6 +386,20 @@ export class SpamDetectionService {
       .filter(isNonEmptyString)
   }
 
+  private async getRecentGameContent(userId: string, recentTimeWindow: Date): Promise<string[]> {
+    const recentGames = await this.prisma.game.findMany({
+      where: {
+        submittedBy: userId,
+        submittedAt: { gte: recentTimeWindow },
+      },
+      select: { title: true },
+      take: SPAM_DETECTION_THRESHOLDS.MAX_RECENT_ITEMS_TO_CHECK,
+      orderBy: { submittedAt: 'desc' },
+    })
+
+    return recentGames.map((entry) => entry.title).filter(isNonEmptyString)
+  }
+
   private async getRecentCommentContent(userId: string, recentTimeWindow: Date): Promise<string[]> {
     const [recentComments, recentPcComments] = await Promise.all([
       this.prisma.comment.findMany({

From 01c294c63f4a187322578380e5a7cea5cc143c62 Mon Sep 17 00:00:00 2001
From: Producdevity <y.gherbi.dev@gmail.com>
Date: Mon, 1 Jun 2026 19:40:43 +0200
Subject: [PATCH 29/35] fix: update game placeholder SVG with new design and
 layout adjustments

---
 public/placeholder/game.svg | 49 +++++++++++++++++++++++++++++++++----
 1 file changed, 44 insertions(+), 5 deletions(-)

diff --git a/public/placeholder/game.svg b/public/placeholder/game.svg
index c69a573a3..6d2627eb2 100644
--- a/public/placeholder/game.svg
+++ b/public/placeholder/game.svg
@@ -1,8 +1,50 @@
 <svg xmlns="http://www.w3.org/2000/svg" width="300" height="400" viewBox="0 0 300 400">
   <rect width="100%" height="100%" fill="#CBD5E1" />
+
+  <g transform="translate(97.06 77.06) scale(0.35294)">
+    <path
+      d="
+        M136 65
+        H164
+        Q177 65 177 78
+        V116
+        Q177 123 184 123
+        H222
+        Q235 123 235 136
+        V164
+        Q235 177 222 177
+        H184
+        Q177 177 177 184
+        V222
+        Q177 235 164 235
+        H136
+        Q123 235 123 222
+        V184
+        Q123 177 116 177
+        H78
+        Q65 177 65 164
+        V136
+        Q65 123 78 123
+        H116
+        Q123 123 123 116
+        V78
+        Q123 65 136 65
+        Z
+      "
+      fill="#64748B"
+    />
+
+    <path
+      d="M150 100 V200 M100 150 H200"
+      stroke="#536176"
+      stroke-width="12"
+      stroke-linecap="round"
+    />
+  </g>
+
   <text
     x="150"
-    y="200"
+    y="220"
     font-family="Arial, sans-serif"
     font-size="24"
     text-anchor="middle"
@@ -10,10 +52,7 @@
   >
     Game Cover
   </text>
-  <path
-    d="M140,160 L120,140 L120,120 L140,120 L140,100 L160,100 L160,120 L180,120 L180,140 L160,140 L160,160 Z"
-    fill="#64748B"
-  />
+
   <rect x="120" y="240" width="60" height="20" rx="5" fill="#64748B" />
   <rect x="120" y="270" width="40" height="10" rx="5" fill="#64748B" />
   <rect x="120" y="290" width="30" height="10" rx="5" fill="#64748B" />

From 8eba5f7e2edbcb2efeae67cfa29089089ddff640 Mon Sep 17 00:00:00 2001
From: Producdevity <y.gherbi.dev@gmail.com>
Date: Mon, 1 Jun 2026 19:41:13 +0200
Subject: [PATCH 30/35] fix: improve Turnstile script handling and widget
 rendering reliability, add support for structured error callbacks and
 flexible UI options

---
 .../HumanVerificationProvider.test.tsx        | 107 +++++++++++++++---
 .../components/HumanVerificationProvider.tsx  |  87 +++++++++-----
 2 files changed, 151 insertions(+), 43 deletions(-)

diff --git a/src/features/human-verification/client/components/HumanVerificationProvider.test.tsx b/src/features/human-verification/client/components/HumanVerificationProvider.test.tsx
index c34312115..e6cf0a857 100644
--- a/src/features/human-verification/client/components/HumanVerificationProvider.test.tsx
+++ b/src/features/human-verification/client/components/HumanVerificationProvider.test.tsx
@@ -1,8 +1,16 @@
 import { act, fireEvent, render, screen } from '@testing-library/react'
-import { afterEach, describe, expect, it, vi } from 'vitest'
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
 import { HumanVerificationProvider, useHumanVerification } from './HumanVerificationProvider'
 import { HUMAN_VERIFICATION_ACTION } from '../../shared/constants'
 
+interface ScriptProps {
+  src: string
+  onReady?: () => void
+  onError?: () => void
+}
+
+const scriptProps: ScriptProps[] = []
+
 vi.mock('@/lib/env', () => ({
   env: {
     TURNSTILE_SITE_KEY: 'site-key',
@@ -10,7 +18,8 @@ vi.mock('@/lib/env', () => ({
 }))
 
 vi.mock('next/script', () => ({
-  default: function Script() {
+  default: function Script(props: ScriptProps) {
+    scriptProps.push(props)
     return null
   },
 }))
@@ -35,9 +44,9 @@ function CaptureButton(props: CaptureButtonProps) {
 }
 
 function createTurnstileStub() {
-  const renderWidget = vi.fn<(container: HTMLElement, options: Record<string, unknown>) => string>(
-    () => 'widget-id',
-  )
+  const renderWidget = vi.fn<
+    (container: HTMLElement, options: Record<string, unknown>) => string | undefined
+  >(() => 'widget-id')
   const ready = vi.fn(() => {
     throw new Error('turnstile.ready() must not be called when api.js is loaded async/defer')
   })
@@ -55,9 +64,12 @@ function createTurnstileStub() {
 }
 
 describe('HumanVerificationProvider', () => {
+  beforeEach(() => {
+    scriptProps.length = 0
+  })
+
   afterEach(() => {
     delete window.turnstile
-    delete window.onloadTurnstileCallback
     vi.unstubAllEnvs()
   })
 
@@ -66,7 +78,12 @@ describe('HumanVerificationProvider', () => {
 
     render(
       <HumanVerificationProvider>
-        <CaptureButton onRequest={(promise) => requests.push(promise)} />
+        <CaptureButton
+          onRequest={(promise) => {
+            requests.push(promise)
+            void promise.catch(() => {})
+          }}
+        />
       </HumanVerificationProvider>,
     )
 
@@ -78,20 +95,26 @@ describe('HumanVerificationProvider', () => {
     await expect(requests[1]).rejects.toThrow(/already in progress/i)
   })
 
-  it('becomes ready via onloadTurnstileCallback, renders the widget directly, then resolves with the token', async () => {
+  it('becomes ready from the script onReady callback, renders the widget, then resolves with the token', async () => {
     const stub = createTurnstileStub()
     const requests: Promise<string>[] = []
 
     render(
       <HumanVerificationProvider>
-        <CaptureButton onRequest={(promise) => requests.push(promise)} />
+        <CaptureButton
+          onRequest={(promise) => {
+            requests.push(promise)
+            void promise.catch(() => {})
+          }}
+        />
       </HumanVerificationProvider>,
     )
 
-    expect(typeof window.onloadTurnstileCallback).toBe('function')
-
     window.turnstile = stub.api
-    act(() => window.onloadTurnstileCallback?.())
+    expect(scriptProps[0].src).toBe(
+      'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit',
+    )
+    act(() => scriptProps[0].onReady?.())
 
     fireEvent.click(screen.getByRole('button', { name: /request verification/i }))
 
@@ -101,6 +124,8 @@ describe('HumanVerificationProvider', () => {
     const options = stub.renderWidget.mock.calls[0][1]
     expect(options.sitekey).toBe('site-key')
     expect(options.action).toBe(HUMAN_VERIFICATION_ACTION)
+    expect(options.size).toBe('flexible')
+    expect(options.appearance).toBe('always')
 
     const onToken = options.callback
     if (typeof onToken !== 'function') throw new Error('callback option is not a function')
@@ -119,11 +144,65 @@ describe('HumanVerificationProvider', () => {
       </HumanVerificationProvider>,
     )
 
-    expect(window.onloadTurnstileCallback).toBeUndefined()
-
     fireEvent.click(screen.getByRole('button', { name: /request verification/i }))
 
     expect(stub.renderWidget).toHaveBeenCalledTimes(1)
     expect(stub.ready).not.toHaveBeenCalled()
   })
+
+  it('rejects when Turnstile cannot render a widget id', async () => {
+    const stub = createTurnstileStub()
+    stub.renderWidget.mockReturnValueOnce(undefined)
+    const requests: Promise<string>[] = []
+
+    render(
+      <HumanVerificationProvider>
+        <CaptureButton
+          onRequest={(promise) => {
+            requests.push(promise)
+            void promise.catch(() => {})
+          }}
+        />
+      </HumanVerificationProvider>,
+    )
+
+    window.turnstile = stub.api
+    act(() => scriptProps[0].onReady?.())
+
+    await act(async () => {
+      fireEvent.click(screen.getByRole('button', { name: /request verification/i }))
+      await Promise.resolve()
+    })
+
+    await expect(requests[0]).rejects.toThrow(/failed to render/i)
+  })
+
+  it('rejects when Turnstile render throws', async () => {
+    const stub = createTurnstileStub()
+    stub.renderWidget.mockImplementationOnce(() => {
+      throw new Error('render failed')
+    })
+    const requests: Promise<string>[] = []
+
+    render(
+      <HumanVerificationProvider>
+        <CaptureButton
+          onRequest={(promise) => {
+            requests.push(promise)
+            void promise.catch(() => {})
+          }}
+        />
+      </HumanVerificationProvider>,
+    )
+
+    window.turnstile = stub.api
+    act(() => scriptProps[0].onReady?.())
+
+    await act(async () => {
+      fireEvent.click(screen.getByRole('button', { name: /request verification/i }))
+      await Promise.resolve()
+    })
+
+    await expect(requests[0]).rejects.toThrow(/failed to render/i)
+  })
 })
diff --git a/src/features/human-verification/client/components/HumanVerificationProvider.tsx b/src/features/human-verification/client/components/HumanVerificationProvider.tsx
index 7e09cebd3..8f2589692 100644
--- a/src/features/human-verification/client/components/HumanVerificationProvider.tsx
+++ b/src/features/human-verification/client/components/HumanVerificationProvider.tsx
@@ -34,7 +34,6 @@ interface TurnstileApi {
 declare global {
   interface Window {
     turnstile?: TurnstileApi
-    onloadTurnstileCallback?: () => void
   }
 }
 
@@ -45,6 +44,8 @@ interface VerificationRequest {
 type RequestVerification = (request: VerificationRequest) => Promise<string>
 type ScriptStatus = 'idle' | 'ready' | 'failed'
 
+const VERIFICATION_SCRIPT_LOAD_TIMEOUT_MS = 20_000
+
 const HumanVerificationContext = createContext<RequestVerification | null>(null)
 
 interface PendingRequest {
@@ -92,37 +93,64 @@ export function HumanVerificationProvider(props: PropsWithChildren) {
   }, [])
 
   useEffect(() => {
-    if (window.turnstile) return
-    window.onloadTurnstileCallback = () => setScriptStatus('ready')
-    return () => {
-      delete window.onloadTurnstileCallback
-    }
-  }, [])
+    if (!pendingRequest || scriptStatus !== 'idle') return
+
+    const timeoutId = window.setTimeout(() => {
+      setScriptStatus('failed')
+      pendingRequest.reject(new Error('Human verification failed to load. Please try again.'))
+      closeDialog()
+    }, VERIFICATION_SCRIPT_LOAD_TIMEOUT_MS)
+
+    return () => window.clearTimeout(timeoutId)
+  }, [closeDialog, pendingRequest, scriptStatus])
 
   useEffect(() => {
     const turnstile = window.turnstile
     if (!pendingRequest || scriptStatus !== 'ready' || !widgetContainer || !turnstile) return
 
-    const widgetId = turnstile.render(widgetContainer, {
-      sitekey: turnstileSiteKey,
-      action: pendingRequest.action,
-      theme: 'auto',
-      callback: (token: string) => {
-        pendingRequest.resolve(token)
-        closeDialog()
-      },
-      'error-callback': () => {
-        pendingRequest.reject(new Error('Human verification failed. Please try again.'))
-        closeDialog()
-      },
-      'expired-callback': () => {
-        if (widgetIdRef.current && window.turnstile) {
-          window.turnstile.reset(widgetIdRef.current)
-        }
-      },
-    })
-
-    widgetIdRef.current = widgetId ?? null
+    let widgetId: TurnstileWidgetId | undefined
+    try {
+      widgetId = turnstile.render(widgetContainer, {
+        sitekey: turnstileSiteKey,
+        action: pendingRequest.action,
+        theme: 'auto',
+        size: 'flexible',
+        appearance: 'always',
+        callback: (token: string) => {
+          pendingRequest.resolve(token)
+          closeDialog()
+        },
+        'error-callback': () => {
+          pendingRequest.reject(new Error('Human verification failed. Please try again.'))
+          closeDialog()
+        },
+        'expired-callback': () => {
+          if (widgetIdRef.current && window.turnstile) {
+            window.turnstile.reset(widgetIdRef.current)
+          }
+        },
+        'timeout-callback': () => {
+          pendingRequest.reject(new Error('Human verification timed out. Please try again.'))
+          closeDialog()
+        },
+        'unsupported-callback': () => {
+          pendingRequest.reject(new Error('Human verification is not supported in this browser.'))
+          closeDialog()
+        },
+      })
+    } catch {
+      pendingRequest.reject(new Error('Human verification failed to render. Please try again.'))
+      queueMicrotask(closeDialog)
+      return
+    }
+
+    if (!widgetId) {
+      pendingRequest.reject(new Error('Human verification failed to render. Please try again.'))
+      queueMicrotask(closeDialog)
+      return
+    }
+
+    widgetIdRef.current = widgetId
 
     return () => {
       if (widgetIdRef.current && window.turnstile) {
@@ -142,8 +170,9 @@ export function HumanVerificationProvider(props: PropsWithChildren) {
     <HumanVerificationContext.Provider value={requestVerification}>
       {turnstileSiteKey && (
         <Script
-          src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit&onload=onloadTurnstileCallback"
+          src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit"
           strategy="afterInteractive"
+          onReady={() => setScriptStatus('ready')}
           onError={() => {
             setScriptStatus('failed')
             pendingRequestRef.current?.reject(new Error('Human verification failed to load.'))
@@ -164,7 +193,7 @@ export function HumanVerificationProvider(props: PropsWithChildren) {
             {scriptStatus !== 'ready' && (
               <p className="text-sm text-gray-500 dark:text-gray-400">Loading verification...</p>
             )}
-            <div ref={setWidgetContainer} />
+            <div ref={setWidgetContainer} className="min-h-16 w-full" />
           </div>
         </DialogContent>
       </Dialog>

From b1c811237e43b8b9319ce526310c69c4ce1ce179 Mon Sep 17 00:00:00 2001
From: Producdevity <y.gherbi.dev@gmail.com>
Date: Mon, 1 Jun 2026 19:43:06 +0200
Subject: [PATCH 31/35] fix: validate Turnstile responses with stricter
 hostname and action checks, add development hostname support, and fix test
 coverage

---
 .../server/providers/turnstile.test.ts        | 74 +++++++++++++++++-
 .../server/providers/turnstile.ts             | 76 ++++++++++++++++++-
 2 files changed, 146 insertions(+), 4 deletions(-)

diff --git a/src/features/human-verification/server/providers/turnstile.test.ts b/src/features/human-verification/server/providers/turnstile.test.ts
index a26e2cc36..3ccba67c5 100644
--- a/src/features/human-verification/server/providers/turnstile.test.ts
+++ b/src/features/human-verification/server/providers/turnstile.test.ts
@@ -38,9 +38,16 @@ describe('turnstile provider', () => {
 
   it('validates a Turnstile token with Siteverify', async () => {
     vi.stubEnv('TURNSTILE_SECRET_KEY', 'secret')
+    vi.stubEnv('NEXT_PUBLIC_APP_URL', 'https://www.emuready.com')
     global.fetch = vi.fn(
       async () =>
-        new Response(JSON.stringify({ success: true, action: HUMAN_VERIFICATION_ACTION })),
+        new Response(
+          JSON.stringify({
+            success: true,
+            action: HUMAN_VERIFICATION_ACTION,
+            hostname: 'www.emuready.com',
+          }),
+        ),
     )
 
     await expect(
@@ -62,8 +69,12 @@ describe('turnstile provider', () => {
 
   it('rejects tokens issued for a different action', async () => {
     vi.stubEnv('TURNSTILE_SECRET_KEY', 'secret')
+    vi.stubEnv('NEXT_PUBLIC_APP_URL', 'https://www.emuready.com')
     global.fetch = vi.fn(
-      async () => new Response(JSON.stringify({ success: true, action: 'login' })),
+      async () =>
+        new Response(
+          JSON.stringify({ success: true, action: 'login', hostname: 'www.emuready.com' }),
+        ),
     )
 
     await expect(verifyTurnstileToken({ token: 'token' })).resolves.toEqual({
@@ -72,6 +83,65 @@ describe('turnstile provider', () => {
     })
   })
 
+  it('rejects successful Siteverify responses without an action field', async () => {
+    vi.stubEnv('TURNSTILE_SECRET_KEY', 'secret')
+    vi.stubEnv('NEXT_PUBLIC_APP_URL', 'https://www.emuready.com')
+    global.fetch = vi.fn(
+      async () => new Response(JSON.stringify({ success: true, hostname: 'www.emuready.com' })),
+    )
+
+    await expect(verifyTurnstileToken({ token: 'token' })).resolves.toEqual({
+      success: false,
+      errorCodes: ['action-mismatch'],
+    })
+  })
+
+  it('rejects successful Siteverify responses without a hostname field', async () => {
+    vi.stubEnv('TURNSTILE_SECRET_KEY', 'secret')
+    vi.stubEnv('NEXT_PUBLIC_APP_URL', 'https://www.emuready.com')
+    global.fetch = vi.fn(
+      async () =>
+        new Response(JSON.stringify({ success: true, action: HUMAN_VERIFICATION_ACTION })),
+    )
+
+    await expect(verifyTurnstileToken({ token: 'token' })).resolves.toEqual({
+      success: false,
+      errorCodes: ['hostname-missing'],
+    })
+  })
+
+  it('rejects successful Siteverify responses for an unexpected hostname', async () => {
+    vi.stubEnv('TURNSTILE_SECRET_KEY', 'secret')
+    vi.stubEnv('NEXT_PUBLIC_APP_URL', 'https://www.emuready.com')
+    global.fetch = vi.fn(
+      async () =>
+        new Response(
+          JSON.stringify({
+            success: true,
+            action: HUMAN_VERIFICATION_ACTION,
+            hostname: 'evil.example',
+          }),
+        ),
+    )
+
+    await expect(verifyTurnstileToken({ token: 'token' })).resolves.toEqual({
+      success: false,
+      errorCodes: ['hostname-mismatch'],
+    })
+  })
+
+  it('allows Cloudflare test-key responses to use their documented test action', async () => {
+    vi.stubEnv('TURNSTILE_SECRET_KEY', '1x0000000000000000000000000000000AA')
+    global.fetch = vi.fn(
+      async () => new Response(JSON.stringify({ success: true, action: 'test' })),
+    )
+
+    await expect(verifyTurnstileToken({ token: 'XXXX.DUMMY.TOKEN.XXXX' })).resolves.toEqual({
+      success: true,
+      errorCodes: [],
+    })
+  })
+
   it('returns provider error codes for invalid tokens', async () => {
     vi.stubEnv('TURNSTILE_SECRET_KEY', 'secret')
     global.fetch = vi.fn(
diff --git a/src/features/human-verification/server/providers/turnstile.ts b/src/features/human-verification/server/providers/turnstile.ts
index 941fc647f..874faf6df 100644
--- a/src/features/human-verification/server/providers/turnstile.ts
+++ b/src/features/human-verification/server/providers/turnstile.ts
@@ -3,6 +3,12 @@ import { HUMAN_VERIFICATION_ACTION } from '../../shared/constants'
 
 const TURNSTILE_SITEVERIFY_URL = 'https://challenges.cloudflare.com/turnstile/v0/siteverify'
 const TURNSTILE_SITEVERIFY_TIMEOUT_MS = 5000
+const TURNSTILE_TEST_SECRET_KEYS = new Set([
+  '1x0000000000000000000000000000000AA',
+  '2x0000000000000000000000000000000AA',
+  '3x0000000000000000000000000000000AA',
+])
+const LOCAL_DEVELOPMENT_HOSTNAMES = ['localhost', '127.0.0.1', '::1']
 
 const TurnstileResponseSchema = z
   .object({
@@ -68,8 +74,15 @@ export async function verifyTurnstileToken(params: {
   const parsed = TurnstileResponseSchema.safeParse(await response.json())
   if (!parsed.success) return { success: false, errorCodes: ['invalid-siteverify-response'] }
 
-  if (parsed.data.success && parsed.data.action !== HUMAN_VERIFICATION_ACTION) {
-    return { success: false, errorCodes: ['action-mismatch'] }
+  if (parsed.data.success && !TURNSTILE_TEST_SECRET_KEYS.has(secret)) {
+    const validationErrors = validateSuccessfulResponse({
+      action: parsed.data.action,
+      hostname: parsed.data.hostname,
+    })
+
+    if (validationErrors.length > 0) {
+      return { success: false, errorCodes: validationErrors }
+    }
   }
 
   return {
@@ -78,6 +91,65 @@ export async function verifyTurnstileToken(params: {
   }
 }
 
+function validateSuccessfulResponse(params: { action?: string; hostname?: string }): string[] {
+  const errors: string[] = []
+
+  if (params.action !== HUMAN_VERIFICATION_ACTION) errors.push('action-mismatch')
+
+  const hostname = normalizeHostname(params.hostname)
+  if (!hostname) {
+    errors.push('hostname-missing')
+  } else if (!getAllowedHostnames().has(hostname)) {
+    errors.push('hostname-mismatch')
+  }
+
+  return errors
+}
+
+function getAllowedHostnames(): Set<string> {
+  const hostnames = new Set<string>()
+
+  addCommaSeparatedHostnames(hostnames, process.env.TURNSTILE_ALLOWED_HOSTNAMES)
+  addHostnameFromUrl(hostnames, process.env.NEXT_PUBLIC_APP_URL)
+  addHostnameFromUrl(hostnames, process.env.VERCEL_URL)
+  addHostnameFromUrl(hostnames, process.env.VERCEL_BRANCH_URL)
+  addHostnameFromUrl(hostnames, process.env.VERCEL_PROJECT_PRODUCTION_URL)
+
+  if (process.env.NODE_ENV !== 'production' || process.env.NEXT_PUBLIC_APP_ENV === 'test') {
+    for (const hostname of LOCAL_DEVELOPMENT_HOSTNAMES) hostnames.add(hostname)
+  }
+
+  return hostnames
+}
+
+function addCommaSeparatedHostnames(hostnames: Set<string>, value: string | undefined): void {
+  if (!value) return
+  for (const hostname of value.split(',')) addHostname(hostnames, hostname)
+}
+
+function addHostnameFromUrl(hostnames: Set<string>, value: string | undefined): void {
+  if (!value) return
+
+  try {
+    const url = value.includes('://') ? new URL(value) : new URL(`https://${value}`)
+    addHostname(hostnames, url.hostname)
+  } catch {
+    addHostname(hostnames, value)
+  }
+}
+
+function addHostname(hostnames: Set<string>, value: string | undefined): void {
+  const hostname = normalizeHostname(value)
+  if (hostname) hostnames.add(hostname)
+}
+
+function normalizeHostname(value: string | undefined): string | null {
+  const trimmed = value?.trim().toLowerCase()
+  if (!trimmed) return null
+
+  return trimmed.replace(/^\[/, '').replace(/\]$/, '').replace(/\.$/, '')
+}
+
 function isAbortError(error: unknown): boolean {
   if (typeof error !== 'object' || error === null || !('name' in error)) return false
   const { name } = error

From 68a30e9ab97b4b3f425fb447e0d8b054c8b1e808 Mon Sep 17 00:00:00 2001
From: Producdevity <y.gherbi.dev@gmail.com>
Date: Mon, 1 Jun 2026 19:49:17 +0200
Subject: [PATCH 32/35] tests: fix tests, remove tests that are not actually
 testing anything

---
 src/schemas/listing.test.ts                   | 445 +++++-------------
 src/server/api/routers/games.test.ts          | 134 ++++++
 .../api/routers/listings/comments.test.ts     |  21 +
 src/server/api/routers/listings/core.test.ts  |  29 ++
 src/server/api/routers/pcListings.test.ts     |  55 +++
 src/server/utils/spam-check.test.ts           |  35 +-
 6 files changed, 384 insertions(+), 335 deletions(-)
 create mode 100644 src/server/api/routers/games.test.ts

diff --git a/src/schemas/listing.test.ts b/src/schemas/listing.test.ts
index 2ef1fb2e8..0dffdcc1a 100644
--- a/src/schemas/listing.test.ts
+++ b/src/schemas/listing.test.ts
@@ -1,360 +1,137 @@
-import { describe, it, expect } from 'vitest'
-import { HUMAN_VERIFICATION_TOKEN_MAX_LENGTH } from '@/features/human-verification/shared/constants'
+import { describe, expect, it } from 'vitest'
 import { ApprovalStatus } from '@orm'
 import {
+  CreateCommentSchema,
   CreateListingSchema,
-  GetListingsSchema,
   GetAllListingsAdminSchema,
-  UpdateListingAdminSchema,
-  RejectListingSchema,
-  CreateCommentSchema,
+  GetListingsSchema,
   OverrideApprovalStatusSchema,
+  RejectListingSchema,
+  UpdateListingAdminSchema,
 } from './listing'
 
-describe('Listing Schemas - Null Handling', () => {
-  describe('CreateListingSchema', () => {
-    it('should accept null for optional fields', () => {
-      const validInput = {
-        gameId: '123e4567-e89b-12d3-a456-426614174000',
-        deviceId: '123e4567-e89b-12d3-a456-426614174001',
-        emulatorId: '123e4567-e89b-12d3-a456-426614174002',
-        performanceId: 5,
-        notes: null,
-        customFieldValues: null,
-      }
-
-      const result = CreateListingSchema.safeParse(validInput)
-      expect(result.success).toBe(true)
-      if (result.success) {
-        expect(result.data.notes).toBeNull()
-        expect(result.data.customFieldValues).toBeNull()
-      }
-    })
-
-    it('should accept undefined for optional fields', () => {
-      const validInput = {
-        gameId: '123e4567-e89b-12d3-a456-426614174000',
-        deviceId: '123e4567-e89b-12d3-a456-426614174001',
-        emulatorId: '123e4567-e89b-12d3-a456-426614174002',
-        performanceId: 5,
-      }
-
-      const result = CreateListingSchema.safeParse(validInput)
-      expect(result.success).toBe(true)
-      if (result.success) {
-        expect(result.data.notes).toBeUndefined()
-        expect(result.data.customFieldValues).toBeUndefined()
-      }
-    })
-
-    it('should reject invalid data types', () => {
-      const invalidInput = {
-        gameId: 'not-a-uuid',
-        deviceId: '123e4567-e89b-12d3-a456-426614174001',
-        emulatorId: '123e4567-e89b-12d3-a456-426614174002',
-        performanceId: 5,
-      }
-
-      const result = CreateListingSchema.safeParse(invalidInput)
-      expect(result.success).toBe(false)
-    })
-
-    it('should enforce max length on notes', () => {
-      const invalidInput = {
-        gameId: '123e4567-e89b-12d3-a456-426614174000',
-        deviceId: '123e4567-e89b-12d3-a456-426614174001',
-        emulatorId: '123e4567-e89b-12d3-a456-426614174002',
-        performanceId: 5,
-        notes: 'a'.repeat(5001), // Exceeds 5000 character limit
-      }
-
-      const result = CreateListingSchema.safeParse(invalidInput)
-      expect(result.success).toBe(false)
-    })
-
-    it('should enforce max length on human verification tokens', () => {
-      const validInput = {
-        gameId: '123e4567-e89b-12d3-a456-426614174000',
-        deviceId: '123e4567-e89b-12d3-a456-426614174001',
-        emulatorId: '123e4567-e89b-12d3-a456-426614174002',
-        performanceId: 5,
-        humanVerificationToken: 'a'.repeat(HUMAN_VERIFICATION_TOKEN_MAX_LENGTH),
-      }
-
-      expect(CreateListingSchema.safeParse(validInput).success).toBe(true)
-
-      const invalidInput = {
-        ...validInput,
-        humanVerificationToken: 'a'.repeat(HUMAN_VERIFICATION_TOKEN_MAX_LENGTH + 1),
-      }
-
-      expect(CreateListingSchema.safeParse(invalidInput).success).toBe(false)
-    })
+describe('Listing schemas - null compatibility contracts', () => {
+  it('keeps null and undefined optional fields distinct for create input', () => {
+    const nullResult = CreateListingSchema.safeParse({
+      gameId: '123e4567-e89b-12d3-a456-426614174000',
+      deviceId: '123e4567-e89b-12d3-a456-426614174001',
+      emulatorId: '123e4567-e89b-12d3-a456-426614174002',
+      performanceId: 5,
+      notes: null,
+      customFieldValues: null,
+    })
+
+    expect(nullResult.success).toBe(true)
+    if (nullResult.success) {
+      expect(nullResult.data.notes).toBeNull()
+      expect(nullResult.data.customFieldValues).toBeNull()
+    }
+
+    const undefinedResult = CreateListingSchema.safeParse({
+      gameId: '123e4567-e89b-12d3-a456-426614174000',
+      deviceId: '123e4567-e89b-12d3-a456-426614174001',
+      emulatorId: '123e4567-e89b-12d3-a456-426614174002',
+      performanceId: 5,
+    })
+
+    expect(undefinedResult.success).toBe(true)
+    if (undefinedResult.success) {
+      expect(undefinedResult.data.notes).toBeUndefined()
+      expect(undefinedResult.data.customFieldValues).toBeUndefined()
+    }
   })
 
-  describe('GetListingsSchema', () => {
-    it('should accept null for all optional filter fields', () => {
-      const validInput = {
-        systemIds: null,
-        deviceIds: null,
-        socIds: null,
-        emulatorIds: null,
-        performanceIds: null,
-        searchTerm: null,
-        sortField: null,
-        sortDirection: null,
-        approvalStatus: null,
-        myListings: null,
-        page: 1,
-        limit: 10,
-      }
-
-      const result = GetListingsSchema.safeParse(validInput)
-      expect(result.success).toBe(true)
-      if (result.success) {
-        expect(result.data.systemIds).toBeNull()
-        expect(result.data.searchTerm).toBeNull()
-        expect(result.data.sortDirection).toBeNull()
-      }
-    })
-
-    it('should accept valid enum values', () => {
-      const validInput = {
-        page: 1,
-        limit: 10,
-        sortField: 'game.title',
-        sortDirection: 'asc',
-        approvalStatus: ApprovalStatus.APPROVED,
-      }
-
-      const result = GetListingsSchema.safeParse(validInput)
-      expect(result.success).toBe(true)
-      if (result.success) {
-        expect(result.data.sortField).toBe('game.title')
-        expect(result.data.sortDirection).toBe('asc')
-        expect(result.data.approvalStatus).toBe(ApprovalStatus.APPROVED)
-      }
-    })
+  it('accepts null filter values from listing URLs', () => {
+    const result = GetListingsSchema.safeParse({
+      systemIds: null,
+      deviceIds: null,
+      socIds: null,
+      emulatorIds: null,
+      performanceIds: null,
+      searchTerm: null,
+      sortField: null,
+      sortDirection: null,
+      approvalStatus: null,
+      myListings: null,
+      page: 1,
+      limit: 10,
+    })
+
+    expect(result.success).toBe(true)
+    if (result.success) {
+      expect(result.data.systemIds).toBeNull()
+      expect(result.data.searchTerm).toBeNull()
+      expect(result.data.sortDirection).toBeNull()
+    }
   })
 
-  describe('GetAllListingsAdminSchema', () => {
-    it('should accept null for optional admin filter fields', () => {
-      const validInput = {
-        page: 1,
-        limit: 20,
-        sortField: null,
-        sortDirection: null,
-        search: null,
-        statusFilter: null,
-        systemFilter: null,
-        emulatorFilter: null,
-      }
-
-      const result = GetAllListingsAdminSchema.safeParse(validInput)
-      expect(result.success).toBe(true)
-      if (result.success) {
-        expect(result.data.sortField).toBeNull()
-        expect(result.data.sortDirection).toBeNull()
-        expect(result.data.search).toBeNull()
-        expect(result.data.statusFilter).toBeNull()
-      }
-    })
-
-    it('should enforce minimum search string length when provided', () => {
-      const invalidInput = {
-        page: 1,
-        limit: 20,
-        search: '', // Empty string should fail min(1) validation
-      }
-
-      const result = GetAllListingsAdminSchema.safeParse(invalidInput)
-      expect(result.success).toBe(false)
-    })
-
-    it('should accept valid status filter', () => {
-      const validInput = {
-        page: 1,
-        limit: 20,
-        statusFilter: ApprovalStatus.PENDING,
-      }
-
-      const result = GetAllListingsAdminSchema.safeParse(validInput)
-      expect(result.success).toBe(true)
-      if (result.success) {
-        expect(result.data.statusFilter).toBe(ApprovalStatus.PENDING)
-      }
-    })
+  it('accepts null filter values from admin listing URLs', () => {
+    const result = GetAllListingsAdminSchema.safeParse({
+      page: 1,
+      limit: 20,
+      sortField: null,
+      sortDirection: null,
+      search: null,
+      statusFilter: null,
+      systemFilter: null,
+      emulatorFilter: null,
+    })
+
+    expect(result.success).toBe(true)
+    if (result.success) {
+      expect(result.data.sortField).toBeNull()
+      expect(result.data.sortDirection).toBeNull()
+      expect(result.data.search).toBeNull()
+      expect(result.data.statusFilter).toBeNull()
+    }
   })
 
-  describe('UpdateListingAdminSchema', () => {
-    it('should accept null for notes and customFieldValues', () => {
-      const validInput = {
-        id: '123e4567-e89b-12d3-a456-426614174000',
-        gameId: '123e4567-e89b-12d3-a456-426614174001',
-        deviceId: '123e4567-e89b-12d3-a456-426614174002',
-        emulatorId: '123e4567-e89b-12d3-a456-426614174003',
-        performanceId: 5,
-        status: ApprovalStatus.APPROVED,
-        notes: null,
-        customFieldValues: null,
-      }
-
-      const result = UpdateListingAdminSchema.safeParse(validInput)
-      expect(result.success).toBe(true)
-      if (result.success) {
-        expect(result.data.notes).toBeNull()
-        expect(result.data.customFieldValues).toBeNull()
-      }
-    })
-
-    it('should validate custom field values structure', () => {
-      const validInput = {
-        id: '123e4567-e89b-12d3-a456-426614174000',
-        gameId: '123e4567-e89b-12d3-a456-426614174001',
-        deviceId: '123e4567-e89b-12d3-a456-426614174002',
-        emulatorId: '123e4567-e89b-12d3-a456-426614174003',
-        performanceId: 5,
-        status: ApprovalStatus.APPROVED,
-        customFieldValues: [
-          {
-            customFieldDefinitionId: '123e4567-e89b-12d3-a456-426614174004',
-            value: 'test value',
-          },
-          {
-            customFieldDefinitionId: '123e4567-e89b-12d3-a456-426614174005',
-            value: true,
-          },
-          {
-            customFieldDefinitionId: '123e4567-e89b-12d3-a456-426614174006',
-            value: 42,
-          },
-        ],
-      }
-
-      const result = UpdateListingAdminSchema.safeParse(validInput)
-      expect(result.success).toBe(true)
-    })
+  it('accepts null editable fields for admin updates', () => {
+    const result = UpdateListingAdminSchema.safeParse({
+      id: '123e4567-e89b-12d3-a456-426614174000',
+      gameId: '123e4567-e89b-12d3-a456-426614174001',
+      deviceId: '123e4567-e89b-12d3-a456-426614174002',
+      emulatorId: '123e4567-e89b-12d3-a456-426614174003',
+      performanceId: 5,
+      status: ApprovalStatus.APPROVED,
+      notes: null,
+      customFieldValues: null,
+    })
+
+    expect(result.success).toBe(true)
+    if (result.success) {
+      expect(result.data.notes).toBeNull()
+      expect(result.data.customFieldValues).toBeNull()
+    }
   })
 
-  describe('RejectListingSchema', () => {
-    it('should accept null for notes', () => {
-      const validInput = {
-        listingId: '123e4567-e89b-12d3-a456-426614174000',
-        notes: null,
-      }
-
-      const result = RejectListingSchema.safeParse(validInput)
-      expect(result.success).toBe(true)
-      if (result.success) {
-        expect(result.data.notes).toBeNull()
-      }
-    })
-
-    it('should accept string for notes', () => {
-      const validInput = {
-        listingId: '123e4567-e89b-12d3-a456-426614174000',
-        notes: 'Rejection reason here',
-      }
-
-      const result = RejectListingSchema.safeParse(validInput)
-      expect(result.success).toBe(true)
-      if (result.success) {
-        expect(result.data.notes).toBe('Rejection reason here')
-      }
+  it('accepts null moderator notes when rejecting or overriding a report', () => {
+    const rejectResult = RejectListingSchema.safeParse({
+      listingId: '123e4567-e89b-12d3-a456-426614174000',
+      notes: null,
     })
-  })
 
-  describe('CreateCommentSchema', () => {
-    it('should accept null for parentId', () => {
-      const validInput = {
-        listingId: '123e4567-e89b-12d3-a456-426614174000',
-        content: 'This is a comment',
-        parentId: null,
-      }
+    expect(rejectResult.success).toBe(true)
+    if (rejectResult.success) expect(rejectResult.data.notes).toBeNull()
 
-      const result = CreateCommentSchema.safeParse(validInput)
-      expect(result.success).toBe(true)
-      if (result.success) {
-        expect(result.data.parentId).toBeNull()
-      }
+    const overrideResult = OverrideApprovalStatusSchema.safeParse({
+      listingId: '123e4567-e89b-12d3-a456-426614174000',
+      newStatus: ApprovalStatus.APPROVED,
+      overrideNotes: null,
     })
 
-    it('should enforce content length limits', () => {
-      const invalidInput = {
-        listingId: '123e4567-e89b-12d3-a456-426614174000',
-        content: '', // Too short
-      }
-
-      const result = CreateCommentSchema.safeParse(invalidInput)
-      expect(result.success).toBe(false)
-
-      const tooLongInput = {
-        listingId: '123e4567-e89b-12d3-a456-426614174000',
-        content: 'a'.repeat(1001), // Too long
-      }
-
-      const result2 = CreateCommentSchema.safeParse(tooLongInput)
-      expect(result2.success).toBe(false)
-    })
-
-    it('should enforce max length on human verification tokens', () => {
-      const validInput = {
-        listingId: '123e4567-e89b-12d3-a456-426614174000',
-        content: 'This is a comment',
-        humanVerificationToken: 'a'.repeat(HUMAN_VERIFICATION_TOKEN_MAX_LENGTH),
-      }
-
-      expect(CreateCommentSchema.safeParse(validInput).success).toBe(true)
-
-      const invalidInput = {
-        ...validInput,
-        humanVerificationToken: 'a'.repeat(HUMAN_VERIFICATION_TOKEN_MAX_LENGTH + 1),
-      }
-
-      expect(CreateCommentSchema.safeParse(invalidInput).success).toBe(false)
-    })
+    expect(overrideResult.success).toBe(true)
+    if (overrideResult.success) expect(overrideResult.data.overrideNotes).toBeNull()
   })
 
-  describe('OverrideApprovalStatusSchema', () => {
-    it('should accept null for overrideNotes', () => {
-      const validInput = {
-        listingId: '123e4567-e89b-12d3-a456-426614174000',
-        newStatus: ApprovalStatus.APPROVED,
-        overrideNotes: null,
-      }
-
-      const result = OverrideApprovalStatusSchema.safeParse(validInput)
-      expect(result.success).toBe(true)
-      if (result.success) {
-        expect(result.data.overrideNotes).toBeNull()
-      }
-    })
-
-    it('should require valid ApprovalStatus enum value', () => {
-      const invalidInput = {
-        listingId: '123e4567-e89b-12d3-a456-426614174000',
-        newStatus: 'INVALID_STATUS',
-      }
-
-      const result = OverrideApprovalStatusSchema.safeParse(invalidInput)
-      expect(result.success).toBe(false)
+  it('accepts null for top-level comment parentId', () => {
+    const result = CreateCommentSchema.safeParse({
+      listingId: '123e4567-e89b-12d3-a456-426614174000',
+      content: 'This is a comment',
+      parentId: null,
     })
 
-    it('should accept all valid ApprovalStatus values', () => {
-      const statuses = [ApprovalStatus.PENDING, ApprovalStatus.APPROVED, ApprovalStatus.REJECTED]
-
-      statuses.forEach((status) => {
-        const input = {
-          listingId: '123e4567-e89b-12d3-a456-426614174000',
-          newStatus: status,
-        }
-
-        const result = OverrideApprovalStatusSchema.safeParse(input)
-        expect(result.success).toBe(true)
-        if (result.success) {
-          expect(result.data.newStatus).toBe(status)
-        }
-      })
-    })
+    expect(result.success).toBe(true)
+    if (result.success) expect(result.data.parentId).toBeNull()
   })
 })
diff --git a/src/server/api/routers/games.test.ts b/src/server/api/routers/games.test.ts
new file mode 100644
index 000000000..85a4d2672
--- /dev/null
+++ b/src/server/api/routers/games.test.ts
@@ -0,0 +1,134 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+import { ApprovalStatus, Role } from '@orm/client'
+
+vi.unmock('@/server/api/trpc')
+vi.unmock('@/server/api/root')
+
+const mockCheckSpamContent = vi.fn().mockResolvedValue(undefined)
+const mockGetSubmissionBalance = vi.fn().mockResolvedValue({
+  gamesCount: 0,
+  listingsCount: 1,
+  pcListingsCount: 0,
+  totalListingsCount: 1,
+  buffer: 2,
+  canSubmitGame: true,
+  reportsNeeded: 0,
+})
+const mockGameStatsCacheDelete = vi.fn()
+
+vi.mock('@/server/utils/spam-check', () => ({
+  checkSpamContent: (...args: unknown[]) => mockCheckSpamContent(...args),
+}))
+
+vi.mock('@/server/utils/submission-balance', () => ({
+  getSubmissionBalance: (...args: unknown[]) => mockGetSubmissionBalance(...args),
+}))
+
+vi.mock('@/server/utils/cache', () => ({
+  gameStatsCache: { delete: mockGameStatsCacheDelete, get: vi.fn(), set: vi.fn() },
+}))
+
+vi.mock('@/server/cache/invalidation', () => ({
+  invalidateGame: vi.fn().mockResolvedValue(undefined),
+  invalidateGameRelatedContent: vi.fn().mockResolvedValue(undefined),
+  invalidateListPages: vi.fn().mockResolvedValue(undefined),
+  invalidateSitemap: vi.fn().mockResolvedValue(undefined),
+  revalidateByTag: vi.fn().mockResolvedValue(undefined),
+}))
+
+vi.mock('@/server/notifications/eventEmitter', () => ({
+  notificationEventEmitter: { emitNotificationEvent: vi.fn() },
+  NOTIFICATION_EVENTS: {
+    GAME_STATUS_APPROVED: 'GAME_STATUS_APPROVED',
+    GAME_STATUS_REJECTED: 'GAME_STATUS_REJECTED',
+    GAME_STATUS_OVERRIDDEN: 'GAME_STATUS_OVERRIDDEN',
+  },
+}))
+
+vi.mock('@/lib/analytics', () => ({
+  default: { performance: { errorOccurred: vi.fn() } },
+}))
+
+const { gamesRouter } = await import('./games')
+
+const USER_ID = '00000000-0000-4000-a000-000000000001'
+const SYSTEM_ID = '00000000-0000-4000-a000-000000000010'
+const GAME_ID = '00000000-0000-4000-a000-000000000020'
+
+function createMockPrisma() {
+  return {
+    system: {
+      findUnique: vi.fn().mockResolvedValue({ id: SYSTEM_ID, name: 'Nintendo Switch' }),
+    },
+    game: {
+      findFirst: vi.fn().mockResolvedValue(null),
+      create: vi.fn().mockResolvedValue({
+        id: GAME_ID,
+        title: 'Amazing!! This runs perfectly!!',
+        systemId: SYSTEM_ID,
+        status: ApprovalStatus.PENDING,
+        system: { id: SYSTEM_ID, name: 'Nintendo Switch' },
+        submitter: { id: USER_ID, name: 'Test User' },
+      }),
+    },
+  }
+}
+
+type MockPrisma = ReturnType<typeof createMockPrisma>
+
+function createCaller(overrides: { role?: Role; prisma?: MockPrisma } = {}) {
+  const prisma = overrides.prisma ?? createMockPrisma()
+  return {
+    caller: gamesRouter.createCaller({
+      session: {
+        user: {
+          id: USER_ID,
+          email: 'test@test.com',
+          name: 'Test User',
+          role: overrides.role ?? Role.USER,
+          permissions: [],
+          showNsfw: false,
+        },
+      },
+      prisma: prisma as never,
+      headers: new Headers(),
+    }),
+    prisma,
+  }
+}
+
+describe('games router', () => {
+  beforeEach(() => {
+    vi.clearAllMocks()
+  })
+
+  describe('create', () => {
+    it('passes a human verification token to the spam check when retrying game creation', async () => {
+      const { caller, prisma } = createCaller()
+
+      await caller.create({
+        title: 'Amazing!! This runs perfectly!!',
+        systemId: SYSTEM_ID,
+        humanVerificationToken: 'verification-token',
+      })
+
+      expect(mockCheckSpamContent).toHaveBeenCalledWith({
+        prisma,
+        userId: USER_ID,
+        content: 'Amazing!! This runs perfectly!!',
+        entityType: 'game',
+        challengeMode: 'challenge',
+        enableRateLimiting: false,
+        enableDuplicateDetection: false,
+        humanVerificationToken: 'verification-token',
+        headers: expect.any(Headers),
+      })
+      expect(prisma.game.create).toHaveBeenCalledWith(
+        expect.objectContaining({
+          data: expect.not.objectContaining({ humanVerificationToken: expect.anything() }),
+        }),
+      )
+      expect(mockGameStatsCacheDelete).toHaveBeenCalled()
+    })
+  })
+})
diff --git a/src/server/api/routers/listings/comments.test.ts b/src/server/api/routers/listings/comments.test.ts
index 50ff89197..37060c3fd 100644
--- a/src/server/api/routers/listings/comments.test.ts
+++ b/src/server/api/routers/listings/comments.test.ts
@@ -243,4 +243,25 @@ describe('handheld comments router — create', () => {
     })
     expect(prisma.comment.create).toHaveBeenCalled()
   })
+
+  it('passes a human verification token to the spam check when retrying creation', async () => {
+    const { caller, prisma } = createCaller()
+
+    await caller.create({
+      listingId: LISTING_ID,
+      content: 'Amazing!! This runs perfectly!!',
+      humanVerificationToken: 'verification-token',
+    })
+
+    expect(mockCheckSpamContent).toHaveBeenCalledWith({
+      prisma,
+      userId: USER_ID,
+      content: 'Amazing!! This runs perfectly!!',
+      entityType: 'comment',
+      challengeMode: 'challenge',
+      humanVerificationToken: 'verification-token',
+      headers: expect.any(Headers),
+    })
+    expect(prisma.comment.create).toHaveBeenCalled()
+  })
 })
diff --git a/src/server/api/routers/listings/core.test.ts b/src/server/api/routers/listings/core.test.ts
index ec5f971ee..1d4717c3e 100644
--- a/src/server/api/routers/listings/core.test.ts
+++ b/src/server/api/routers/listings/core.test.ts
@@ -193,6 +193,35 @@ describe('handheld listings trust integration (core.ts)', () => {
       })
       expect(mockRepositoryCreate).toHaveBeenCalled()
     })
+
+    it('passes a human verification token to the spam check when retrying creation', async () => {
+      mockRepositoryCreate.mockResolvedValue({
+        id: LISTING_ID,
+        status: ApprovalStatus.PENDING,
+      })
+
+      const { caller } = createCaller({ permissions: [PERMISSIONS.CREATE_LISTING] })
+
+      await caller.create({
+        gameId: '00000000-0000-4000-a000-000000000030',
+        deviceId: '00000000-0000-4000-a000-000000000031',
+        emulatorId: '00000000-0000-4000-a000-000000000032',
+        performanceId: 1,
+        notes: 'Amazing!! This runs perfectly!!',
+        humanVerificationToken: 'verification-token',
+      })
+
+      expect(mockCheckSpamContent).toHaveBeenCalledWith({
+        prisma: expect.anything(),
+        userId: USER_ID,
+        content: 'Amazing!! This runs perfectly!!',
+        entityType: 'listing',
+        challengeMode: 'challenge',
+        humanVerificationToken: 'verification-token',
+        headers: expect.any(Headers),
+      })
+      expect(mockRepositoryCreate).toHaveBeenCalled()
+    })
   })
 
   describe('byId', () => {
diff --git a/src/server/api/routers/pcListings.test.ts b/src/server/api/routers/pcListings.test.ts
index 769d9a47f..eda2acc58 100644
--- a/src/server/api/routers/pcListings.test.ts
+++ b/src/server/api/routers/pcListings.test.ts
@@ -443,6 +443,28 @@ describe('pcListings trust integration', () => {
       })
       expect(prisma.pcListingComment.create).toHaveBeenCalled()
     })
+
+    it('passes a human verification token to the spam check when retrying comment creation', async () => {
+      const { caller, prisma } = createCaller()
+      prisma.pcListing.findUnique.mockResolvedValue({ id: LISTING_ID, authorId: AUTHOR_ID })
+
+      await caller.createComment({
+        pcListingId: LISTING_ID,
+        content: 'Amazing!! This runs perfectly!!',
+        humanVerificationToken: 'verification-token',
+      })
+
+      expect(mockCheckSpamContent).toHaveBeenCalledWith({
+        prisma,
+        userId: USER_ID,
+        content: 'Amazing!! This runs perfectly!!',
+        entityType: 'pcComment',
+        challengeMode: 'challenge',
+        humanVerificationToken: 'verification-token',
+        headers: expect.any(Headers),
+      })
+      expect(prisma.pcListingComment.create).toHaveBeenCalled()
+    })
   })
 
   describe('create', () => {
@@ -502,6 +524,39 @@ describe('pcListings trust integration', () => {
       })
       expect(mockRepositoryCreate).toHaveBeenCalled()
     })
+
+    it('passes a human verification token to the spam check when retrying creation', async () => {
+      const newListingId = '00000000-0000-4000-a000-000000000099'
+      mockRepositoryCreate.mockResolvedValue({
+        id: newListingId,
+        status: ApprovalStatus.PENDING,
+      })
+
+      const { caller } = createCaller({ permissions: [PERMISSIONS.CREATE_LISTING] })
+
+      await caller.create({
+        gameId: '00000000-0000-4000-a000-000000000030',
+        cpuId: '00000000-0000-4000-a000-000000000031',
+        emulatorId: '00000000-0000-4000-a000-000000000032',
+        performanceId: 1,
+        memorySize: 16,
+        os: PcOs.WINDOWS,
+        osVersion: '11',
+        notes: 'Amazing!! This runs perfectly!!',
+        humanVerificationToken: 'verification-token',
+      })
+
+      expect(mockCheckSpamContent).toHaveBeenCalledWith({
+        prisma: expect.anything(),
+        userId: USER_ID,
+        content: 'Amazing!! This runs perfectly!!',
+        entityType: 'pcListing',
+        challengeMode: 'challenge',
+        humanVerificationToken: 'verification-token',
+        headers: expect.any(Headers),
+      })
+      expect(mockRepositoryCreate).toHaveBeenCalled()
+    })
   })
 
   describe('byId', () => {
diff --git a/src/server/utils/spam-check.test.ts b/src/server/utils/spam-check.test.ts
index f57457ad6..05f5f61dd 100644
--- a/src/server/utils/spam-check.test.ts
+++ b/src/server/utils/spam-check.test.ts
@@ -4,7 +4,10 @@ import {
   isTurnstileConfigured,
   verifyTurnstileToken,
 } from '@/features/human-verification/server/providers/turnstile'
-import { HUMAN_VERIFICATION_ERROR_CODES } from '@/features/human-verification/shared/constants'
+import {
+  HUMAN_VERIFICATION_ERROR_CODES,
+  HUMAN_VERIFICATION_TOKEN_MAX_LENGTH,
+} from '@/features/human-verification/shared/constants'
 import analytics from '@/lib/analytics'
 import { type PrismaClient } from '@orm/client'
 import { checkSpamContent } from './spam-check'
@@ -241,6 +244,36 @@ describe('checkSpamContent', () => {
     })
   })
 
+  it('rejects oversized human verification tokens before calling the provider', async () => {
+    vi.spyOn(SpamDetectionService.prototype, 'detectSpam').mockResolvedValue({
+      isSpam: true,
+      confidence: 0.85,
+      method: 'pattern_matching',
+      reason: 'Spam pattern matched',
+    })
+
+    try {
+      await checkSpamContent({
+        prisma: mockPrisma,
+        userId: USER_ID,
+        content: 'Flagged content',
+        entityType: 'comment',
+        challengeMode: 'challenge',
+        humanVerificationToken: 'a'.repeat(HUMAN_VERIFICATION_TOKEN_MAX_LENGTH + 1),
+      })
+      throw new Error('Expected checkSpamContent to throw')
+    } catch (error) {
+      expect(error).toBeInstanceOf(TRPCError)
+      expect((error as TRPCError).cause).toEqual(
+        expect.objectContaining({
+          code: HUMAN_VERIFICATION_ERROR_CODES.FAILED,
+        }),
+      )
+    }
+
+    expect(verifyTurnstileToken).not.toHaveBeenCalled()
+  })
+
   it('does not allow high-confidence spam with a human verification token', async () => {
     vi.spyOn(SpamDetectionService.prototype, 'detectSpam').mockResolvedValue({
       isSpam: true,

From 91cd3dce98bf9168f9ad3fe16ec5176444a0945f Mon Sep 17 00:00:00 2001
From: Producdevity <y.gherbi.dev@gmail.com>
Date: Mon, 1 Jun 2026 19:52:10 +0200
Subject: [PATCH 33/35] refactor: remove redundant comments from
 CommunitySupportBanner component

---
 src/components/banners/CommunitySupportBanner.tsx | 4 ----
 1 file changed, 4 deletions(-)

diff --git a/src/components/banners/CommunitySupportBanner.tsx b/src/components/banners/CommunitySupportBanner.tsx
index 7c1fa9313..c5672b4bf 100644
--- a/src/components/banners/CommunitySupportBanner.tsx
+++ b/src/components/banners/CommunitySupportBanner.tsx
@@ -61,7 +61,6 @@ function CommunitySupportBanner(props: Props) {
               isHome ? 'md:gap-x-4' : '',
             )}
           >
-            {/* Icon */}
             <div
               className={cn(
                 'flex-shrink-0 rounded-lg bg-gradient-to-br from-pink-100 to-rose-100 dark:from-pink-950/30 dark:to-rose-950/30',
@@ -74,7 +73,6 @@ function CommunitySupportBanner(props: Props) {
               />
             </div>
 
-            {/* Text — min-width ensures icon + text + dismiss fill the first row, forcing CTA to wrap */}
             <p
               className={cn(
                 'flex-1 text-gray-700 dark:text-gray-200 leading-snug',
@@ -86,7 +84,6 @@ function CommunitySupportBanner(props: Props) {
               keeping it online isn&apos;t. Patreon supporters help with hosting and bandwidth.
             </p>
 
-            {/* CTA — wraps to second line on mobile via order-1, inline on desktop */}
             <a
               href={env.PATREON_URL}
               target="_blank"
@@ -106,7 +103,6 @@ function CommunitySupportBanner(props: Props) {
               Support Us
             </a>
 
-            {/* Dismiss — stays in first row (default order 0, same as icon + text) */}
             <button
               type="button"
               onClick={dismiss}

From fb51cfaba91812e5067e26f951917a3d22badac5 Mon Sep 17 00:00:00 2001
From: Producdevity <y.gherbi.dev@gmail.com>
Date: Mon, 1 Jun 2026 19:57:40 +0200
Subject: [PATCH 34/35] refactor: add noinspection directive to spam-check test
 file

---
 src/server/utils/spam-check.test.ts | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/server/utils/spam-check.test.ts b/src/server/utils/spam-check.test.ts
index 05f5f61dd..f028ec33f 100644
--- a/src/server/utils/spam-check.test.ts
+++ b/src/server/utils/spam-check.test.ts
@@ -1,3 +1,5 @@
+// noinspection ExceptionCaughtLocallyJS
+
 import { TRPCError } from '@trpc/server'
 import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
 import {

From 75f19a6bf274f4e7528c5af8e08f005379616bfc Mon Sep 17 00:00:00 2001
From: Producdevity <y.gherbi.dev@gmail.com>
Date: Tue, 2 Jun 2026 13:05:55 +0200
Subject: [PATCH 35/35] chore: use pnpm in git hooks

---
 .husky/pre-commit | 2 +-
 .husky/pre-push   | 6 +++---
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/.husky/pre-commit b/.husky/pre-commit
index 5c3e95f02..5ee7abd87 100644
--- a/.husky/pre-commit
+++ b/.husky/pre-commit
@@ -1 +1 @@
-npm exec lint-staged
+pnpm exec lint-staged
diff --git a/.husky/pre-push b/.husky/pre-push
index 66688b858..318004d3a 100644
--- a/.husky/pre-push
+++ b/.husky/pre-push
@@ -1,3 +1,3 @@
-npm run lint
-npm run types
-npm run test
+pnpm lint
+pnpm types
+pnpm test
