Repository: IgnisDa/ryot (language: rust)

Issue:
Remove dependency on `zodix`
The library has not been updated for 2 years. Moreover, it has a dependency on `@remix-run/react` which hinders upgrading to `react-router@v7`.


Discussion / hints:

Fix patch:
diff --git a/Cargo.lock b/Cargo.lock
index 985ab34dba..25ef85eb46 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -303,9 +303,9 @@ dependencies = [
 
 [[package]]
 name = "async-graphql"
-version = "7.0.13"
+version = "7.0.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "59fd6bd734afb8b6e4d0f84a3e77305ce0a7ccc60d70f6001cb5e1c3f38d8ff1"
+checksum = "e0a9916334e00a14428e03b9b1c73a0baf1c834ebe0ff1be146e1247390b31f1"
 dependencies = [
  "async-graphql-derive",
  "async-graphql-parser",
@@ -342,12 +342,11 @@ dependencies = [
 
 [[package]]
 name = "async-graphql-axum"
-version = "7.0.13"
+version = "7.0.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec8c1bb47161c37286e40e2fa58055e97b2a2b6cf1022a6686967e10636fa5d7"
+checksum = "0344482a065904d2595377810e0bb35ad445ca4712b459757436303251d81b65"
 dependencies = [
  "async-graphql",
- "async-trait",
  "axum",
  "bytes",
  "futures-util",
@@ -360,9 +359,9 @@ dependencies = [
 
 [[package]]
 name = "async-graphql-derive"
-version = "7.0.13"
+version = "7.0.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac38b4dd452d529d6c0248b51df23603f0a875770352e26ae8c346ce6c149b3e"
+checksum = "88d6f3ad293f7b9974aef6297673ac3e9097824f84264a9548cbb87006c94044"
 dependencies = [
  "Inflector",
  "async-graphql-parser",
@@ -377,9 +376,9 @@ dependencies = [
 
 [[package]]
 name = "async-graphql-parser"
-version = "7.0.13"
+version = "7.0.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "42d271ddda2f55b13970928abbcbc3423cfc18187c60e8769b48f21a93b7adaa"
+checksum = "7ca5697e57fcad289d26948e2ab2f11b9cfe7d645503a1f37fa86640c061c772"
 dependencies = [
  "async-graphql-value",
  "pest",
@@ -389,9 +388,9 @@ dependencies = [
 
 [[package]]
 name = "async-graphql-value"
-version = "7.0.13"
+version = "7.0.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aefe909173a037eaf3281b046dc22580b59a38b765d7b8d5116f2ffef098048d"
+checksum = "6266ea7ab3ce41585e16caa0e1e8d97de37827227950820fdab6b69d9c09a63a"
 dependencies = [
  "bytes",
  "indexmap 2.7.0",
@@ -722,15 +721,15 @@ dependencies = [
 
 [[package]]
 name = "axum"
-version = "0.7.9"
+version = "0.8.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
+checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8"
 dependencies = [
- "async-trait",
  "axum-core",
  "axum-macros",
  "base64 0.22.1",
  "bytes",
+ "form_urlencoded",
  "futures-util",
  "http 1.2.0",
  "http-body 1.0.1",
@@ -761,11 +760,10 @@ dependencies = [
 
 [[package]]
 name = "axum-core"
-version = "0.4.5"
+version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199"
+checksum = "df1362f362fd16024ae199c1970ce98f9661bf5ef94b9808fee734bc3698b733"
 dependencies = [
- "async-trait",
  "bytes",
  "futures-util",
  "http 1.2.0",
@@ -782,9 +780,9 @@ dependencies = [
 
 [[package]]
 name = "axum-macros"
-version = "0.4.2"
+version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce"
+checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -3688,9 +3686,9 @@ dependencies = [
 
 [[package]]
 name = "matchit"
-version = "0.7.3"
+version = "0.8.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
+checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
 
 [[package]]
 name = "md-5"
@@ -6623,9 +6621,9 @@ dependencies = [
 
 [[package]]
 name = "tokio-tungstenite"
-version = "0.24.0"
+version = "0.26.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9"
+checksum = "be4bf6fecd69fcdede0ec680aaf474cdab988f9de6bc73d3758f0160e3b7025a"
 dependencies = [
  "futures-util",
  "log",
@@ -6828,9 +6826,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
 
 [[package]]
 name = "tungstenite"
-version = "0.24.0"
+version = "0.26.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a"
+checksum = "413083a99c579593656008130e29255e54dcaae495be556cc26888f211648c24"
 dependencies = [
  "byteorder",
  "bytes",
@@ -6840,7 +6838,7 @@ dependencies = [
  "log",
  "rand 0.8.5",
  "sha1",
- "thiserror 1.0.69",
+ "thiserror 2.0.9",
  "utf-8",
 ]
 
diff --git a/Cargo.toml b/Cargo.toml
index 2befffbb5f..a5c0053458 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -52,18 +52,17 @@ anyhow = "=1.0.95"
 apalis = { version = "=0.6.2", features = ["catch-panic", "limit", "retry"] }
 apalis-cron = "=0.6.2"
 argon2 = "=0.6.0-pre.1"
-async-graphql = { version = "=7.0.13", features = [
+async-graphql = { version = "=7.0.14", features = [
     "chrono",
     "decimal",
     "log",
     "tracing",
     "uuid",
 ] }
-async-graphql-axum = "=7.0.13"
+async-graphql-axum = "=7.0.14"
 async-trait = "=0.1.83"
 aws-sdk-s3 = { version = "=1.67.0", features = ["behavior-version-latest"] }
-# TODO: Update when https://github.com/async-graphql/async-graphql/pull/1653 is merged
-axum = { version = "=0.7.9", features = ["macros", "multipart"] }
+axum = { version = "=0.8.1", features = ["macros", "multipart"] }
 boilermates = "=0.3.0"
 chrono = "=0.4.39"
 chrono-tz = "=0.10.0"
diff --git a/README.md b/README.md
index 7d92559c95..3724d0328e 100644
--- a/README.md
+++ b/README.md
@@ -76,7 +76,7 @@ You can also open an issue on GitHub if you find any bugs or have feature reques
 - ✅ Sends notifications to Discord, Ntfy, Apprise etc
 - ✅ Self-hosted
 - ✅ PWA enabled
-- ✅ Documented GraphQL API
+- ✅ [Documented](https://app.ryot.io/backend/graphql) GraphQL API
 - ✅ Easy to understand UI
 - ✅ Lightning fast (written in Rust BTW)
 - ✅ Free and open-source
diff --git a/apps/backend/src/common.rs b/apps/backend/src/common.rs
index ddceada932..c5ac2b0ed5 100644
--- a/apps/backend/src/common.rs
+++ b/apps/backend/src/common.rs
@@ -122,8 +122,10 @@ pub async fn create_app_services(
         .allow_origin(cors_origins)
         .allow_credentials(true);
 
-    let webhook_routes =
-        Router::new().route("/integrations/:integration_slug", post(integration_webhook));
+    let webhook_routes = Router::new().route(
+        "/integrations/{integration_slug}",
+        post(integration_webhook),
+    );
 
     join_all(
         [
diff --git a/apps/backend/src/job.rs b/apps/backend/src/job.rs
index 873998c294..b51173396f 100644
--- a/apps/backend/src/job.rs
+++ b/apps/backend/src/job.rs
@@ -173,12 +173,6 @@ pub async fn perform_lp_application_job(
                 .handle_on_seen_complete(id)
                 .await
         }
-        LpApplicationJob::DeleteAllApplicationCache => {
-            app_services
-                .miscellaneous_service
-                .delete_all_application_cache()
-                .await
-        }
         LpApplicationJob::UpdateUserLastActivityPerformed(user_id, timestamp) => {
             app_services
                 .miscellaneous_service
diff --git a/apps/frontend/app/lib/generals.tsx b/apps/frontend/app/lib/generals.tsx
index e99d5c53ae..18cec2b8fb 100644
--- a/apps/frontend/app/lib/generals.tsx
+++ b/apps/frontend/app/lib/generals.tsx
@@ -57,17 +57,17 @@ dayjs.extend(localizedFormat);
 
 export { dayjs as dayjsLib };
 
-export const commaDelimitedString = z
+export const zodCommaDelimitedString = z
 	.string()
 	.optional()
 	.transform((v) => (isString(v) ? v.split(",") : undefined));
 
-export const emptyNumberString = z
+export const zodEmptyNumberString = z
 	.any()
 	.transform((v) => (!v ? undefined : Number.parseInt(v)))
 	.nullable();
 
-export const emptyDecimalString = z
+export const zodEmptyDecimalString = z
 	.any()
 	.transform((v) => (!v ? undefined : Number.parseFloat(v).toString()))
 	.nullable();
diff --git a/apps/frontend/app/lib/utilities.server.ts b/apps/frontend/app/lib/utilities.server.ts
index 6b3c33829a..e59b8bb7da 100644
--- a/apps/frontend/app/lib/utilities.server.ts
+++ b/apps/frontend/app/lib/utilities.server.ts
@@ -31,11 +31,11 @@ import { v4 as randomUUID } from "uuid";
 import { z } from "zod";
 import {
 	FRONTEND_AUTH_COOKIE_NAME,
-	emptyDecimalString,
-	emptyNumberString,
 	pageQueryParam,
 	redirectToQueryParam,
 	toastKey,
+	zodEmptyDecimalString,
+	zodEmptyNumberString,
 } from "~/lib/generals";
 
 export const API_URL = process.env.API_URL || "http://127.0.0.1:8000/backend";
@@ -155,12 +155,12 @@ export const combineHeaders = (
 export const MetadataIdSchema = z.object({ metadataId: z.string() });
 
 export const MetadataSpecificsSchema = z.object({
-	showSeasonNumber: emptyNumberString,
-	showEpisodeNumber: emptyNumberString,
-	podcastEpisodeNumber: emptyNumberString,
-	animeEpisodeNumber: emptyNumberString,
-	mangaChapterNumber: emptyDecimalString,
-	mangaVolumeNumber: emptyNumberString,
+	showSeasonNumber: zodEmptyNumberString,
+	showEpisodeNumber: zodEmptyNumberString,
+	podcastEpisodeNumber: zodEmptyNumberString,
+	animeEpisodeNumber: zodEmptyNumberString,
+	mangaChapterNumber: zodEmptyDecimalString,
+	mangaVolumeNumber: zodEmptyNumberString,
 });
 
 export const getDecodedJwt = (request: Request) => {
diff --git a/apps/frontend/app/routes/_dashboard._index.tsx b/apps/frontend/app/routes/_dashboard._index.tsx
index 8b8de96f02..0f9343f4b2 100644
--- a/apps/frontend/app/routes/_dashboard._index.tsx
+++ b/apps/frontend/app/routes/_dashboard._index.tsx
@@ -29,6 +29,7 @@ import {
 	IconInfoCircle,
 	IconRotateClockwise,
 } from "@tabler/icons-react";
+import { useMutation } from "@tanstack/react-query";
 import CryptoJS from "crypto-js";
 import type { ReactNode } from "react";
 import { ClientOnly } from "remix-utils/client-only";
@@ -139,6 +140,14 @@ export default function Page() {
 		"false",
 	);
 
+	const refreshUserMetadataRecommendationsMutation = useMutation({
+		onSuccess: revalidator.revalidate,
+		mutationFn: async () =>
+			clientGqlService.request(UserMetadataRecommendationsDocument, {
+				shouldRefresh: true,
+			}),
+	});
+
 	const isDashboardEmpty =
 		loaderData.userUpcomingCalendarEvents.length +
 			loaderData.inProgressCollectionContents.results.items.length +
@@ -203,41 +212,39 @@ export default function Page() {
 								</Section>
 							) : null,
 						)
-						.with([DashboardElementLot.Recommendations, false], ([v, _]) =>
-							loaderData.userRecommendations.length > 0 ? (
-								<Section key={v} lot={v}>
-									<Group justify="space-between">
-										<SectionTitle text="Recommendations" />
-										<ActionIcon
-											variant="subtle"
-											onClick={() => {
-												openConfirmationModal(
-													"Are you sure you want to refresh the recommendations?",
-													async () => {
-														await clientGqlService.request(
-															UserMetadataRecommendationsDocument,
-															{ shouldRefresh: true },
-														);
-														revalidator.revalidate();
-													},
-												);
-											}}
-										>
-											<IconRotateClockwise />
-										</ActionIcon>
-									</Group>
-									{coreDetails.isServerKeyValidated ? (
-										<ApplicationGrid>
-											{loaderData.userRecommendations.map((lm) => (
-												<MetadataDisplayItem key={lm} metadataId={lm} />
-											))}
-										</ApplicationGrid>
-									) : (
-										<ProRequiredAlert tooltipLabel="Get new recommendations every hour" />
-									)}
-								</Section>
-							) : null,
-						)
+						.with([DashboardElementLot.Recommendations, false], ([v, _]) => (
+							<Section key={v} lot={v}>
+								<Group justify="space-between">
+									<SectionTitle text="Recommendations" />
+									<ActionIcon
+										variant="subtle"
+										loading={
+											refreshUserMetadataRecommendationsMutation.isPending
+										}
+										onClick={() => {
+											openConfirmationModal(
+												"Are you sure you want to refresh the recommendations?",
+												refreshUserMetadataRecommendationsMutation.mutate,
+											);
+										}}
+									>
+										<IconRotateClockwise />
+									</ActionIcon>
+								</Group>
+								{coreDetails.isServerKeyValidated ? (
+									<ApplicationGrid>
+										{loaderData.userRecommendations.map((lm) => (
+											<MetadataDisplayItem key={lm} metadataId={lm} />
+										))}
+									</ApplicationGrid>
+								) : (
+									<ProRequiredAlert tooltipLabel="Get new recommendations every hour" />
+								)}
+								{loaderData.userRecommendations.length === 0 ? (
+									<Text c="dimmed">No recommendations available</Text>
+								) : null}
+							</Section>
+						))
 						.with([DashboardElementLot.Summary, false], ([v, _]) =>
 							latestUserSummary ? (
 								<Section key={v} lot={v}>
diff --git a/apps/frontend/app/routes/_dashboard.calendar.tsx b/apps/frontend/app/routes/_dashboard.calendar.tsx
index c43a79df30..51d040af6b 100644
--- a/apps/frontend/app/routes/_dashboard.calendar.tsx
+++ b/apps/frontend/app/routes/_dashboard.calendar.tsx
@@ -15,12 +15,11 @@ import {
 	UserCalendarEventsDocument,
 	type UserCalendarEventsQuery,
 } from "@ryot/generated/graphql/backend/graphql";
-import { sum } from "@ryot/ts-utils";
+import { parseSearchQuery, sum } from "@ryot/ts-utils";
 import { IconChevronLeft, IconChevronRight } from "@tabler/icons-react";
 import { Fragment } from "react/jsx-runtime";
 import { match } from "ts-pattern";
 import { z } from "zod";
-import { zx } from "zodix";
 import { ApplicationGrid } from "~/components/common";
 import { MetadataDisplayItem } from "~/components/media";
 import { dayjsLib } from "~/lib/generals";
@@ -40,7 +39,7 @@ export type SearchParams = z.infer<typeof searchParamsSchema>;
 export const loader = async ({ request }: LoaderFunctionArgs) => {
 	const cookieName = await getEnhancedCookieName("calendar", request);
 	await redirectUsingEnhancedCookieSearchParams(request, cookieName);
-	const query = zx.parseQuery(request, searchParamsSchema);
+	const query = parseSearchQuery(request, searchParamsSchema);
 	const date = dayjsLib(query.date);
 	const [{ userCalendarEvents }] = await Promise.all([
 		serverGqlService.authenticatedRequest(request, UserCalendarEventsDocument, {
diff --git a/apps/frontend/app/routes/_dashboard.collections.$id._index.tsx b/apps/frontend/app/routes/_dashboard.collections.$id._index.tsx
index aee883ac1e..ff24402e59 100644
--- a/apps/frontend/app/routes/_dashboard.collections.$id._index.tsx
+++ b/apps/frontend/app/routes/_dashboard.collections.$id._index.tsx
@@ -24,7 +24,12 @@ import {
 	GraphqlSortOrder,
 	MediaLot,
 } from "@ryot/generated/graphql/backend/graphql";
-import { startCase } from "@ryot/ts-utils";
+import {
+	parseParameters,
+	parseSearchQuery,
+	startCase,
+	zodIntAsString,
+} from "@ryot/ts-utils";
 import {
 	IconBucketDroplet,
 	IconFilter,
@@ -37,7 +42,6 @@ import {
 import { useState } from "react";
 import { $path } from "remix-routes";
 import { z } from "zod";
-import { zx } from "zodix";
 import {
 	ApplicationGrid,
 	DebouncedSearchInput,
@@ -64,27 +68,30 @@ const defaultFiltersValue = {
 };
 
 const searchParamsSchema = z.object({
-	defaultTab: z.string().optional(),
-	[pageQueryParam]: zx.IntAsString.optional(),
 	query: z.string().optional(),
+	defaultTab: z.string().optional(),
+	[pageQueryParam]: zodIntAsString.optional(),
+	entityLot: z.nativeEnum(EntityLot).optional(),
+	metadataLot: z.nativeEnum(MediaLot).optional(),
+	orderBy: z.nativeEnum(GraphqlSortOrder).default(defaultFiltersValue.order),
 	sortBy: z
 		.nativeEnum(CollectionContentsSortBy)
 		.default(defaultFiltersValue.sort),
-	orderBy: z.nativeEnum(GraphqlSortOrder).default(defaultFiltersValue.order),
-	entityLot: z.nativeEnum(EntityLot).optional(),
-	metadataLot: z.nativeEnum(MediaLot).optional(),
 });
 
 export type SearchParams = z.infer<typeof searchParamsSchema>;
 
 export const loader = async ({ request, params }: LoaderFunctionArgs) => {
-	const { id: collectionId } = zx.parseParams(params, { id: z.string() });
+	const { id: collectionId } = parseParameters(
+		params,
+		z.object({ id: z.string() }),
+	);
 	const cookieName = await getEnhancedCookieName(
 		`collections.details.${collectionId}`,
 		request,
 	);
 	await redirectUsingEnhancedCookieSearchParams(request, cookieName);
-	const query = zx.parseQuery(request, searchParamsSchema);
+	const query = parseSearchQuery(request, searchParamsSchema);
 	const [{ collectionContents }] = await Promise.all([
 		serverGqlService.authenticatedRequest(request, CollectionContentsDocument, {
 			input: {
diff --git a/apps/frontend/app/routes/_dashboard.collections.list.tsx b/apps/frontend/app/routes/_dashboard.collections.list.tsx
index e283933bf2..308a216309 100644
--- a/apps/frontend/app/routes/_dashboard.collections.list.tsx
+++ b/apps/frontend/app/routes/_dashboard.collections.list.tsx
@@ -53,6 +53,7 @@ import {
 	isString,
 	processSubmission,
 	truncate,
+	zodCheckboxAsString,
 } from "@ryot/ts-utils";
 import {
 	IconEdit,
@@ -68,16 +69,15 @@ import { $path } from "remix-routes";
 import { match } from "ts-pattern";
 import { withQuery } from "ufo";
 import { z } from "zod";
-import { zx } from "zodix";
 import { DebouncedSearchInput, ProRequiredAlert } from "~/components/common";
 import {
 	PRO_REQUIRED_MESSAGE,
 	clientGqlService,
-	commaDelimitedString,
 	getPartialMetadataDetailsQuery,
 	openConfirmationModal,
 	queryClient,
 	queryFactory,
+	zodCommaDelimitedString,
 } from "~/lib/generals";
 import {
 	useConfirmSubmit,
@@ -178,15 +178,15 @@ const createOrUpdateSchema = z.object({
 	name: z.string(),
 	updateId: z.string().optional(),
 	description: z.string().optional(),
-	collaborators: commaDelimitedString,
+	collaborators: zodCommaDelimitedString,
 	informationTemplate: z
 		.array(
 			z.object({
 				name: z.string(),
 				description: z.string(),
-				lot: z.nativeEnum(CollectionExtraInformationLot),
 				defaultValue: z.string().optional(),
-				required: zx.CheckboxAsString.optional(),
+				required: zodCheckboxAsString.optional(),
+				lot: z.nativeEnum(CollectionExtraInformationLot),
 			}),
 		)
 		.optional(),
diff --git a/apps/frontend/app/routes/_dashboard.fitness.$action.tsx b/apps/frontend/app/routes/_dashboard.fitness.$action.tsx
index ffcf0a35fc..465bf254f4 100644
--- a/apps/frontend/app/routes/_dashboard.fitness.$action.tsx
+++ b/apps/frontend/app/routes/_dashboard.fitness.$action.tsx
@@ -61,6 +61,7 @@ import {
 	isEqual,
 	isNumber,
 	isString,
+	parseParameters,
 	snakeCase,
 	sortBy,
 	startCase,
@@ -102,7 +103,6 @@ import { match } from "ts-pattern";
 import { useInterval, useOnClickOutside } from "usehooks-ts";
 import { v4 as randomUUID } from "uuid";
 import { z } from "zod";
-import { zx } from "zodix";
 import {
 	DisplaySetStatistics,
 	ExerciseHistory,
@@ -154,9 +154,10 @@ import {
 const DEFAULT_SET_TIMEOUT_DELAY = 800;
 
 export const loader = async ({ params }: LoaderFunctionArgs) => {
-	const { action } = zx.parseParams(params, {
-		action: z.nativeEnum(FitnessAction),
-	});
+	const { action } = parseParameters(
+		params,
+		z.object({ action: z.nativeEnum(FitnessAction) }),
+	);
 	return {
 		action,
 		isUpdatingWorkout: action === FitnessAction.UpdateWorkout,
diff --git a/apps/frontend/app/routes/_dashboard.fitness.$entity.$id._index.tsx b/apps/frontend/app/routes/_dashboard.fitness.$entity.$id._index.tsx
index 5094e56712..cd739188e0 100644
--- a/apps/frontend/app/routes/_dashboard.fitness.$entity.$id._index.tsx
+++ b/apps/frontend/app/routes/_dashboard.fitness.$entity.$id._index.tsx
@@ -34,6 +34,7 @@ import {
 	changeCase,
 	getActionIntent,
 	humanizeDuration,
+	parseParameters,
 	processSubmission,
 } from "@ryot/ts-utils";
 import {
@@ -58,7 +59,6 @@ import { $path } from "remix-routes";
 import { match } from "ts-pattern";
 import { withQuery } from "ufo";
 import { z } from "zod";
-import { zx } from "zodix";
 import { DisplayCollection } from "~/components/common";
 import {
 	ExerciseHistory,
@@ -88,10 +88,10 @@ import {
 } from "~/lib/utilities.server";
 
 export const loader = async ({ request, params }: LoaderFunctionArgs) => {
-	const { id: entityId, entity } = zx.parseParams(params, {
-		id: z.string(),
-		entity: z.nativeEnum(FitnessEntity),
-	});
+	const { id: entityId, entity } = parseParameters(
+		params,
+		z.object({ id: z.string(), entity: z.nativeEnum(FitnessEntity) }),
+	);
 	const resp = await match(entity)
 		.with(FitnessEntity.Workouts, async () => {
 			const [{ userWorkoutDetails }] = await Promise.all([
diff --git a/apps/frontend/app/routes/_dashboard.fitness.$entity.list.tsx b/apps/frontend/app/routes/_dashboard.fitness.$entity.list.tsx
index b3e0c8d51b..5139712a8c 100644
--- a/apps/frontend/app/routes/_dashboard.fitness.$entity.list.tsx
+++ b/apps/frontend/app/routes/_dashboard.fitness.$entity.list.tsx
@@ -26,7 +26,14 @@ import {
 	UserWorkoutsListDocument,
 	type WorkoutSummary,
 } from "@ryot/generated/graphql/backend/graphql";
-import { changeCase, humanizeDuration, truncate } from "@ryot/ts-utils";
+import {
+	changeCase,
+	humanizeDuration,
+	parseParameters,
+	parseSearchQuery,
+	truncate,
+	zodIntAsString,
+} from "@ryot/ts-utils";
 import {
 	IconChevronDown,
 	IconChevronUp,
@@ -42,7 +49,6 @@ import { $path } from "remix-routes";
 import invariant from "tiny-invariant";
 import { match } from "ts-pattern";
 import { z } from "zod";
-import { zx } from "zodix";
 import { DebouncedSearchInput } from "~/components/common";
 import {
 	displayDistanceWithUnit,
@@ -75,19 +81,20 @@ import {
 } from "~/lib/utilities.server";
 
 const searchParamsSchema = z.object({
-	[pageQueryParam]: zx.IntAsString.default("1"),
 	query: z.string().optional(),
+	[pageQueryParam]: zodIntAsString.default("1"),
 });
 
 export type SearchParams = z.infer<typeof searchParamsSchema>;
 
 export const loader = async ({ params, request }: LoaderFunctionArgs) => {
-	const { entity } = zx.parseParams(params, {
-		entity: z.nativeEnum(FitnessEntity),
-	});
+	const { entity } = parseParameters(
+		params,
+		z.object({ entity: z.nativeEnum(FitnessEntity) }),
+	);
 	const cookieName = await getEnhancedCookieName(`${entity}.list`, request);
 	await redirectUsingEnhancedCookieSearchParams(request, cookieName);
-	const query = zx.parseQuery(request, searchParamsSchema);
+	const query = parseSearchQuery(request, searchParamsSchema);
 	const itemList = await match(entity)
 		.with(FitnessEntity.Workouts, async () => {
 			const { userWorkoutsList } = await serverGqlService.authenticatedRequest(
diff --git a/apps/frontend/app/routes/_dashboard.fitness.exercises.$action.tsx b/apps/frontend/app/routes/_dashboard.fitness.exercises.$action.tsx
index 4242b20d59..4d8fd728e2 100644
--- a/apps/frontend/app/routes/_dashboard.fitness.exercises.$action.tsx
+++ b/apps/frontend/app/routes/_dashboard.fitness.exercises.$action.tsx
@@ -33,8 +33,11 @@ import {
 import {
 	cloneDeep,
 	getActionIntent,
+	parseParameters,
+	parseSearchQuery,
 	processSubmission,
 	startCase,
+	zodBoolAsString,
 } from "@ryot/ts-utils";
 import { IconPhoto } from "@tabler/icons-react";
 import { ClientError } from "graphql-request";
@@ -43,7 +46,6 @@ import invariant from "tiny-invariant";
 import { match } from "ts-pattern";
 import { withQuery } from "ufo";
 import { z } from "zod";
-import { zx } from "zodix";
 import { getExerciseDetailsPath } from "~/lib/generals";
 import { useCoreDetails } from "~/lib/hooks";
 import {
@@ -62,8 +64,11 @@ enum Action {
 }
 
 export const loader = async ({ params, request }: LoaderFunctionArgs) => {
-	const { action } = zx.parseParams(params, { action: z.nativeEnum(Action) });
-	const query = zx.parseQuery(request, searchParamsSchema);
+	const { action } = parseParameters(
+		params,
+		z.object({ action: z.nativeEnum(Action) }),
+	);
+	const query = parseSearchQuery(request, searchParamsSchema);
 	const details = await match(action)
 		.with(Action.Create, () => undefined)
 		.with(Action.Update, async () => {
@@ -157,7 +162,7 @@ const schema = z.object({
 	images: optionalStringArray,
 	instructions: optionalString,
 	lot: z.nativeEnum(ExerciseLot),
-	shouldDelete: zx.BoolAsString.optional(),
+	shouldDelete: zodBoolAsString.optional(),
 	level: z.nativeEnum(ExerciseLevel),
 	force: z.nativeEnum(ExerciseForce).optional(),
 	mechanic: z.nativeEnum(ExerciseMechanic).optional(),
diff --git a/apps/frontend/app/routes/_dashboard.fitness.exercises.item.$id._index.tsx b/apps/frontend/app/routes/_dashboard.fitness.exercises.item.$id._index.tsx
index cb91227831..2f995428aa 100644
--- a/apps/frontend/app/routes/_dashboard.fitness.exercises.item.$id._index.tsx
+++ b/apps/frontend/app/routes/_dashboard.fitness.exercises.item.$id._index.tsx
@@ -44,6 +44,8 @@ import {
 import {
 	changeCase,
 	isNumber,
+	parseParameters,
+	parseSearchQuery,
 	snakeCase,
 	sortBy,
 	startCase,
@@ -70,7 +72,6 @@ import { match } from "ts-pattern";
 import { withFragment } from "ufo";
 import { useLocalStorage } from "usehooks-ts";
 import { z } from "zod";
-import { zx } from "zodix";
 import { DisplayCollection, ReviewItemDisplay } from "~/components/common";
 import {
 	ExerciseHistory,
@@ -107,11 +108,12 @@ const searchParamsSchema = z.object({
 
 export type SearchParams = z.infer<typeof searchParamsSchema>;
 
-const paramsSchema = { id: z.string() };
-
 export const loader = async ({ params, request }: LoaderFunctionArgs) => {
-	const { id: exerciseId } = zx.parseParams(params, paramsSchema);
-	const query = zx.parseQuery(request, searchParamsSchema);
+	const { id: exerciseId } = parseParameters(
+		params,
+		z.object({ id: z.string() }),
+	);
+	const query = parseSearchQuery(request, searchParamsSchema);
 	const [{ exerciseDetails }, { userExerciseDetails }] = await Promise.all([
 		serverGqlService.request(ExerciseDetailsDocument, { exerciseId }),
 		serverGqlService.authenticatedRequest(
diff --git a/apps/frontend/app/routes/_dashboard.fitness.exercises.list.tsx b/apps/frontend/app/routes/_dashboard.fitness.exercises.list.tsx
index 1202f29bc3..fd96a0fff1 100644
--- a/apps/frontend/app/routes/_dashboard.fitness.exercises.list.tsx
+++ b/apps/frontend/app/routes/_dashboard.fitness.exercises.list.tsx
@@ -47,9 +47,11 @@ import {
 import {
 	getActionIntent,
 	isNumber,
+	parseSearchQuery,
 	processSubmission,
 	snakeCase,
 	startCase,
+	zodIntAsString,
 } from "@ryot/ts-utils";
 import {
 	IconAlertCircle,
@@ -63,7 +65,6 @@ import { $path } from "remix-routes";
 import { match } from "ts-pattern";
 import { withQuery } from "ufo";
 import { z } from "zod";
-import { zx } from "zodix";
 import { DebouncedSearchInput, FiltersModal } from "~/components/common";
 import {
 	dayjsLib,
@@ -105,16 +106,16 @@ const defaultFiltersValue = {
 };
 
 const searchParamsSchema = z.object({
-	[pageQueryParam]: zx.IntAsString.optional(),
 	query: z.string().optional(),
-	sortBy: z.nativeEnum(ExerciseSortBy).optional(),
+	collection: z.string().optional(),
+	[pageQueryParam]: zodIntAsString.optional(),
 	type: z.nativeEnum(ExerciseLot).optional(),
 	level: z.nativeEnum(ExerciseLevel).optional(),
 	force: z.nativeEnum(ExerciseForce).optional(),
+	sortBy: z.nativeEnum(ExerciseSortBy).optional(),
+	muscle: z.nativeEnum(ExerciseMuscle).optional(),
 	mechanic: z.nativeEnum(ExerciseMechanic).optional(),
 	equipment: z.nativeEnum(ExerciseEquipment).optional(),
-	muscle: z.nativeEnum(ExerciseMuscle).optional(),
-	collection: z.string().optional(),
 });
 
 export type SearchParams = z.infer<typeof searchParamsSchema>;
@@ -122,7 +123,7 @@ export type SearchParams = z.infer<typeof searchParamsSchema>;
 export const loader = async ({ request }: LoaderFunctionArgs) => {
 	const cookieName = await getEnhancedCookieName("exercises.list", request);
 	await redirectUsingEnhancedCookieSearchParams(request, cookieName);
-	const query = zx.parseQuery(request, searchParamsSchema);
+	const query = parseSearchQuery(request, searchParamsSchema);
 	query.sortBy = query.sortBy ?? defaultFiltersValue.sortBy;
 	query[pageQueryParam] = query[pageQueryParam] ?? 1;
 	const [{ exercisesList }] = await Promise.all([
diff --git a/apps/frontend/app/routes/_dashboard.fitness.measurements.list.tsx b/apps/frontend/app/routes/_dashboard.fitness.measurements.list.tsx
index 5a86603540..8d6bd77a5b 100644
--- a/apps/frontend/app/routes/_dashboard.fitness.measurements.list.tsx
+++ b/apps/frontend/app/routes/_dashboard.fitness.measurements.list.tsx
@@ -22,7 +22,12 @@ import {
 	DeleteUserMeasurementDocument,
 	UserMeasurementsListDocument,
 } from "@ryot/generated/graphql/backend/graphql";
-import { getActionIntent, processSubmission, startCase } from "@ryot/ts-utils";
+import {
+	getActionIntent,
+	parseSearchQuery,
+	processSubmission,
+	startCase,
+} from "@ryot/ts-utils";
 import {
 	IconChartArea,
 	IconPlus,
@@ -34,7 +39,6 @@ import { match } from "ts-pattern";
 import { withQuery } from "ufo";
 import { useLocalStorage } from "usehooks-ts";
 import { z } from "zod";
-import { zx } from "zodix";
 import {
 	TimeSpan,
 	dayjsLib,
@@ -67,7 +71,7 @@ const defaultTimeSpan = TimeSpan.Last30Days;
 export const loader = async ({ request }: LoaderFunctionArgs) => {
 	const cookieName = await getEnhancedCookieName("measurements.list", request);
 	await redirectUsingEnhancedCookieSearchParams(request, cookieName);
-	const query = zx.parseQuery(request, searchParamsSchema);
+	const query = parseSearchQuery(request, searchParamsSchema);
 	const now = dayjsLib();
 	const startTime = getDateFromTimeSpan(query.timeSpan || defaultTimeSpan);
 	const [{ userMeasurementsList }] = await Promise.all([
diff --git a/apps/frontend/app/routes/_dashboard.media.$action.$lot.tsx b/apps/frontend/app/routes/_dashboard.media.$action.$lot.tsx
index 79fc4b2a0b..5c47ecdb08 100644
--- a/apps/frontend/app/routes/_dashboard.media.$action.$lot.tsx
+++ b/apps/frontend/app/routes/_dashboard.media.$action.$lot.tsx
@@ -36,7 +36,15 @@ import {
 	MetadataSearchDocument,
 	type MetadataSearchQuery,
 } from "@ryot/generated/graphql/backend/graphql";
-import { changeCase, snakeCase, startCase } from "@ryot/ts-utils";
+import {
+	changeCase,
+	parseParameters,
+	parseSearchQuery,
+	snakeCase,
+	startCase,
+	zodBoolAsString,
+	zodIntAsString,
+} from "@ryot/ts-utils";
 import {
 	IconBoxMultiple,
 	IconCheck,
@@ -53,7 +61,6 @@ import { $path } from "remix-routes";
 import { match } from "ts-pattern";
 import { withoutHost } from "ufo";
 import { z } from "zod";
-import { zx } from "zodix";
 import {
 	ApplicationGrid,
 	BaseMediaDisplayItem,
@@ -66,12 +73,12 @@ import { MetadataDisplayItem } from "~/components/media";
 import {
 	ApplicationTimeRange,
 	Verb,
-	commaDelimitedString,
 	dayjsLib,
 	getLot,
 	getStartTimeFromRange,
 	getVerb,
 	pageQueryParam,
+	zodCommaDelimitedString,
 } from "~/lib/generals";
 import {
 	useAppSearchParam,
@@ -111,26 +118,30 @@ enum Action {
 }
 
 export const loader = async ({ request, params }: LoaderFunctionArgs) => {
-	const { action, lot } = zx.parseParams(params, {
-		action: z.nativeEnum(Action),
-		lot: z.string().transform((v) => getLot(v) as MediaLot),
-	});
+	const { action, lot } = parseParameters(
+		params,
+		z.object({
+			action: z.nativeEnum(Action),
+			lot: z.string().transform((v) => getLot(v) as MediaLot),
+		}),
+	);
 	const cookieName = await getEnhancedCookieName(
 		`media.${action}.${lot}`,
 		request,
 	);
 	await redirectUsingEnhancedCookieSearchParams(request, cookieName);
-	const query = zx.parseQuery(request, {
+	const schema = z.object({
 		query: z.string().optional(),
-		[pageQueryParam]: zx.IntAsString.default("1"),
+		[pageQueryParam]: zodIntAsString.default("1"),
 	});
+	const query = parseSearchQuery(request, schema);
 	const [totalResults, mediaList, mediaSearch] = await match(action)
 		.with(Action.List, async () => {
-			const urlParse = zx.parseQuery(request, {
-				collections: commaDelimitedString,
+			const listSchema = z.object({
+				collections: zodCommaDelimitedString,
 				endDateRange: z.string().optional(),
 				startDateRange: z.string().optional(),
-				invertCollection: zx.BoolAsString.optional(),
+				invertCollection: zodBoolAsString.optional(),
 				sortBy: z.nativeEnum(MediaSortBy).default(defaultFilters.mineSortBy),
 				dateRange: z
 					.nativeEnum(ApplicationTimeRange)
@@ -142,6 +153,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
 					.nativeEnum(MediaGeneralFilter)
 					.default(defaultFilters.mineGeneralFilter),
 			});
+			const urlParse = parseSearchQuery(request, listSchema);
 			const { metadataList } = await serverGqlService.authenticatedRequest(
 				request,
 				MetadataListDocument,
@@ -174,11 +186,12 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
 				(m) => m.lot === lot,
 			);
 			if (!metadataSourcesForLot) throw new Error("Mapping not found");
-			const urlParse = zx.parseQuery(request, {
+			const searchSchema = z.object({
 				source: z
 					.nativeEnum(MediaSource)
 					.default(metadataSourcesForLot.sources[0]),
 			});
+			const urlParse = parseSearchQuery(request, searchSchema);
 			let metadataSearch: MetadataSearchQuery["metadataSearch"] | false;
 			try {
 				const response = await serverGqlService.authenticatedRequest(
@@ -187,8 +200,8 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
 					{
 						input: {
 							lot,
-							search: { page: query[pageQueryParam], query: query.query },
 							source: urlParse.source,
+							search: { page: query[pageQueryParam], query: query.query },
 						},
 					},
 				);
diff --git a/apps/frontend/app/routes/_dashboard.media.genre.$id._index.tsx b/apps/frontend/app/routes/_dashboard.media.genre.$id._index.tsx
index 975d400f10..353d3d3083 100644
--- a/apps/frontend/app/routes/_dashboard.media.genre.$id._index.tsx
+++ b/apps/frontend/app/routes/_dashboard.media.genre.$id._index.tsx
@@ -10,8 +10,12 @@ import {
 import type { LoaderFunctionArgs, MetaArgs } from "@remix-run/node";
 import { useLoaderData } from "@remix-run/react";
 import { GenreDetailsDocument } from "@ryot/generated/graphql/backend/graphql";
+import {
+	parseParameters,
+	parseSearchQuery,
+	zodIntAsString,
+} from "@ryot/ts-utils";
 import { z } from "zod";
-import { zx } from "zodix";
 import { ApplicationGrid } from "~/components/common";
 import { MetadataDisplayItem } from "~/components/media";
 import { pageQueryParam } from "~/lib/generals";
@@ -24,16 +28,16 @@ import {
 } from "~/lib/utilities.server";
 
 const searchParamsSchema = z.object({
-	[pageQueryParam]: zx.IntAsString.default("1"),
+	[pageQueryParam]: zodIntAsString.default("1"),
 });
 
 export type SearchParams = z.infer<typeof searchParamsSchema>;
 
 export const loader = async ({ request, params }: LoaderFunctionArgs) => {
-	const { id: genreId } = zx.parseParams(params, { id: z.string() });
+	const { id: genreId } = parseParameters(params, z.object({ id: z.string() }));
 	const cookieName = await getEnhancedCookieName(`genre.${genreId}`, request);
 	await redirectUsingEnhancedCookieSearchParams(request, cookieName);
-	const query = zx.parseQuery(request, searchParamsSchema);
+	const query = parseSearchQuery(request, searchParamsSchema);
 	const [{ genreDetails }] = await Promise.all([
 		serverGqlService.request(GenreDetailsDocument, {
 			input: { genreId, page: query[pageQueryParam] },
diff --git a/apps/frontend/app/routes/_dashboard.media.genre.list.tsx b/apps/frontend/app/routes/_dashboard.media.genre.list.tsx
index 1751a52a17..09e4385633 100644
--- a/apps/frontend/app/routes/_dashboard.media.genre.list.tsx
+++ b/apps/frontend/app/routes/_dashboard.media.genre.list.tsx
@@ -20,11 +20,16 @@ import {
 	GenreDetailsDocument,
 	GenresListDocument,
 } from "@ryot/generated/graphql/backend/graphql";
-import { getInitials, isString, truncate } from "@ryot/ts-utils";
+import {
+	getInitials,
+	isString,
+	parseSearchQuery,
+	truncate,
+	zodIntAsString,
+} from "@ryot/ts-utils";
 import { useQuery } from "@tanstack/react-query";
 import { $path } from "remix-routes";
 import { z } from "zod";
-import { zx } from "zodix";
 import {
 	ApplicationGrid,
 	DebouncedSearchInput,
@@ -51,8 +56,8 @@ import {
 } from "~/lib/utilities.server";
 
 const searchParamsSchema = z.object({
-	[pageQueryParam]: zx.IntAsString.default("1"),
 	query: z.string().optional(),
+	[pageQueryParam]: zodIntAsString.default("1"),
 });
 
 export type SearchParams = z.infer<typeof searchParamsSchema>;
@@ -60,7 +65,7 @@ export type SearchParams = z.infer<typeof searchParamsSchema>;
 export const loader = async ({ request }: LoaderFunctionArgs) => {
 	const cookieName = await getEnhancedCookieName("genre.list", request);
 	await redirectUsingEnhancedCookieSearchParams(request, cookieName);
-	const query = zx.parseQuery(request, searchParamsSchema);
+	const query = parseSearchQuery(request, searchParamsSchema);
 	const [{ genresList }] = await Promise.all([
 		serverGqlService.request(GenresListDocument, {
 			input: { page: query[pageQueryParam], query: query.query },
diff --git a/apps/frontend/app/routes/_dashboard.media.groups.$action.tsx b/apps/frontend/app/routes/_dashboard.media.groups.$action.tsx
index 555ebc4eea..4fd43855dc 100644
--- a/apps/frontend/app/routes/_dashboard.media.groups.$action.tsx
+++ b/apps/frontend/app/routes/_dashboard.media.groups.$action.tsx
@@ -26,7 +26,15 @@ import {
 	MetadataGroupsListDocument,
 	PersonAndMetadataGroupsSortBy,
 } from "@ryot/generated/graphql/backend/graphql";
-import { changeCase, isString, startCase } from "@ryot/ts-utils";
+import {
+	changeCase,
+	isString,
+	parseParameters,
+	parseSearchQuery,
+	startCase,
+	zodBoolAsString,
+	zodIntAsString,
+} from "@ryot/ts-utils";
 import {
 	IconCheck,
 	IconFilter,
@@ -40,7 +48,6 @@ import { $path } from "remix-routes";
 import invariant from "tiny-invariant";
 import { match } from "ts-pattern";
 import { z } from "zod";
-import { zx } from "zodix";
 import {
 	ApplicationGrid,
 	CollectionsFilter,
@@ -49,7 +56,7 @@ import {
 } from "~/components/common";
 import { BaseMediaDisplayItem } from "~/components/common";
 import { MetadataGroupDisplayItem } from "~/components/media";
-import { commaDelimitedString, pageQueryParam } from "~/lib/generals";
+import { pageQueryParam, zodCommaDelimitedString } from "~/lib/generals";
 import { useAppSearchParam, useCoreDetails } from "~/lib/hooks";
 import { useBulkEditCollection } from "~/lib/state/collection";
 import {
@@ -74,32 +81,37 @@ enum Action {
 }
 
 export const loader = async ({ request, params }: LoaderFunctionArgs) => {
-	const { action } = zx.parseParams(params, { action: z.nativeEnum(Action) });
+	const { action } = parseParameters(
+		params,
+		z.object({ action: z.nativeEnum(Action) }),
+	);
 	const cookieName = await getEnhancedCookieName(`groups.${action}`, request);
-	const query = zx.parseQuery(request, {
+	const schema = z.object({
 		query: z.string().optional(),
-		[pageQueryParam]: zx.IntAsString.default("1"),
+		[pageQueryParam]: zodIntAsString.default("1"),
 	});
+	const query = parseSearchQuery(request, schema);
 	const [totalResults, list, search] = await match(action)
 		.with(Action.List, async () => {
-			const urlParse = zx.parseQuery(request, {
+			const listSchema = z.object({
+				collections: zodCommaDelimitedString,
+				invertCollection: zodBoolAsString.optional(),
+				orderBy: z.nativeEnum(GraphqlSortOrder).default(defaultFilters.orderBy),
 				sortBy: z
 					.nativeEnum(PersonAndMetadataGroupsSortBy)
 					.default(defaultFilters.sortBy),
-				orderBy: z.nativeEnum(GraphqlSortOrder).default(defaultFilters.orderBy),
-				collections: commaDelimitedString,
-				invertCollection: zx.BoolAsString.optional(),
 			});
+			const urlParse = parseSearchQuery(request, listSchema);
 			const { metadataGroupsList } =
 				await serverGqlService.authenticatedRequest(
 					request,
 					MetadataGroupsListDocument,
 					{
 						input: {
-							search: { page: query[pageQueryParam], query: query.query },
-							sort: { by: urlParse.sortBy, order: urlParse.orderBy },
-							filter: { collections: urlParse.collections },
 							invertCollection: urlParse.invertCollection,
+							filter: { collections: urlParse.collections },
+							sort: { by: urlParse.sortBy, order: urlParse.orderBy },
+							search: { page: query[pageQueryParam], query: query.query },
 						},
 					},
 				);
@@ -110,9 +122,10 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
 			] as const;
 		})
 		.with(Action.Search, async () => {
-			const urlParse = zx.parseQuery(request, {
+			const searchSchema = z.object({
 				source: z.nativeEnum(MediaSource).default(MediaSource.Tmdb),
 			});
+			const urlParse = parseSearchQuery(request, searchSchema);
 			const coreDetails = await getCoreDetails();
 			const lot = coreDetails.metadataGroupSourceLotMappings.find(
 				(m) => m.source === urlParse.source,
diff --git a/apps/frontend/app/routes/_dashboard.media.groups.item.$id._index.tsx b/apps/frontend/app/routes/_dashboard.media.groups.item.$id._index.tsx
index 42a1c5437b..bbf726a205 100644
--- a/apps/frontend/app/routes/_dashboard.media.groups.item.$id._index.tsx
+++ b/apps/frontend/app/routes/_dashboard.media.groups.item.$id._index.tsx
@@ -17,6 +17,7 @@ import {
 	MetadataGroupDetailsDocument,
 	UserMetadataGroupDetailsDocument,
 } from "@ryot/generated/graphql/backend/graphql";
+import { parseParameters, parseSearchQuery } from "@ryot/ts-utils";
 import {
 	IconDeviceTv,
 	IconInfoCircle,
@@ -24,7 +25,6 @@ import {
 	IconUser,
 } from "@tabler/icons-react";
 import { z } from "zod";
-import { zx } from "zodix";
 import {
 	DisplayCollection,
 	MediaDetailsLayout,
@@ -48,8 +48,11 @@ const searchParamsSchema = z.object({
 export type SearchParams = z.infer<typeof searchParamsSchema>;
 
 export const loader = async ({ request, params }: LoaderFunctionArgs) => {
-	const { id: metadataGroupId } = zx.parseParams(params, { id: z.string() });
-	const query = zx.parseQuery(request, searchParamsSchema);
+	const { id: metadataGroupId } = parseParameters(
+		params,
+		z.object({ id: z.string() }),
+	);
+	const query = parseSearchQuery(request, searchParamsSchema);
 	const [{ metadataGroupDetails }, { userMetadataGroupDetails }] =
 		await Promise.all([
 			serverGqlService.request(MetadataGroupDetailsDocument, {
diff --git a/apps/frontend/app/routes/_dashboard.media.item.$id._index.tsx b/apps/frontend/app/routes/_dashboard.media.item.$id._index.tsx
index eb02d422f6..198f64a1e9 100644
--- a/apps/frontend/app/routes/_dashboard.media.item.$id._index.tsx
+++ b/apps/frontend/app/routes/_dashboard.media.item.$id._index.tsx
@@ -64,6 +64,8 @@ import {
 	isInteger,
 	isNumber,
 	isString,
+	parseParameters,
+	parseSearchQuery,
 	processSubmission,
 } from "@ryot/ts-utils";
 import {
@@ -96,7 +98,6 @@ import { $path } from "remix-routes";
 import { match } from "ts-pattern";
 import { withQuery } from "ufo";
 import { z } from "zod";
-import { zx } from "zodix";
 import {
 	DisplayCollection,
 	DisplayThreePointReview,
@@ -152,8 +153,11 @@ const searchParamsSchema = z
 export type SearchParams = z.infer<typeof searchParamsSchema>;
 
 export const loader = async ({ request, params }: LoaderFunctionArgs) => {
-	const { id: metadataId } = zx.parseParams(params, { id: z.string() });
-	const query = zx.parseQuery(request, searchParamsSchema);
+	const { id: metadataId } = parseParameters(
+		params,
+		z.object({ id: z.string() }),
+	);
+	const query = parseSearchQuery(request, searchParamsSchema);
 	const [{ metadataDetails }, { userMetadataDetails }] = await Promise.all([
 		serverGqlService.request(MetadataDetailsDocument, { metadataId }),
 		serverGqlService.authenticatedRequest(
diff --git a/apps/frontend/app/routes/_dashboard.media.people.$action.tsx b/apps/frontend/app/routes/_dashboard.media.people.$action.tsx
index ecf3fe26bf..ff41f1dd0d 100644
--- a/apps/frontend/app/routes/_dashboard.media.people.$action.tsx
+++ b/apps/frontend/app/routes/_dashboard.media.people.$action.tsx
@@ -26,7 +26,14 @@ import {
 	type PeopleSearchQuery,
 	PersonAndMetadataGroupsSortBy,
 } from "@ryot/generated/graphql/backend/graphql";
-import { changeCase, startCase } from "@ryot/ts-utils";
+import {
+	changeCase,
+	parseParameters,
+	parseSearchQuery,
+	startCase,
+	zodBoolAsString,
+	zodIntAsString,
+} from "@ryot/ts-utils";
 import {
 	IconCheck,
 	IconFilter,
@@ -39,7 +46,6 @@ import { useState } from "react";
 import { $path } from "remix-routes";
 import { match } from "ts-pattern";
 import { z } from "zod";
-import { zx } from "zodix";
 import {
 	ApplicationGrid,
 	CollectionsFilter,
@@ -48,7 +54,7 @@ import {
 } from "~/components/common";
 import { BaseMediaDisplayItem } from "~/components/common";
 import { PersonDisplayItem } from "~/components/media";
-import { commaDelimitedString, pageQueryParam } from "~/lib/generals";
+import { pageQueryParam, zodCommaDelimitedString } from "~/lib/generals";
 import { useAppSearchParam, useCoreDetails } from "~/lib/hooks";
 import { useBulkEditCollection } from "~/lib/state/collection";
 import {
@@ -72,31 +78,36 @@ enum Action {
 	Search = "search",
 }
 
-const searchUrlSchema = z.object({
-	isTmdbCompany: zx.BoolAsString.optional(),
-	isAnilistStudio: zx.BoolAsString.optional(),
-	isHardcoverPublisher: zx.BoolAsString.optional(),
+const searchSchema = z.object({
+	isTmdbCompany: zodBoolAsString.optional(),
+	isAnilistStudio: zodBoolAsString.optional(),
+	isHardcoverPublisher: zodBoolAsString.optional(),
 	source: z.nativeEnum(MediaSource).default(MediaSource.Tmdb),
 });
 
 export const loader = async ({ request, params }: LoaderFunctionArgs) => {
-	const { action } = zx.parseParams(params, { action: z.nativeEnum(Action) });
+	const { action } = parseParameters(
+		params,
+		z.object({ action: z.nativeEnum(Action) }),
+	);
 	const cookieName = await getEnhancedCookieName(`people.${action}`, request);
 	await redirectUsingEnhancedCookieSearchParams(request, cookieName);
-	const query = zx.parseQuery(request, {
+	const schema = z.object({
 		query: z.string().optional(),
-		[pageQueryParam]: zx.IntAsString.default("1"),
+		[pageQueryParam]: zodIntAsString.default("1"),
 	});
+	const query = parseSearchQuery(request, schema);
 	const [totalResults, peopleList, peopleSearch] = await match(action)
 		.with(Action.List, async () => {
-			const urlParse = zx.parseQuery(request, {
+			const listSchema = z.object({
+				collections: zodCommaDelimitedString,
+				invertCollection: zodBoolAsString.optional(),
+				orderBy: z.nativeEnum(GraphqlSortOrder).default(defaultFilters.orderBy),
 				sortBy: z
 					.nativeEnum(PersonAndMetadataGroupsSortBy)
 					.default(defaultFilters.sortBy),
-				orderBy: z.nativeEnum(GraphqlSortOrder).default(defaultFilters.orderBy),
-				collections: commaDelimitedString,
-				invertCollection: zx.BoolAsString.optional(),
 			});
+			const urlParse = parseSearchQuery(request, listSchema);
 			const { peopleList } = await serverGqlService.authenticatedRequest(
 				request,
 				PeopleListDocument,
@@ -116,7 +127,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
 			] as const;
 		})
 		.with(Action.Search, async () => {
-			const urlParse = zx.parseQuery(request, searchUrlSchema);
+			const urlParse = parseSearchQuery(request, searchSchema);
 			const { peopleSearch } = await serverGqlService.authenticatedRequest(
 				request,
 				PeopleSearchDocument,
@@ -392,7 +403,7 @@ const PersonSearchItem = (props: {
 const commitPerson = async (
 	name: string,
 	identifier: string,
-	additionalData: z.infer<typeof searchUrlSchema>,
+	additionalData: z.infer<typeof searchSchema>,
 ) => {
 	const data = new FormData();
 	data.append("identifier", identifier);
diff --git a/apps/frontend/app/routes/_dashboard.media.people.item.$id._index.tsx b/apps/frontend/app/routes/_dashboard.media.people.item.$id._index.tsx
index 04fcfaeab3..8ece4df183 100644
--- a/apps/frontend/app/routes/_dashboard.media.people.item.$id._index.tsx
+++ b/apps/frontend/app/routes/_dashboard.media.people.item.$id._index.tsx
@@ -18,7 +18,7 @@ import {
 	PersonDetailsDocument,
 	UserPersonDetailsDocument,
 } from "@ryot/generated/graphql/backend/graphql";
-import { sum } from "@ryot/ts-utils";
+import { parseParameters, parseSearchQuery, sum } from "@ryot/ts-utils";
 import {
 	IconDeviceTv,
 	IconInfoCircle,
@@ -31,7 +31,6 @@ import type { ReactNode } from "react";
 import { $path } from "remix-routes";
 import { useLocalStorage } from "usehooks-ts";
 import { z } from "zod";
-import { zx } from "zodix";
 import {
 	DisplayCollection,
 	MediaDetailsLayout,
@@ -56,8 +55,11 @@ const searchParamsSchema = z.object({
 export type SearchParams = z.infer<typeof searchParamsSchema>;
 
 export const loader = async ({ request, params }: LoaderFunctionArgs) => {
-	const { id: personId } = zx.parseParams(params, { id: z.string() });
-	const query = zx.parseQuery(request, searchParamsSchema);
+	const { id: personId } = parseParameters(
+		params,
+		z.object({ id: z.string() }),
+	);
+	const query = parseSearchQuery(request, searchParamsSchema);
 	const [{ personDetails }, { userPersonDetails }] = await Promise.all([
 		serverGqlService.request(PersonDetailsDocument, { personId }),
 		serverGqlService.authenticatedRequest(request, UserPersonDetailsDocument, {
diff --git a/apps/frontend/app/routes/_dashboard.media.update.$action.tsx b/apps/frontend/app/routes/_dashboard.media.update.$action.tsx
index ba1ba470e7..bf01ea0643 100644
--- a/apps/frontend/app/routes/_dashboard.media.update.$action.tsx
+++ b/apps/frontend/app/routes/_dashboard.media.update.$action.tsx
@@ -28,13 +28,18 @@ import {
 	MetadataDetailsDocument,
 	UpdateCustomMetadataDocument,
 } from "@ryot/generated/graphql/backend/graphql";
-import { camelCase, changeCase, processSubmission } from "@ryot/ts-utils";
+import {
+	camelCase,
+	changeCase,
+	parseParameters,
+	parseSearchQuery,
+	processSubmission,
+} from "@ryot/ts-utils";
 import { IconCalendar, IconPhoto, IconVideo } from "@tabler/icons-react";
 import { $path } from "remix-routes";
 import invariant from "tiny-invariant";
 import { match } from "ts-pattern";
 import { z } from "zod";
-import { zx } from "zodix";
 import { useCoreDetails } from "~/lib/hooks";
 import { s3FileUploader, serverGqlService } from "~/lib/utilities.server";
 
@@ -51,8 +56,11 @@ const searchParamsSchema = z.object({
 export type SearchParams = z.infer<typeof searchParamsSchema>;
 
 export const loader = async ({ params, request }: LoaderFunctionArgs) => {
-	const { action } = zx.parseParams(params, { action: z.nativeEnum(Action) });
-	const query = zx.parseQuery(request, searchParamsSchema);
+	const { action } = parseParameters(
+		params,
+		z.object({ action: z.nativeEnum(Action) }),
+	);
+	const query = parseSearchQuery(request, searchParamsSchema);
 	const details = await match(action)
 		.with(Action.Create, () => undefined)
 		.with(Action.Edit, async () => {
diff --git a/apps/frontend/app/routes/_dashboard.settings.integrations.tsx b/apps/frontend/app/routes/_dashboard.settings.integrations.tsx
index 99d893ea33..eebbc4bb80 100644
--- a/apps/frontend/app/routes/_dashboard.settings.integrations.tsx
+++ b/apps/frontend/app/routes/_dashboard.settings.integrations.tsx
@@ -38,7 +38,12 @@ import {
 	UserIntegrationsDocument,
 	type UserIntegrationsQuery,
 } from "@ryot/generated/graphql/backend/graphql";
-import { changeCase, getActionIntent, processSubmission } from "@ryot/ts-utils";
+import {
+	changeCase,
+	getActionIntent,
+	processSubmission,
+	zodCheckboxAsString,
+} from "@ryot/ts-utils";
 import {
 	IconCheck,
 	IconCopy,
@@ -51,13 +56,12 @@ import { useState } from "react";
 import { match } from "ts-pattern";
 import { withQuery } from "ufo";
 import { z } from "zod";
-import { zx } from "zodix";
 import {
 	PRO_REQUIRED_MESSAGE,
 	applicationBaseUrl,
-	commaDelimitedString,
 	dayjsLib,
 	openConfirmationModal,
+	zodCommaDelimitedString,
 } from "~/lib/generals";
 import {
 	useConfirmSubmit,
@@ -179,7 +183,7 @@ const createSchema = z.object({
 	minimumProgress: z.string().optional(),
 	maximumProgress: z.string().optional(),
 	provider: z.nativeEnum(IntegrationProvider),
-	syncToOwnedCollection: zx.CheckboxAsString.optional(),
+	syncToOwnedCollection: zodCheckboxAsString.optional(),
 	providerSpecifics: z
 		.object({
 			plexYankBaseUrl: z.string().optional(),
@@ -195,12 +199,12 @@ const createSchema = z.object({
 			radarrApiKey: z.string().optional(),
 			radarrProfileId: z.number().optional(),
 			radarrRootFolderPath: z.string().optional(),
-			radarrSyncCollectionIds: commaDelimitedString,
+			radarrSyncCollectionIds: zodCommaDelimitedString,
 			sonarrBaseUrl: z.string().optional(),
 			sonarrApiKey: z.string().optional(),
 			sonarrProfileId: z.number().optional(),
 			sonarrRootFolderPath: z.string().optional(),
-			sonarrSyncCollectionIds: commaDelimitedString,
+			sonarrSyncCollectionIds: zodCommaDelimitedString,
 			jellyfinPushBaseUrl: z.string().optional(),
 			jellyfinPushUsername: z.string().optional(),
 			jellyfinPushPassword: z.string().optional(),
@@ -218,8 +222,8 @@ const updateSchema = z.object({
 	integrationId: z.string(),
 	minimumProgress: z.string().optional(),
 	maximumProgress: z.string().optional(),
-	isDisabled: zx.CheckboxAsString.optional(),
-	syncToOwnedCollection: zx.CheckboxAsString.optional(),
+	isDisabled: zodCheckboxAsString.optional(),
+	syncToOwnedCollection: zodCheckboxAsString.optional(),
 });
 
 export default function Page() {
diff --git a/apps/frontend/app/routes/_dashboard.settings.miscellaneous.tsx b/apps/frontend/app/routes/_dashboard.settings.miscellaneous.tsx
index 0307fb8609..f23c0af338 100644
--- a/apps/frontend/app/routes/_dashboard.settings.miscellaneous.tsx
+++ b/apps/frontend/app/routes/_dashboard.settings.miscellaneous.tsx
@@ -96,15 +96,6 @@ const getJobDetails = (job: BackgroundJob) =>
 					true,
 				] as const,
 		)
-		.with(
-			BackgroundJob.DeleteAllApplicationCache,
-			() =>
-				[
-					"Delete all cache",
-					"Delete all application caches. Use this if you updated a critical configuration parameter and can not see the changes reflected in the UI.",
-					true,
-				] as const,
-		)
 		.with(
 			BackgroundJob.CalculateUserActivitiesAndSummary,
 			() =>
diff --git a/apps/frontend/app/routes/_dashboard.settings.notifications.tsx b/apps/frontend/app/routes/_dashboard.settings.notifications.tsx
index 47738ae6b5..d14a37a68c 100644
--- a/apps/frontend/app/routes/_dashboard.settings.notifications.tsx
+++ b/apps/frontend/app/routes/_dashboard.settings.notifications.tsx
@@ -32,7 +32,12 @@ import {
 	UserNotificationPlatformsDocument,
 	type UserNotificationPlatformsQuery,
 } from "@ryot/generated/graphql/backend/graphql";
-import { changeCase, getActionIntent, processSubmission } from "@ryot/ts-utils";
+import {
+	changeCase,
+	getActionIntent,
+	processSubmission,
+	zodBoolAsString,
+} from "@ryot/ts-utils";
 import {
 	IconPlayerPause,
 	IconPlayerPlay,
@@ -43,7 +48,6 @@ import { $path } from "remix-routes";
 import { match } from "ts-pattern";
 import { withQuery } from "ufo";
 import { z } from "zod";
-import { zx } from "zodix";
 import { dayjsLib, openConfirmationModal } from "~/lib/generals";
 import { useConfirmSubmit, useCoreDetails } from "~/lib/hooks";
 import { createToastHeaders, serverGqlService } from "~/lib/utilities.server";
@@ -137,7 +141,7 @@ const createSchema = z.object({
 
 const updateSchema = z.object({
 	notificationId: z.string(),
-	isDisabled: zx.BoolAsString.optional(),
+	isDisabled: zodBoolAsString.optional(),
 });
 
 export default function Page() {
diff --git a/apps/frontend/app/routes/_dashboard.settings.preferences.tsx b/apps/frontend/app/routes/_dashboard.settings.preferences.tsx
index f00ed34eca..cfd7a141ad 100644
--- a/apps/frontend/app/routes/_dashboard.settings.preferences.tsx
+++ b/apps/frontend/app/routes/_dashboard.settings.preferences.tsx
@@ -41,6 +41,7 @@ import {
 	cn,
 	isBoolean,
 	isNumber,
+	parseSearchQuery,
 	snakeCase,
 	startCase,
 } from "@ryot/ts-utils";
@@ -55,7 +56,6 @@ import { type Draft, produce } from "immer";
 import { Fragment, useState } from "react";
 import { match } from "ts-pattern";
 import { z } from "zod";
-import { zx } from "zodix";
 import { PRO_REQUIRED_MESSAGE, clientGqlService } from "~/lib/generals";
 import {
 	useCoreDetails,
@@ -70,7 +70,7 @@ const searchSchema = z.object({
 });
 
 export const loader = async ({ request }: LoaderFunctionArgs) => {
-	const query = zx.parseQuery(request, searchSchema);
+	const query = parseSearchQuery(request, searchSchema);
 	return { query };
 };
 
diff --git a/apps/frontend/app/routes/_dashboard.settings.profile-and-sharing.tsx b/apps/frontend/app/routes/_dashboard.settings.profile-and-sharing.tsx
index 4b27608566..32fea32b02 100644
--- a/apps/frontend/app/routes/_dashboard.settings.profile-and-sharing.tsx
+++ b/apps/frontend/app/routes/_dashboard.settings.profile-and-sharing.tsx
@@ -42,6 +42,8 @@ import {
 	isNumber,
 	isString,
 	processSubmission,
+	zodCheckboxAsString,
+	zodIntAsString,
 } from "@ryot/ts-utils";
 import {
 	IconEye,
@@ -53,7 +55,6 @@ import { ClientOnly } from "remix-utils/client-only";
 import { match } from "ts-pattern";
 import { withQuery } from "ufo";
 import { z } from "zod";
-import { zx } from "zodix";
 import {
 	applicationBaseUrl,
 	dayjsLib,
@@ -165,8 +166,8 @@ const createAccessLinkFormSchema = z.object({
 	name: z.string(),
 	expiresOn: z.string().optional(),
 	redirectTo: z.string().optional(),
-	maximumUses: zx.IntAsString.optional(),
-	isMutationAllowed: zx.CheckboxAsString.optional(),
+	maximumUses: zodIntAsString.optional(),
+	isMutationAllowed: zodCheckboxAsString.optional(),
 });
 
 export default function Page() {
diff --git a/apps/frontend/app/routes/_dashboard.settings.users.tsx b/apps/frontend/app/routes/_dashboard.settings.users.tsx
index 6348080510..2dc4c7e85f 100644
--- a/apps/frontend/app/routes/_dashboard.settings.users.tsx
+++ b/apps/frontend/app/routes/_dashboard.settings.users.tsx
@@ -36,8 +36,10 @@ import {
 import {
 	changeCase,
 	getActionIntent,
+	parseSearchQuery,
 	processSubmission,
 	truncate,
+	zodCheckboxAsString,
 } from "@ryot/ts-utils";
 import {
 	IconPencil,
@@ -52,7 +54,6 @@ import { $path } from "remix-routes";
 import { match } from "ts-pattern";
 import { withQuery } from "ufo";
 import { z } from "zod";
-import { zx } from "zodix";
 import { DebouncedSearchInput } from "~/components/common";
 import { openConfirmationModal } from "~/lib/generals";
 import { useConfirmSubmit, useCoreDetails } from "~/lib/hooks";
@@ -75,7 +76,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
 	if (userDetails.lot !== UserLot.Admin) throw redirect($path("/"));
 	const cookieName = await getEnhancedCookieName("settings.users", request);
 	await redirectUsingEnhancedCookieSearchParams(request, cookieName);
-	const query = zx.parseQuery(request, searchParamsSchema);
+	const query = parseSearchQuery(request, searchParamsSchema);
 	const [{ usersList }] = await Promise.all([
 		serverGqlService.authenticatedRequest(request, UsersListDocument, {
 			query: query.query,
@@ -171,9 +172,9 @@ const deleteSchema = z.object({ toDeleteUserId: z.string() });
 const updateUserSchema = z.object({
 	userId: z.string(),
 	adminAccessToken: z.string(),
-	isDisabled: zx.CheckboxAsString.optional(),
-	lot: z.nativeEnum(UserLot).optional(),
 	password: z.string().optional(),
+	lot: z.nativeEnum(UserLot).optional(),
+	isDisabled: zodCheckboxAsString.optional(),
 });
 
 export default function Page() {
diff --git a/apps/frontend/app/routes/actions.tsx b/apps/frontend/app/routes/actions.tsx
index da37824b9e..e759dd5770 100644
--- a/apps/frontend/app/routes/actions.tsx
+++ b/apps/frontend/app/routes/actions.tsx
@@ -32,12 +32,13 @@ import {
 	omitBy,
 	processSubmission,
 	set,
+	zodBoolAsString,
+	zodCheckboxAsString,
 } from "@ryot/ts-utils";
 import { $path } from "remix-routes";
 import invariant from "tiny-invariant";
 import { match } from "ts-pattern";
 import { z } from "zod";
-import { zx } from "zodix";
 import { redirectToQueryParam } from "~/lib/generals";
 import {
 	MetadataIdSchema,
@@ -508,18 +509,18 @@ const commitPersonSchema = z.object({
 	name: z.string(),
 	identifier: z.string(),
 	source: z.nativeEnum(MediaSource),
-	isTmdbCompany: zx.BoolAsString.optional(),
-	isAnilistStudio: zx.BoolAsString.optional(),
-	isHardcoverPublisher: zx.BoolAsString.optional(),
+	isTmdbCompany: zodBoolAsString.optional(),
+	isAnilistStudio: zodBoolAsString.optional(),
+	isHardcoverPublisher: zodBoolAsString.optional(),
 });
 
 const reviewCommentSchema = z.object({
 	reviewId: z.string(),
-	commentId: z.string().optional(),
 	text: z.string().optional(),
-	decrementLikes: zx.BoolAsString.optional(),
-	incrementLikes: zx.BoolAsString.optional(),
-	shouldDelete: zx.BoolAsString.optional(),
+	commentId: z.string().optional(),
+	shouldDelete: zodBoolAsString.optional(),
+	decrementLikes: zodBoolAsString.optional(),
+	incrementLikes: zodBoolAsString.optional(),
 });
 
 const changeCollectionToEntitySchema = z.object({
@@ -532,14 +533,14 @@ const changeCollectionToEntitySchema = z.object({
 
 const reviewSchema = z
 	.object({
-		shouldDelete: zx.BoolAsString.optional(),
-		rating: z.string().optional(),
 		text: z.string().optional(),
-		visibility: z.nativeEnum(Visibility).optional(),
-		isSpoiler: zx.CheckboxAsString.optional(),
+		rating: z.string().optional(),
 		entityId: z.string().optional(),
-		entityLot: z.nativeEnum(EntityLot).optional(),
 		reviewId: z.string().optional(),
+		shouldDelete: zodBoolAsString.optional(),
+		isSpoiler: zodCheckboxAsString.optional(),
+		entityLot: z.nativeEnum(EntityLot).optional(),
+		visibility: z.nativeEnum(Visibility).optional(),
 	})
 	.merge(MetadataSpecificsSchema);
 
@@ -553,14 +554,14 @@ const getChangeCollectionToEntityVariables = (formData: FormData) => {
 
 const progressUpdateSchema = z
 	.object({
-		metadataLot: z.nativeEnum(MediaLot),
 		date: z.string().optional(),
-		[redirectToQueryParam]: z.string().optional(),
-		showAllEpisodesBefore: zx.BoolAsString.optional(),
-		podcastAllEpisodesBefore: zx.CheckboxAsString.optional(),
-		animeAllEpisodesBefore: zx.CheckboxAsString.optional(),
-		mangaAllChaptersOrVolumesBefore: zx.CheckboxAsString.optional(),
+		metadataLot: z.nativeEnum(MediaLot),
 		providerWatchedOn: z.string().optional(),
+		[redirectToQueryParam]: z.string().optional(),
+		showAllEpisodesBefore: zodBoolAsString.optional(),
+		animeAllEpisodesBefore: zodCheckboxAsString.optional(),
+		podcastAllEpisodesBefore: zodCheckboxAsString.optional(),
+		mangaAllChaptersOrVolumesBefore: zodCheckboxAsString.optional(),
 	})
 	.merge(MetadataIdSchema)
 	.merge(MetadataSpecificsSchema);
diff --git a/apps/frontend/app/routes/api.auth.tsx b/apps/frontend/app/routes/api.auth.tsx
index bb750a4252..e14a67b606 100644
--- a/apps/frontend/app/routes/api.auth.tsx
+++ b/apps/frontend/app/routes/api.auth.tsx
@@ -6,9 +6,9 @@ import {
 	RegisterUserDocument,
 	UserByOidcIssuerIdDocument,
 } from "@ryot/generated/graphql/backend/graphql";
+import { parseSearchQuery } from "@ryot/ts-utils";
 import { $path } from "remix-routes";
 import { z } from "zod";
-import { zx } from "zodix";
 import {
 	getCookiesForApplication,
 	getCoreDetails,
@@ -21,7 +21,7 @@ const searchParamsSchema = z.object({ code: z.string() });
 export type SearchParams = z.infer<typeof searchParamsSchema>;
 
 export const loader = async ({ request }: LoaderFunctionArgs) => {
-	const input = zx.parseQuery(request, searchParamsSchema);
+	const input = parseSearchQuery(request, searchParamsSchema);
 	const { getOidcToken } = await serverGqlService.request(
 		GetOidcTokenDocument,
 		input,
diff --git a/apps/frontend/app/routes/api.sharing.$accessLinkId.tsx b/apps/frontend/app/routes/api.sharing.$accessLinkId.tsx
index 0fb65b6289..175b38495d 100644
--- a/apps/frontend/app/routes/api.sharing.$accessLinkId.tsx
+++ b/apps/frontend/app/routes/api.sharing.$accessLinkId.tsx
@@ -3,10 +3,14 @@ import {
 	ProcessAccessLinkDocument,
 	type ProcessAccessLinkInput,
 } from "@ryot/generated/graphql/backend/graphql";
+import {
+	parseParameters,
+	parseSearchQuery,
+	zodBoolAsString,
+} from "@ryot/ts-utils";
 import { $path } from "remix-routes";
 import { safeRedirect } from "remix-utils/safe-redirect";
 import { z } from "zod";
-import { zx } from "zodix";
 import { redirectToQueryParam } from "~/lib/generals";
 import {
 	createToastHeaders,
@@ -14,16 +18,17 @@ import {
 	serverGqlService,
 } from "~/lib/utilities.server";
 
-const paramsSchema = z.object({ accessLinkId: z.string() });
-
 const searchParamsSchema = z.object({
-	isAccountDefault: zx.BoolAsString.optional(),
+	isAccountDefault: zodBoolAsString.optional(),
 	[redirectToQueryParam]: z.string().optional(),
 });
 
 export const loader = async ({ request, params }: LoaderFunctionArgs) => {
-	const query = zx.parseQuery(request, searchParamsSchema);
-	const routeParams = zx.parseParams(params, paramsSchema);
+	const routeParams = parseParameters(
+		params,
+		z.object({ accessLinkId: z.string() }),
+	);
+	const query = parseSearchQuery(request, searchParamsSchema);
 	const input: ProcessAccessLinkInput = {};
 	if (query.isAccountDefault) input.username = routeParams.accessLinkId;
 	else input.id = routeParams.accessLinkId;
@@ -32,14 +37,13 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
 		{ input },
 	);
 	if (processAccessLink.__typename === "ProcessAccessLinkResponse") {
-		const queryParams = zx.parseQuery(request, searchParamsSchema);
 		const headers = await getCookiesForApplication(
 			processAccessLink.apiKey,
 			processAccessLink.tokenValidForDays,
 		);
 		return redirect(
 			safeRedirect(
-				queryParams[redirectToQueryParam] ||
+				query[redirectToQueryParam] ||
 					processAccessLink.redirectTo ||
 					$path("/"),
 			),
diff --git a/apps/frontend/app/routes/auth.tsx b/apps/frontend/app/routes/auth.tsx
index f95a86e382..f3b2c911e6 100644
--- a/apps/frontend/app/routes/auth.tsx
+++ b/apps/frontend/app/routes/auth.tsx
@@ -26,14 +26,19 @@ import {
 	RegisterErrorVariant,
 	RegisterUserDocument,
 } from "@ryot/generated/graphql/backend/graphql";
-import { getActionIntent, processSubmission, startCase } from "@ryot/ts-utils";
+import {
+	getActionIntent,
+	parseSearchQuery,
+	processSubmission,
+	startCase,
+	zodNumAsString,
+} from "@ryot/ts-utils";
 import { IconAt } from "@tabler/icons-react";
 import { $path } from "remix-routes";
 import { safeRedirect } from "remix-utils/safe-redirect";
 import { match } from "ts-pattern";
 import { withQuery } from "ufo";
 import { z } from "zod";
-import { zx } from "zodix";
 import { dayjsLib, redirectToQueryParam } from "~/lib/generals";
 import {
 	createToastHeaders,
@@ -52,7 +57,7 @@ export type SearchParams = z.infer<typeof searchParamsSchema> &
 	Record<string, string>;
 
 export const loader = async ({ request }: LoaderFunctionArgs) => {
-	const query = zx.parseQuery(request, searchParamsSchema);
+	const query = parseSearchQuery(request, searchParamsSchema);
 	const isAuthenticated = !!getAuthorizationCookie(request);
 	if (isAuthenticated) {
 		throw await redirectWithToast(
@@ -193,8 +198,8 @@ const registerSchema = z
 const loginSchema = z.object({
 	username: z.string(),
 	password: z.string(),
+	tokenValidForDays: zodNumAsString,
 	[redirectToQueryParam]: z.string().optional(),
-	tokenValidForDays: zx.NumAsString,
 });
 
 export default function Page() {
diff --git a/apps/frontend/package.json b/apps/frontend/package.json
index a219f2f0fb..8959cbf9ed 100644
--- a/apps/frontend/package.json
+++ b/apps/frontend/package.json
@@ -63,8 +63,7 @@
 		"ufo": "1.5.4",
 		"usehooks-ts": "3.1.0",
 		"uuid": "11.0.5",
-		"zod": "3.24.1",
-		"zodix": "0.4.4"
+		"zod": "3.24.1"
 	},
 	"devDependencies": {
 		"@bitprojects/umami-logger-typescript": "1.0.10",
diff --git a/apps/website/app/lib/config.server.ts b/apps/website/app/lib/config.server.ts
index 9ee23b7a18..5404684312 100644
--- a/apps/website/app/lib/config.server.ts
+++ b/apps/website/app/lib/config.server.ts
@@ -1,7 +1,7 @@
 import { Environment, Paddle } from "@paddle/paddle-node-sdk";
 import { render } from "@react-email/render";
 import { createCookie } from "@remix-run/node";
-import { formatDateToNaiveDate } from "@ryot/ts-utils";
+import { formatDateToNaiveDate, zodBoolAsString } from "@ryot/ts-utils";
 import { Unkey } from "@unkey/api";
 import type { Dayjs } from "dayjs";
 import { eq } from "drizzle-orm";
@@ -11,7 +11,6 @@ import { createTransport } from "nodemailer";
 import { Issuer } from "openid-client";
 import { Honeypot } from "remix-utils/honeypot/server";
 import { z } from "zod";
-import { zx } from "zodix";
 import * as schema from "~/drizzle/schema.server";
 import { PlanTypes, ProductTypes } from "~/drizzle/schema.server";
 
@@ -37,7 +36,7 @@ export const serverVariablesSchema = z.object({
 	SERVER_OIDC_CLIENT_SECRET: z.string(),
 	SERVER_ADMIN_ACCESS_TOKEN: z.string(),
 	PADDLE_WEBHOOK_SECRET_KEY: z.string(),
-	PADDLE_SANDBOX: zx.BoolAsString.optional(),
+	PADDLE_SANDBOX: zodBoolAsString.optional(),
 });
 
 export const serverVariables = serverVariablesSchema.parse(process.env);
diff --git a/apps/website/app/routes/_index.tsx b/apps/website/app/routes/_index.tsx
index edcd01e56a..d5158d7fe8 100644
--- a/apps/website/app/routes/_index.tsx
+++ b/apps/website/app/routes/_index.tsx
@@ -13,7 +13,13 @@ import {
 	useRouteLoaderData,
 } from "@remix-run/react";
 import LoginCodeEmail from "@ryot/transactional/emails/LoginCode";
-import { cn, getActionIntent, processSubmission } from "@ryot/ts-utils";
+import {
+	cn,
+	getActionIntent,
+	parseSearchQuery,
+	processSubmission,
+	zodBoolAsString,
+} from "@ryot/ts-utils";
 import {
 	IconBrandDiscord,
 	IconBrandGithub,
@@ -29,7 +35,6 @@ import { SpamError } from "remix-utils/honeypot/server";
 import { match } from "ts-pattern";
 import { withFragment, withQuery } from "ufo";
 import { z } from "zod";
-import { zx } from "zodix";
 import { contactSubmissions, customers } from "~/drizzle/schema.server";
 import Pricing from "~/lib/components/Pricing";
 import { Button } from "~/lib/components/ui/button";
@@ -55,13 +60,13 @@ dayjs.extend(duration);
 
 const searchParamsSchema = z.object({
 	email: z.string().email().optional(),
-	contactSubmission: zx.BoolAsString.optional(),
+	contactSubmission: zodBoolAsString.optional(),
 });
 
 export type SearchParams = z.infer<typeof searchParamsSchema>;
 
 export const loader = async ({ request }: LoaderFunctionArgs) => {
-	const query = zx.parseQuery(request, searchParamsSchema);
+	const query = parseSearchQuery(request, searchParamsSchema);
 	return { prices, query };
 };
 
@@ -127,7 +132,10 @@ export const action = async ({ request }: ActionFunctionArgs) => {
 			} catch (e) {
 				if (e instanceof SpamError) isSpam = true;
 			}
-			const submission = await zx.parseForm(request, contactSubmissionSchema);
+			// DEV: https://github.com/edmundhung/conform/issues/854
+			const submission = contactSubmissionSchema.parse(
+				Object.fromEntries(formData.entries()),
+			);
 			await db
 				.insert(contactSubmissions)
 				.values({
diff --git a/apps/website/package.json b/apps/website/package.json
index 5c4974a000..4de92a163d 100644
--- a/apps/website/package.json
+++ b/apps/website/package.json
@@ -54,8 +54,7 @@
 		"tiny-invariant": "1.3.3",
 		"ts-pattern": "5.6.2",
 		"ufo": "1.5.4",
-		"zod": "3.24.1",
-		"zodix": "0.4.4"
+		"zod": "3.24.1"
 	},
 	"devDependencies": {
 		"@remix-run/dev": "2.15.2",
diff --git a/crates/models/background/src/lib.rs b/crates/models/background/src/lib.rs
index 0f821b05dc..3e7cefd6d4 100644
--- a/crates/models/background/src/lib.rs
+++ b/crates/models/background/src/lib.rs
@@ -31,7 +31,6 @@ pub enum MpApplicationJob {
 
 #[derive(Debug, Deserialize, Serialize, Display, Clone)]
 pub enum LpApplicationJob {
-    DeleteAllApplicationCache,
     HandleOnSeenComplete(String),
     HandleEntityAddedToCollectionEvent(Uuid),
     HandleAfterMediaSeenTasks(Box<seen::Model>),
diff --git a/crates/models/common/src/lib.rs b/crates/models/common/src/lib.rs
index 2f1b18434b..017eeb6dae 100644
--- a/crates/models/common/src/lib.rs
+++ b/crates/models/common/src/lib.rs
@@ -133,7 +133,6 @@ pub enum BackgroundJob {
     UpdateAllExercises,
     SyncIntegrationsData,
     PerformBackgroundTasks,
-    DeleteAllApplicationCache,
     CalculateUserActivitiesAndSummary,
 }
 
diff --git a/crates/services/integration/src/yank/youtube_music.rs b/crates/services/integration/src/yank/youtube_music.rs
index 5730efb7c5..867f794d25 100644
--- a/crates/services/integration/src/yank/youtube_music.rs
+++ b/crates/services/integration/src/yank/youtube_music.rs
@@ -19,7 +19,7 @@ fn get_end_of_day(date: NaiveDate) -> NaiveDateTime {
     date.and_hms_opt(23, 59, 59).unwrap()
 }
 
-fn get_offset(timezone: &String) -> i16 {
+fn get_offset(timezone: &str) -> i16 {
     let utc_now = Utc::now();
     let parsed = timezone.parse::<Tz>().unwrap();
     let local_time = utc_now.with_timezone(&parsed);
@@ -62,13 +62,9 @@ pub async fn yank_progress(
         .await
         .unwrap();
     let songs_listened_to_today = music_history.items.into_iter().rev().filter_map(|history| {
-        history.playback_date.and_then(|d| {
-            let yt_date =
-                NaiveDate::from_ymd_opt(d.year(), d.month() as u32, d.day().into()).unwrap();
-            match yt_date == date {
-                false => None,
-                true => Some((history.item.id, history.item.name)),
-            }
+        history.playback_date_txt.and_then(|d| match d.as_str() {
+            "Today" => Some((history.item.id, history.item.name)),
+            _ => None,
         })
     });
     let cache_keys = songs_listened_to_today
diff --git a/crates/services/miscellaneous/src/lib.rs b/crates/services/miscellaneous/src/lib.rs
index a752a03ec3..3f7c105757 100644
--- a/crates/services/miscellaneous/src/lib.rs
+++ b/crates/services/miscellaneous/src/lib.rs
@@ -17,7 +17,8 @@ use common_models::{
     UserLevelCacheKey, UserNotificationContent,
 };
 use common_utils::{
-    get_first_and_last_day_of_month, ryot_log, PAGE_SIZE, SHOW_SPECIAL_SEASON_NAMES,
+    get_first_and_last_day_of_month, ryot_log, ENTITY_BULK_DELETE_CHUNK_SIZE,
+    ENTITY_BULK_UPDATE_CHUNK_SIZE, PAGE_SIZE, SHOW_SPECIAL_SEASON_NAMES,
 };
 use convert_case::{Case, Casing};
 use database_models::{
@@ -108,8 +109,6 @@ use traits::{MediaProvider, TraceOk};
 use user_models::DashboardElementLot;
 use uuid::Uuid;
 
-static ENTITY_UPDATE_CHUNK_SIZE: usize = 5;
-
 type Provider = Box<(dyn MediaProvider + Send + Sync)>;
 
 pub struct MiscellaneousService(pub Arc<SupportingService>);
@@ -1735,7 +1734,7 @@ ORDER BY RANDOM() LIMIT 10;
             "Users to be notified for metadata state changes: {:?}",
             m_map
         );
-        let chunks = m_map.keys().chunks(ENTITY_UPDATE_CHUNK_SIZE);
+        let chunks = m_map.keys().chunks(ENTITY_BULK_UPDATE_CHUNK_SIZE);
         let items = chunks
             .into_iter()
             .map(|chunk| chunk.into_iter().collect_vec())
@@ -1756,7 +1755,7 @@ ORDER BY RANDOM() LIMIT 10;
             "Users to be notified for people state changes: {:?}",
             p_map
         );
-        let chunks = p_map.keys().chunks(ENTITY_UPDATE_CHUNK_SIZE);
+        let chunks = p_map.keys().chunks(ENTITY_BULK_UPDATE_CHUNK_SIZE);
         let items = chunks
             .into_iter()
             .map(|chunk| chunk.into_iter().collect_vec())
@@ -2402,11 +2401,6 @@ ORDER BY RANDOM() LIMIT 10;
         Ok(notifications)
     }
 
-    pub async fn delete_all_application_cache(&self) -> Result<()> {
-        ApplicationCache::delete_many().exec(&self.0.db).await?;
-        Ok(())
-    }
-
     pub async fn update_metadata_and_notify_users(&self, metadata_id: &String) -> Result<()> {
         update_metadata_and_notify_users(metadata_id, &self.0).await
     }
@@ -2586,7 +2580,7 @@ ORDER BY RANDOM() LIMIT 10;
     }
 
     pub async fn remove_useless_data(&self) -> Result<()> {
-        let all_metadata = Metadata::find()
+        let metadata_to_delete = Metadata::find()
             .select_only()
             .column(metadata::Column::Id)
             .left_join(UserToEntity)
@@ -2594,12 +2588,14 @@ ORDER BY RANDOM() LIMIT 10;
             .into_tuple::<String>()
             .all(&self.0.db)
             .await?;
-        ryot_log!(debug, "Deleting {} metadata items", all_metadata.len());
-        Metadata::delete_many()
-            .filter(metadata::Column::Id.is_in(all_metadata))
-            .exec(&self.0.db)
-            .await?;
-        let all_people = Person::find()
+        for chunk in metadata_to_delete.chunks(ENTITY_BULK_DELETE_CHUNK_SIZE) {
+            ryot_log!(debug, "Deleting {} metadata items", chunk.len());
+            Metadata::delete_many()
+                .filter(metadata::Column::Id.is_in(chunk))
+                .exec(&self.0.db)
+                .await?;
+        }
+        let people_to_delete = Person::find()
             .select_only()
             .column(person::Column::Id)
             .left_join(UserToEntity)
@@ -2607,12 +2603,14 @@ ORDER BY RANDOM() LIMIT 10;
             .into_tuple::<String>()
             .all(&self.0.db)
             .await?;
-        ryot_log!(debug, "Deleting {} people", all_people.len());
-        Person::delete_many()
-            .filter(person::Column::Id.is_in(all_people))
-            .exec(&self.0.db)
-            .await?;
-        let all_metadata_groups = MetadataGroup::find()
+        for chunk in people_to_delete.chunks(ENTITY_BULK_DELETE_CHUNK_SIZE) {
+            ryot_log!(debug, "Deleting {} people", chunk.len());
+            Person::delete_many()
+                .filter(person::Column::Id.is_in(chunk))
+                .exec(&self.0.db)
+                .await?;
+        }
+        let metadata_groups_to_delete = MetadataGroup::find()
             .select_only()
             .column(metadata_group::Column::Id)
             .left_join(UserToEntity)
@@ -2620,16 +2618,14 @@ ORDER BY RANDOM() LIMIT 10;
             .into_tuple::<String>()
             .all(&self.0.db)
             .await?;
-        ryot_log!(
-            debug,
-            "Deleting {} metadata groups",
-            all_metadata_groups.len()
-        );
-        MetadataGroup::delete_many()
-            .filter(metadata_group::Column::Id.is_in(all_metadata_groups))
-            .exec(&self.0.db)
-            .await?;
-        let all_genre = Genre::find()
+        for chunk in metadata_groups_to_delete.chunks(ENTITY_BULK_DELETE_CHUNK_SIZE) {
+            ryot_log!(debug, "Deleting {} metadata groups", chunk.len());
+            MetadataGroup::delete_many()
+                .filter(metadata_group::Column::Id.is_in(chunk))
+                .exec(&self.0.db)
+                .await?;
+        }
+        let genre_to_delete = Genre::find()
             .select_only()
             .column(genre::Column::Id)
             .left_join(MetadataToGenre)
@@ -2637,11 +2633,13 @@ ORDER BY RANDOM() LIMIT 10;
             .into_tuple::<String>()
             .all(&self.0.db)
             .await?;
-        ryot_log!(debug, "Deleting {} genres", all_genre.len());
-        Genre::delete_many()
-            .filter(genre::Column::Id.is_in(all_genre))
-            .exec(&self.0.db)
-            .await?;
+        for chunk in genre_to_delete.chunks(ENTITY_BULK_DELETE_CHUNK_SIZE) {
+            ryot_log!(debug, "Deleting {} genres", chunk.len());
+            Genre::delete_many()
+                .filter(genre::Column::Id.is_in(chunk))
+                .exec(&self.0.db)
+                .await?;
+        }
         ryot_log!(debug, "Deleting all addressed user notifications");
         UserNotification::delete_many()
             .filter(user_notification::Column::IsAddressed.eq(true))
@@ -2834,6 +2832,20 @@ ORDER BY RANDOM() LIMIT 10;
         Ok(())
     }
 
+    pub async fn mark_old_display_user_notifications_as_addressed(&self) -> Result<()> {
+        let threshold = Utc::now() - Duration::days(1);
+        UserNotification::update_many()
+            .filter(user_notification::Column::CreatedOn.lt(threshold))
+            .filter(user_notification::Column::Lot.eq(UserNotificationLot::Display))
+            .col_expr(
+                user_notification::Column::IsAddressed,
+                Expr::val(true).into(),
+            )
+            .exec(&self.0.db)
+            .await?;
+        Ok(())
+    }
+
     pub async fn perform_background_jobs(&self) -> Result<()> {
         ryot_log!(debug, "Starting background jobs...");
 
@@ -2878,7 +2890,7 @@ ORDER BY RANDOM() LIMIT 10;
         ryot_log!(trace, "Putting entities in partial state");
         self.put_entities_in_partial_state().await.trace_ok();
         // DEV: This is called after removing useless data so that recommendations are not
-        // delete right after they are downloaded.
+        // deleted right after they are downloaded.
         ryot_log!(trace, "Downloading recommendations for users");
         self.update_claimed_recommendations_and_download_new_ones()
             .await
@@ -2887,6 +2899,10 @@ ORDER BY RANDOM() LIMIT 10;
         // function after removing useless data.
         ryot_log!(trace, "Revoking invalid access tokens");
         self.revoke_invalid_access_tokens().await.trace_ok();
+        ryot_log!(trace, "Marking old display notifications as addressed");
+        self.mark_old_display_user_notifications_as_addressed()
+            .await
+            .trace_ok();
 
         ryot_log!(debug, "Completed background jobs...");
         Ok(())
diff --git a/crates/services/user/src/lib.rs b/crates/services/user/src/lib.rs
index f7e4f26e50..ec090edeab 100644
--- a/crates/services/user/src/lib.rs
+++ b/crates/services/user/src/lib.rs
@@ -1,4 +1,4 @@
-use std::{collections::HashSet, sync::Arc};
+use std::{collections::HashSet, sync::Arc, time::Instant};
 
 use application_utils::user_id_from_token;
 use argon2::{Argon2, PasswordHash, PasswordVerifier};
@@ -96,12 +96,16 @@ impl UserService {
         if enabled.is_empty() {
             return Ok(Vec::new());
         }
+        let started_at = Instant::now();
         let mut recommendations = HashSet::new();
-        for i in 0..10 {
-            if recommendations.len() >= limit.try_into().unwrap() {
+        for i in 0.. {
+            let now = Instant::now();
+            if recommendations.len() >= limit.try_into().unwrap()
+                || now.duration_since(started_at).as_secs() > 5
+            {
                 break;
             }
-            ryot_log!(debug, "Generating recommendation in loop: {}", i);
+            ryot_log!(debug, "Recommendations loop {} for user: {}", i, user_id);
             let selected_lot = enabled.choose(&mut rand::rng()).unwrap();
             let rec = Metadata::find()
                 .select_only()
diff --git a/crates/utils/application/src/lib.rs b/crates/utils/application/src/lib.rs
index 8b12224e2e..70e30fa6fb 100644
--- a/crates/utils/application/src/lib.rs
+++ b/crates/utils/application/src/lib.rs
@@ -31,7 +31,6 @@ pub struct AuthContext {
     pub user_id: Option<String>,
 }
 
-#[async_trait]
 impl<S> FromRequestParts<S> for AuthContext
 where
     S: Send + Sync,
diff --git a/crates/utils/common/src/lib.rs b/crates/utils/common/src/lib.rs
index 4fb965d4ed..cc1e87fddb 100644
--- a/crates/utils/common/src/lib.rs
+++ b/crates/utils/common/src/lib.rs
@@ -9,6 +9,8 @@ use tokio::time::{sleep, Duration};
 
 pub const PROJECT_NAME: &str = "ryot";
 pub const AUTHOR: &str = "ignisda";
+pub static ENTITY_BULK_UPDATE_CHUNK_SIZE: usize = 5;
+pub static ENTITY_BULK_DELETE_CHUNK_SIZE: usize = 2000;
 pub const AUTHOR_EMAIL: &str = "ignisda2001@gmail.com";
 pub const USER_AGENT_STR: &str = const_str::concat!(
     AUTHOR,
diff --git a/crates/utils/dependent/src/lib.rs b/crates/utils/dependent/src/lib.rs
index 46da5f68a6..daecc86637 100644
--- a/crates/utils/dependent/src/lib.rs
+++ b/crates/utils/dependent/src/lib.rs
@@ -869,7 +869,6 @@ pub async fn deploy_background_job(
     match job_name {
         BackgroundJob::UpdateAllMetadata
         | BackgroundJob::UpdateAllExercises
-        | BackgroundJob::DeleteAllApplicationCache
         | BackgroundJob::PerformBackgroundTasks => {
             admin_account_guard(user_id, ss).await?;
         }
@@ -893,12 +892,6 @@ pub async fn deploy_background_job(
                 deploy_update_metadata_job(&metadata_id, ss).await?;
             }
         }
-        BackgroundJob::DeleteAllApplicationCache => {
-            ss.perform_application_job(ApplicationJob::Lp(
-                LpApplicationJob::DeleteAllApplicationCache,
-            ))
-            .await?;
-        }
         BackgroundJob::UpdateAllExercises => {
             ss.perform_application_job(ApplicationJob::Mp(MpApplicationJob::UpdateExerciseLibrary))
                 .await?;
diff --git a/docs/content/integrations.md b/docs/content/integrations.md
index afb7aaf105..ebca059643 100644
--- a/docs/content/integrations.md
+++ b/docs/content/integrations.md
@@ -169,15 +169,15 @@ want to sync media progress, then take a look at the [Plex Sink](#plex-sink) int
 L|Pro Version|https://ryot.io|
 
 The [Youtube Music](https://music.youtube.com) integration syncs all music that you have
-listened to ["Yesterday"](https://music.youtube.com/history). Since Youtube Music does not
-have an official API, this integration is prone to breakage and needs some roundabout steps
-to setup.
+listened to ["Today"](https://music.youtube.com/history). Since Youtube Music does not have
+an official API, this integration is prone to breakage and needs some roundabout steps to
+setup.
 
 1. Install the [Cookie Editor](https://cookie-editor.com) extension in your browser. Make
    sure you allow the extension to work on incognito windows.
 2. Open a new incognito window in your browser and login to Youtube Music.
 3. Once logged in, open the extension and export the cookies as "Header String". After
-   copying them, close the browser immediately so that they are not invalidated.
+   copying them, close the incognito browser immediately so that they are not invalidated.
   ![image](./images/integrations_youtube-music-export-cookies.png)
 4. Paste the exported cookies in the input.
 
diff --git a/libs/generated/src/graphql/backend/graphql.ts b/libs/generated/src/graphql/backend/graphql.ts
index ffcba1b04f..6c5b3b9402 100644
--- a/libs/generated/src/graphql/backend/graphql.ts
+++ b/libs/generated/src/graphql/backend/graphql.ts
@@ -114,7 +114,6 @@ export enum BackendError {
 
 export enum BackgroundJob {
   CalculateUserActivitiesAndSummary = 'CALCULATE_USER_ACTIVITIES_AND_SUMMARY',
-  DeleteAllApplicationCache = 'DELETE_ALL_APPLICATION_CACHE',
   PerformBackgroundTasks = 'PERFORM_BACKGROUND_TASKS',
   ReviseUserWorkouts = 'REVISE_USER_WORKOUTS',
   SyncIntegrationsData = 'SYNC_INTEGRATIONS_DATA',
diff --git a/libs/generated/src/graphql/backend/types.generated.ts b/libs/generated/src/graphql/backend/types.generated.ts
index 691b7f2823..481fe3fe2e 100644
--- a/libs/generated/src/graphql/backend/types.generated.ts
+++ b/libs/generated/src/graphql/backend/types.generated.ts
@@ -94,7 +94,6 @@ export enum BackendError {
 
 export enum BackgroundJob {
   CalculateUserActivitiesAndSummary = 'CALCULATE_USER_ACTIVITIES_AND_SUMMARY',
-  DeleteAllApplicationCache = 'DELETE_ALL_APPLICATION_CACHE',
   PerformBackgroundTasks = 'PERFORM_BACKGROUND_TASKS',
   ReviseUserWorkouts = 'REVISE_USER_WORKOUTS',
   SyncIntegrationsData = 'SYNC_INTEGRATIONS_DATA',
diff --git a/libs/ts-utils/package.json b/libs/ts-utils/package.json
index 787dd699e1..e73b389661 100644
--- a/libs/ts-utils/package.json
+++ b/libs/ts-utils/package.json
@@ -8,6 +8,7 @@
 		"dayjs": "1.11.13",
 		"humanize-duration-ts": "2.1.1",
 		"lodash": "4.17.21",
+		"react-router": "7.1.3",
 		"tailwind-merge": "2.6.0",
 		"tiny-invariant": "1.3.3",
 		"zod": "3.24.1"
diff --git a/libs/ts-utils/src/index.ts b/libs/ts-utils/src/index.ts
index 47d714882d..40b2b1d8ad 100644
--- a/libs/ts-utils/src/index.ts
+++ b/libs/ts-utils/src/index.ts
@@ -1,4 +1,5 @@
 import { parseWithZod } from "@conform-to/zod";
+import type { Params } from "react-router";
 import { type ClassValue, clsx } from "clsx";
 import dayjs, { type Dayjs } from "dayjs";
 import {
@@ -29,7 +30,27 @@ import sum from "lodash/sum";
 import truncate from "lodash/truncate";
 import { twMerge } from "tailwind-merge";
 import invariant from "tiny-invariant";
-import type { ZodTypeAny, output } from "zod";
+import { z, type ZodTypeAny, type output } from "zod";
+
+export const zodBoolAsString = z
+	.string()
+	.regex(/^(true|false)$/, 'Must be a boolean string ("true" or "false")')
+	.transform((value) => value === "true");
+
+export const zodCheckboxAsString = z
+	.literal("on")
+	.optional()
+	.transform((value) => value === "on");
+
+export const zodIntAsString = z
+	.string()
+	.regex(/^-?\d+$/, "Must be an integer string")
+	.transform((val) => Number.parseInt(val, 10));
+
+export const zodNumAsString = z
+	.string()
+	.regex(/^-?\d*\.?\d+$/, "Must be a number string")
+	.transform(Number);
 
 /**
  * Humanize a duration.
@@ -93,6 +114,24 @@ export const getActionIntent = (request: Request) => {
 	return intent;
 };
 
+export const parseSearchQuery = <Schema extends ZodTypeAny>(
+	request: Request,
+	schema: Schema,
+): output<Schema> => {
+	const entries = Object.fromEntries(
+		new URL(request.url).searchParams.entries(),
+	);
+	return schema.parse(entries);
+};
+
+export const parseParameters = <Schema extends ZodTypeAny>(
+	params: Params,
+	schema: Schema,
+): output<Schema> => {
+	const parsed = schema.parse(params);
+	return parsed;
+};
+
 export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs));
 
 export {
diff --git a/yarn.lock b/yarn.lock
index c18ee5dd97..e756afe2f2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6044,7 +6044,6 @@ __metadata:
     vite: "npm:5.4.14"
     vite-tsconfig-paths: "npm:5.1.4"
     zod: "npm:3.24.1"
-    zodix: "npm:0.4.4"
   languageName: unknown
   linkType: soft
 
@@ -6103,6 +6102,7 @@ __metadata:
     dayjs: "npm:1.11.13"
     humanize-duration-ts: "npm:2.1.1"
     lodash: "npm:4.17.21"
+    react-router: "npm:7.1.3"
     tailwind-merge: "npm:2.6.0"
     tiny-invariant: "npm:1.3.3"
     zod: "npm:3.24.1"
@@ -6175,7 +6175,6 @@ __metadata:
     vite: "npm:5.4.14"
     vite-tsconfig-paths: "npm:5.1.4"
     zod: "npm:3.24.1"
-    zodix: "npm:0.4.4"
   languageName: unknown
   linkType: soft
 
@@ -8243,7 +8242,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"cookie@npm:1.0.2":
+"cookie@npm:1.0.2, cookie@npm:^1.0.1":
   version: 1.0.2
   resolution: "cookie@npm:1.0.2"
   checksum: 10/f5817cdc84d8977761b12549eba29435e675e65c7fef172bc31737788cd8adc83796bf8abe6d950554e7987325ad2d9ac2971c5bd8ff0c4f81c145f82e4ab1be
@@ -14681,6 +14680,24 @@ __metadata:
   languageName: node
   linkType: hard
 
+"react-router@npm:7.1.3":
+  version: 7.1.3
+  resolution: "react-router@npm:7.1.3"
+  dependencies:
+    "@types/cookie": "npm:^0.6.0"
+    cookie: "npm:^1.0.1"
+    set-cookie-parser: "npm:^2.6.0"
+    turbo-stream: "npm:2.4.0"
+  peerDependencies:
+    react: ">=18"
+    react-dom: ">=18"
+  peerDependenciesMeta:
+    react-dom:
+      optional: true
+  checksum: 10/84b292e4947e5f9e0c5bae4059f28faf0b4c21944dc27707563188f001f006e2a62f0b5cfefda925f890629f40a237230c4e2a8572a64763e2cf809b504c0d87
+  languageName: node
+  linkType: hard
+
 "react-smooth@npm:^4.0.0":
   version: 4.0.0
   resolution: "react-smooth@npm:4.0.0"
@@ -17607,16 +17624,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"zodix@npm:0.4.4":
-  version: 0.4.4
-  resolution: "zodix@npm:0.4.4"
-  peerDependencies:
-    "@remix-run/server-runtime": 1.x || 2.x
-    zod: 3.x
-  checksum: 10/23e1844faa337903ab0928cb3f35abbf334133d9c5432aa3022b4c992e712269028519f079a7b480fd852b913da11b6ab0eb3237167ebcb5cadff641755461e4
-  languageName: node
-  linkType: hard
-
 "zwitch@npm:^2.0.0":
   version: 2.0.4
   resolution: "zwitch@npm:2.0.4"


Test patch: