diff --git a/build.sbt b/build.sbt
index 1c4c170b..3a1ca00a 100644
--- a/build.sbt
+++ b/build.sbt
@@ -86,7 +86,7 @@ val stylesSettings = Seq(
   Compile / resourceGenerators += stylesBuild.taskValue
 )
 
-val webjarSettings = Seq(
+def webjarSettings(queryJS: Project) = Seq(
   Compile / resourceGenerators += Def.task {
     copyWebjarResources(
       Seq((sourceDirectory in Compile).value / "webjar"),
@@ -96,6 +96,18 @@ val webjarSettings = Seq(
       streams.value.log
     )
   }.taskValue,
+  Compile / resourceGenerators += Def.task {
+    val logger = streams.value.log
+    val out = (queryJS/Compile/fullOptJS).value
+    logger.info(s"Produced query js file: ${out.data}")
+    copyWebjarResources(
+      Seq(out.data),
+      (Compile/resourceManaged).value,
+      name.value,
+      version.value,
+      logger
+    )
+  }.taskValue,
   watchSources += Watched.WatchSource(
     (Compile / sourceDirectory).value / "webjar",
     FileFilter.globFilter("*.js") || FileFilter.globFilter("*.css"),
@@ -273,7 +285,6 @@ ${lines.map(_._1).mkString(",\n")}
 val query =
   crossProject(JSPlatform, JVMPlatform)
     .withoutSuffixFor(JVMPlatform)
-    .crossType(CrossType.Pure)
     .in(file("modules/query"))
     .disablePlugins(RevolverPlugin)
     .settings(sharedSettings)
@@ -446,7 +457,7 @@ val webapp = project
   .settings(sharedSettings)
   .settings(elmSettings)
   .settings(stylesSettings)
-  .settings(webjarSettings)
+  .settings(webjarSettings(query.js))
   .settings(
     name := "docspell-webapp",
     openapiTargetLanguage := Language.Elm,
diff --git a/modules/query/js/src/main/scala/docspell/query/js/JSItemQueryParser.scala b/modules/query/js/src/main/scala/docspell/query/js/JSItemQueryParser.scala
new file mode 100644
index 00000000..68f7cd5e
--- /dev/null
+++ b/modules/query/js/src/main/scala/docspell/query/js/JSItemQueryParser.scala
@@ -0,0 +1,29 @@
+package docspell.query.js
+
+import scala.scalajs.js
+import scala.scalajs.js.annotation._
+
+import docspell.query.ItemQueryParser
+
+@JSExportTopLevel("DsItemQueryParser")
+object JSItemQueryParser {
+
+  @JSExport
+  def parseToFailure(input: String): Failure =
+    ItemQueryParser
+      .parse(input)
+      .swap
+      .toOption
+      .map(fr =>
+        new Failure(
+          fr.input,
+          fr.failedAt,
+          js.Array(fr.messages.toList.toSeq.map(_.msg): _*)
+        )
+      )
+      .orNull
+
+  @JSExportAll
+  case class Failure(input: String, failedAt: Int, messages: js.Array[String])
+
+}
diff --git a/modules/query/src/main/scala/docspell/query/Date.scala b/modules/query/shared/src/main/scala/docspell/query/Date.scala
similarity index 100%
rename from modules/query/src/main/scala/docspell/query/Date.scala
rename to modules/query/shared/src/main/scala/docspell/query/Date.scala
diff --git a/modules/query/src/main/scala/docspell/query/ItemQuery.scala b/modules/query/shared/src/main/scala/docspell/query/ItemQuery.scala
similarity index 100%
rename from modules/query/src/main/scala/docspell/query/ItemQuery.scala
rename to modules/query/shared/src/main/scala/docspell/query/ItemQuery.scala
diff --git a/modules/query/src/main/scala/docspell/query/ItemQueryParser.scala b/modules/query/shared/src/main/scala/docspell/query/ItemQueryParser.scala
similarity index 86%
rename from modules/query/src/main/scala/docspell/query/ItemQueryParser.scala
rename to modules/query/shared/src/main/scala/docspell/query/ItemQueryParser.scala
index cf9b491c..2c178140 100644
--- a/modules/query/src/main/scala/docspell/query/ItemQueryParser.scala
+++ b/modules/query/shared/src/main/scala/docspell/query/ItemQueryParser.scala
@@ -1,14 +1,10 @@
 package docspell.query
 
-import scala.scalajs.js.annotation._
-
 import docspell.query.internal.ExprParser
 import docspell.query.internal.ExprUtil
 
-@JSExportTopLevel("DsItemQueryParser")
 object ItemQueryParser {
 
-  @JSExport
   def parse(input: String): Either[ParseFailure, ItemQuery] =
     if (input.isEmpty) Right(ItemQuery.all)
     else {
@@ -22,5 +18,4 @@ object ItemQueryParser {
 
   def parseUnsafe(input: String): ItemQuery =
     parse(input).fold(m => sys.error(m.render), identity)
-
 }
diff --git a/modules/query/src/main/scala/docspell/query/ParseFailure.scala b/modules/query/shared/src/main/scala/docspell/query/ParseFailure.scala
similarity index 100%
rename from modules/query/src/main/scala/docspell/query/ParseFailure.scala
rename to modules/query/shared/src/main/scala/docspell/query/ParseFailure.scala
diff --git a/modules/query/src/main/scala/docspell/query/internal/AttrParser.scala b/modules/query/shared/src/main/scala/docspell/query/internal/AttrParser.scala
similarity index 100%
rename from modules/query/src/main/scala/docspell/query/internal/AttrParser.scala
rename to modules/query/shared/src/main/scala/docspell/query/internal/AttrParser.scala
diff --git a/modules/query/src/main/scala/docspell/query/internal/BasicParser.scala b/modules/query/shared/src/main/scala/docspell/query/internal/BasicParser.scala
similarity index 100%
rename from modules/query/src/main/scala/docspell/query/internal/BasicParser.scala
rename to modules/query/shared/src/main/scala/docspell/query/internal/BasicParser.scala
diff --git a/modules/query/src/main/scala/docspell/query/internal/DateParser.scala b/modules/query/shared/src/main/scala/docspell/query/internal/DateParser.scala
similarity index 100%
rename from modules/query/src/main/scala/docspell/query/internal/DateParser.scala
rename to modules/query/shared/src/main/scala/docspell/query/internal/DateParser.scala
diff --git a/modules/query/src/main/scala/docspell/query/internal/ExprParser.scala b/modules/query/shared/src/main/scala/docspell/query/internal/ExprParser.scala
similarity index 100%
rename from modules/query/src/main/scala/docspell/query/internal/ExprParser.scala
rename to modules/query/shared/src/main/scala/docspell/query/internal/ExprParser.scala
diff --git a/modules/query/src/main/scala/docspell/query/internal/ExprUtil.scala b/modules/query/shared/src/main/scala/docspell/query/internal/ExprUtil.scala
similarity index 100%
rename from modules/query/src/main/scala/docspell/query/internal/ExprUtil.scala
rename to modules/query/shared/src/main/scala/docspell/query/internal/ExprUtil.scala
diff --git a/modules/query/src/main/scala/docspell/query/internal/MacroParser.scala b/modules/query/shared/src/main/scala/docspell/query/internal/MacroParser.scala
similarity index 100%
rename from modules/query/src/main/scala/docspell/query/internal/MacroParser.scala
rename to modules/query/shared/src/main/scala/docspell/query/internal/MacroParser.scala
diff --git a/modules/query/src/main/scala/docspell/query/internal/OperatorParser.scala b/modules/query/shared/src/main/scala/docspell/query/internal/OperatorParser.scala
similarity index 100%
rename from modules/query/src/main/scala/docspell/query/internal/OperatorParser.scala
rename to modules/query/shared/src/main/scala/docspell/query/internal/OperatorParser.scala
diff --git a/modules/query/src/main/scala/docspell/query/internal/SimpleExprParser.scala b/modules/query/shared/src/main/scala/docspell/query/internal/SimpleExprParser.scala
similarity index 100%
rename from modules/query/src/main/scala/docspell/query/internal/SimpleExprParser.scala
rename to modules/query/shared/src/main/scala/docspell/query/internal/SimpleExprParser.scala
diff --git a/modules/query/src/main/scala/docspell/query/internal/StringUtil.scala b/modules/query/shared/src/main/scala/docspell/query/internal/StringUtil.scala
similarity index 100%
rename from modules/query/src/main/scala/docspell/query/internal/StringUtil.scala
rename to modules/query/shared/src/main/scala/docspell/query/internal/StringUtil.scala
diff --git a/modules/query/src/test/scala/docspell/query/internal/AttrParserTest.scala b/modules/query/shared/src/test/scala/docspell/query/internal/AttrParserTest.scala
similarity index 100%
rename from modules/query/src/test/scala/docspell/query/internal/AttrParserTest.scala
rename to modules/query/shared/src/test/scala/docspell/query/internal/AttrParserTest.scala
diff --git a/modules/query/src/test/scala/docspell/query/internal/BasicParserTest.scala b/modules/query/shared/src/test/scala/docspell/query/internal/BasicParserTest.scala
similarity index 100%
rename from modules/query/src/test/scala/docspell/query/internal/BasicParserTest.scala
rename to modules/query/shared/src/test/scala/docspell/query/internal/BasicParserTest.scala
diff --git a/modules/query/src/test/scala/docspell/query/internal/DateParserTest.scala b/modules/query/shared/src/test/scala/docspell/query/internal/DateParserTest.scala
similarity index 100%
rename from modules/query/src/test/scala/docspell/query/internal/DateParserTest.scala
rename to modules/query/shared/src/test/scala/docspell/query/internal/DateParserTest.scala
diff --git a/modules/query/src/test/scala/docspell/query/internal/ExprParserTest.scala b/modules/query/shared/src/test/scala/docspell/query/internal/ExprParserTest.scala
similarity index 100%
rename from modules/query/src/test/scala/docspell/query/internal/ExprParserTest.scala
rename to modules/query/shared/src/test/scala/docspell/query/internal/ExprParserTest.scala
diff --git a/modules/query/src/test/scala/docspell/query/internal/ItemQueryParserTest.scala b/modules/query/shared/src/test/scala/docspell/query/internal/ItemQueryParserTest.scala
similarity index 100%
rename from modules/query/src/test/scala/docspell/query/internal/ItemQueryParserTest.scala
rename to modules/query/shared/src/test/scala/docspell/query/internal/ItemQueryParserTest.scala
diff --git a/modules/query/src/test/scala/docspell/query/internal/MacroParserTest.scala b/modules/query/shared/src/test/scala/docspell/query/internal/MacroParserTest.scala
similarity index 100%
rename from modules/query/src/test/scala/docspell/query/internal/MacroParserTest.scala
rename to modules/query/shared/src/test/scala/docspell/query/internal/MacroParserTest.scala
diff --git a/modules/query/src/test/scala/docspell/query/internal/OperatorParserTest.scala b/modules/query/shared/src/test/scala/docspell/query/internal/OperatorParserTest.scala
similarity index 100%
rename from modules/query/src/test/scala/docspell/query/internal/OperatorParserTest.scala
rename to modules/query/shared/src/test/scala/docspell/query/internal/OperatorParserTest.scala
diff --git a/modules/query/src/test/scala/docspell/query/internal/SimpleExprParserTest.scala b/modules/query/shared/src/test/scala/docspell/query/internal/SimpleExprParserTest.scala
similarity index 100%
rename from modules/query/src/test/scala/docspell/query/internal/SimpleExprParserTest.scala
rename to modules/query/shared/src/test/scala/docspell/query/internal/SimpleExprParserTest.scala
diff --git a/modules/query/src/test/scala/docspell/query/internal/ValueHelper.scala b/modules/query/shared/src/test/scala/docspell/query/internal/ValueHelper.scala
similarity index 100%
rename from modules/query/src/test/scala/docspell/query/internal/ValueHelper.scala
rename to modules/query/shared/src/test/scala/docspell/query/internal/ValueHelper.scala
diff --git a/modules/restserver/src/main/scala/docspell/restserver/webapp/TemplateRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/webapp/TemplateRoutes.scala
index b920168a..9d59c201 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/webapp/TemplateRoutes.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/webapp/TemplateRoutes.scala
@@ -170,7 +170,8 @@ object TemplateRoutes {
         chooseUi(uiVersion),
         Seq(
           "/app/assets" + Webjars.clipboardjs + "/clipboard.min.js",
-          s"/app/assets/docspell-webapp/${BuildInfo.version}/docspell-app.js"
+          s"/app/assets/docspell-webapp/${BuildInfo.version}/docspell-app.js",
+          s"/app/assets/docspell-webapp/${BuildInfo.version}/docspell-query-opt.js"
         ),
         s"/app/assets/docspell-webapp/${BuildInfo.version}/favicon",
         s"/app/assets/docspell-webapp/${BuildInfo.version}/docspell.js",