This is an automated email from the ASF dual-hosted git repository.

piotr pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iggy.git


The following commit(s) were added to refs/heads/master by this push:
     new ff6695ba5 feat(server,web): embed Web UI into server behind iggy-web 
feature (#2485)
ff6695ba5 is described below

commit ff6695ba589656acef68da534b4af55dda452c80
Author: Piotr Gankiewicz <[email protected]>
AuthorDate: Sun Dec 14 16:58:14 2025 +0100

    feat(server,web): embed Web UI into server behind iggy-web feature (#2485)
    
    Adds optional `iggy-web` feature to embed the Svelte dashboard at `/ui`
    directly in the server binary using `rust-embed`.
    
    - Added `iggy-web` feature with embedded static asset serving
    - Refactored web app from SSR to SPA for static build support
    - Added `build:static` script with `/ui` base path
    - Client-side auth (cookies, JWT) instead of server-side
    - Added `authorization` to CORS allowed headers
    - Updated Rust & Web dependencies
---
 Cargo.lock                                         | 409 ++++++++++++---------
 Cargo.toml                                         |   2 +-
 DEPENDENCIES.md                                    |  81 ++--
 bdd/rust/Cargo.toml                                |   2 +-
 core/bench/dashboard/server/Cargo.toml             |   2 +-
 core/common/Cargo.toml                             |   2 +-
 core/configs/server.toml                           |   2 +-
 core/integration/Cargo.toml                        |   2 +-
 core/server/Cargo.toml                             |   3 +
 core/server/src/http/http_server.rs                |   6 +
 core/server/src/http/jwt/middleware.rs             |   1 +
 core/server/src/http/mod.rs                        |   2 +
 core/server/src/http/web.rs                        |  83 +++++
 web/package-lock.json                              | 111 +++---
 web/package.json                                   |   7 +-
 web/src/hooks.client.ts                            |  11 +-
 web/src/hooks.server.ts                            |  73 ----
 web/src/lib/api/ApiSchema.ts                       |   6 +-
 web/src/lib/api/clientApi.ts                       | 106 ++++++
 web/src/lib/api/fetchRouteApi.ts                   |  35 +-
 web/src/lib/api/handleFetchErrors.ts               |   6 +-
 web/src/lib/auth/authStore.svelte.ts               | 125 +++++++
 web/src/lib/components/Breadcrumbs.svelte          |   2 +-
 web/src/lib/components/Header.svelte               |  21 +-
 .../lib/components/Layouts/SettingsLayout.svelte   |  10 +-
 web/src/lib/components/Logo/Logo.svelte            |   5 +-
 web/src/lib/components/Navbar.svelte               |  15 +-
 web/src/lib/types/appRoutes.ts                     |  10 +-
 web/src/routes/+layout.ts                          |  64 ++++
 web/src/routes/api/proxy/+server.ts                |  43 ---
 .../auth/logout/+page.svelte}                      |  19 +-
 .../auth/logout/{+page.server.ts => +page.ts}      |  27 +-
 web/src/routes/auth/sign-in/+page.server.ts        |  77 ----
 web/src/routes/auth/sign-in/+page.svelte           | 100 +++--
 .../{logout/+page.server.ts => sign-in/+page.ts}   |  26 +-
 .../dashboard/{+layout.server.ts => +layout.ts}    |  45 ++-
 .../overview/{+page.server.ts => +page.ts}         |  13 +-
 .../dashboard/settings/server/+page.server.ts      |  39 --
 .../settings/server/+page.ts}                      |  13 +-
 .../settings/users/{+page.server.ts => +page.ts}   |  31 +-
 web/src/routes/dashboard/streams/+layout.server.ts |  37 --
 web/src/routes/dashboard/streams/+layout.svelte    |   5 +-
 .../+page.server.ts => streams/+layout.ts}         |  17 +-
 .../streams/[streamId=i32]/+page.server.ts         |  40 --
 .../dashboard/streams/[streamId=i32]/+page.svelte  |   3 +-
 .../dashboard/streams/[streamId=i32]/+page.ts}     |  15 +-
 .../topics/[topicId=i32]/+page.server.ts           |  40 --
 .../topics/[topicId=i32]/+page.svelte              |   6 +-
 .../[streamId=i32]/topics/[topicId=i32]/+page.ts}  |  17 +-
 .../messages/{+page.server.ts => +page.ts}         |  29 +-
 web/svelte.config.js                               |  21 +-
 51 files changed, 1048 insertions(+), 819 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 8e8525388..839063828 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -27,7 +27,7 @@ checksum = 
"daa239b93927be1ff123eebada5a3ff23e89f0124ccb8609234e5103d5a5ae6d"
 dependencies = [
  "actix-utils",
  "actix-web",
- "derive_more 2.1.0",
+ "derive_more",
  "futures-util",
  "log",
  "once_cell",
@@ -46,7 +46,7 @@ dependencies = [
  "actix-web",
  "bitflags 2.10.0",
  "bytes",
- "derive_more 2.1.0",
+ "derive_more",
  "futures-core",
  "http-range",
  "log",
@@ -72,7 +72,7 @@ dependencies = [
  "brotli",
  "bytes",
  "bytestring",
- "derive_more 2.1.0",
+ "derive_more",
  "encoding_rs",
  "flate2",
  "foldhash 0.1.5",
@@ -187,7 +187,7 @@ dependencies = [
  "bytestring",
  "cfg-if",
  "cookie",
- "derive_more 2.1.0",
+ "derive_more",
  "encoding_rs",
  "foldhash 0.1.5",
  "futures-core",
@@ -621,7 +621,7 @@ dependencies = [
  "memchr",
  "num",
  "regex",
- "regex-syntax 0.8.8",
+ "regex-syntax",
 ]
 
 [[package]]
@@ -630,6 +630,45 @@ version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "b0f477b951e452a0b6b4a10b53ccd569042d1d01729b519e02074a9c0958a063"
 
+[[package]]
+name = "asn1-rs"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60"
+dependencies = [
+ "asn1-rs-derive",
+ "asn1-rs-impl",
+ "displaydoc",
+ "nom 7.1.3",
+ "num-traits",
+ "rusticata-macros",
+ "thiserror 2.0.17",
+ "time",
+]
+
+[[package]]
+name = "asn1-rs-derive"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.111",
+ "synstructure",
+]
+
+[[package]]
+name = "asn1-rs-impl"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.111",
+]
+
 [[package]]
 name = "assert_cmd"
 version = "2.1.1"
@@ -687,9 +726,9 @@ dependencies = [
 
 [[package]]
 name = "async-compression"
-version = "0.4.34"
+version = "0.4.36"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "0e86f6d3dc9dc4352edeea6b8e499e13e3f5dc3b964d7ca5fd411415a3498473"
+checksum = "98ec5f6c2f8bc326c994cb9e241cc257ddaba9afa8555a43cffbb5dd86efaa37"
 dependencies = [
  "compression-codecs",
  "compression-core",
@@ -992,9 +1031,9 @@ checksum = 
"72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
 
 [[package]]
 name = "base64ct"
-version = "1.8.0"
+version = "1.8.1"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
+checksum = "0e050f626429857a27ddccb31e0aca21356bfa709c04041aefddac081a8f068a"
 
 [[package]]
 name = "bdd"
@@ -1050,7 +1089,7 @@ dependencies = [
  "charming",
  "colored",
  "derive-new",
- "derive_more 2.1.0",
+ "derive_more",
  "human-repr",
  "rand 0.9.2",
  "serde",
@@ -1464,11 +1503,12 @@ dependencies = [
 
 [[package]]
 name = "cargo-platform"
-version = "0.3.1"
+version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "122ec45a44b270afd1402f351b782c676b173e3c3fb28d86ff7ebfb4d86a4ee4"
+checksum = "87a0c0e6148f11f01f32650a2ea02d532b2ad4e81d8bd41e6e565b5adc5e6082"
 dependencies = [
  "serde",
+ "serde_core",
 ]
 
 [[package]]
@@ -1492,7 +1532,7 @@ source = 
"registry+https://github.com/rust-lang/crates.io-index";
 checksum = "ef987d17b0a113becdd19d3d0022d04d7ef41f9efe4f3fb63ac44ba61df3ade9"
 dependencies = [
  "camino",
- "cargo-platform 0.3.1",
+ "cargo-platform 0.3.2",
  "semver",
  "serde",
  "serde_json",
@@ -1501,9 +1541,9 @@ dependencies = [
 
 [[package]]
 name = "cc"
-version = "1.2.48"
+version = "1.2.49"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "c481bdbf0ed3b892f6f806287d72acd515b352a4ec27a208489b8c1bc839633a"
+checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215"
 dependencies = [
  "find-msvc-tools",
  "jobserver",
@@ -1619,7 +1659,7 @@ version = "4.5.49"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671"
 dependencies = [
- "heck 0.5.0",
+ "heck",
  "proc-macro2",
  "quote",
  "syn 2.0.111",
@@ -1640,9 +1680,9 @@ dependencies = [
 
 [[package]]
 name = "cmake"
-version = "0.1.54"
+version = "0.1.56"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0"
+checksum = "b042e5d8a74ae91bb0961acd039822472ec99f8ab0948cbf6d1369588f8be586"
 dependencies = [
  "cc",
 ]
@@ -1889,9 +1929,9 @@ dependencies = [
 
 [[package]]
 name = "compression-codecs"
-version = "0.4.33"
+version = "0.4.35"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "302266479cb963552d11bd042013a58ef1adc56768016c8b82b4199488f2d4ad"
+checksum = "b0f7ac3e5b97fdce45e8922fb05cae2c37f7bbd63d30dd94821dacfd8f3f2bf2"
 dependencies = [
  "bzip2",
  "compression-core",
@@ -1930,15 +1970,15 @@ dependencies = [
 
 [[package]]
 name = "console"
-version = "0.15.11"
+version = "0.16.1"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8"
+checksum = "b430743a6eb14e9764d4260d4c0d8123087d504eeb9c48f2b2a5e810dd369df4"
 dependencies = [
  "encode_unicode",
  "libc",
  "once_cell",
  "unicode-width 0.2.2",
- "windows-sys 0.59.0",
+ "windows-sys 0.61.2",
 ]
 
 [[package]]
@@ -2242,28 +2282,26 @@ dependencies = [
 
 [[package]]
 name = "cucumber"
-version = "0.21.1"
+version = "0.22.0"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "6cd12917efc3a8b069a4975ef3cb2f2d835d42d04b3814d90838488f9dd9bf69"
+checksum = "18c09939b8de21501b829a3839fa8a01ef6cc226e6bc1f5f163f7104bd5e847d"
 dependencies = [
  "anyhow",
  "clap",
  "console",
  "cucumber-codegen",
  "cucumber-expressions",
- "derive_more 0.99.20",
- "drain_filter_polyfill",
+ "derive_more",
  "either",
  "futures",
  "gherkin",
  "globwalk",
  "humantime",
  "inventory",
- "itertools 0.13.0",
- "lazy-regex",
+ "itertools 0.14.0",
  "linked-hash-map",
- "once_cell",
  "pin-project",
+ "ref-cast",
  "regex",
  "sealed",
  "smart-default",
@@ -2271,13 +2309,13 @@ dependencies = [
 
 [[package]]
 name = "cucumber-codegen"
-version = "0.21.1"
+version = "0.22.0"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "9e19cd9e8e7cfd79fbf844eb6a7334117973c01f6bad35571262b00891e60f1c"
+checksum = "1f5afe541b5147a7b986816153ccfd502622bb37789420cfff412685f27c0a95"
 dependencies = [
  "cucumber-expressions",
  "inflections",
- "itertools 0.13.0",
+ "itertools 0.14.0",
  "proc-macro2",
  "quote",
  "regex",
@@ -2287,16 +2325,16 @@ dependencies = [
 
 [[package]]
 name = "cucumber-expressions"
-version = "0.3.0"
+version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "d794fed319eea24246fb5f57632f7ae38d61195817b7eb659455aa5bdd7c1810"
+checksum = "6401038de3af44fe74e6fccdb8a5b7db7ba418f480c8e9ad584c6f65c05a27a6"
 dependencies = [
- "derive_more 0.99.20",
+ "derive_more",
  "either",
- "nom",
+ "nom 8.0.0",
  "nom_locate",
  "regex",
- "regex-syntax 0.7.5",
+ "regex-syntax",
 ]
 
 [[package]]
@@ -2376,9 +2414,9 @@ dependencies = [
 
 [[package]]
 name = "cyper-core"
-version = "0.7.0"
+version = "0.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "e53f8cde9bdfe28b1bec664feb1825c237f1b3948c2dfdba019540be3f660b3f"
+checksum = "7f4b86aa741e422dab7f730aa1ec5ab6bc26569e577fe2b8fe0ebf6d779b2325"
 dependencies = [
  "compio",
  "futures-util",
@@ -2484,9 +2522,9 @@ checksum = 
"2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
 
 [[package]]
 name = "dbus"
-version = "0.9.9"
+version = "0.9.10"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "190b6255e8ab55a7b568df5a883e9497edc3e4821c06396612048b430e5ad1e9"
+checksum = "21b3aa68d7e7abee336255bd7248ea965cc393f3e70411135a6f6a4b651345d4"
 dependencies = [
  "libc",
  "libdbus-sys",
@@ -2521,6 +2559,20 @@ dependencies = [
  "zeroize",
 ]
 
+[[package]]
+name = "der-parser"
+version = "10.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6"
+dependencies = [
+ "asn1-rs",
+ "displaydoc",
+ "nom 7.1.3",
+ "num-bigint",
+ "num-traits",
+ "rusticata-macros",
+]
+
 [[package]]
 name = "deranged"
 version = "0.5.5"
@@ -2584,17 +2636,6 @@ dependencies = [
  "syn 2.0.111",
 ]
 
-[[package]]
-name = "derive_more"
-version = "0.99.20"
-source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.111",
-]
-
 [[package]]
 name = "derive_more"
 version = "2.1.0"
@@ -2711,9 +2752,9 @@ dependencies = [
 
 [[package]]
 name = "dlopen2_derive"
-version = "0.4.2"
+version = "0.4.3"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "95f4a04e1bfbfa4835a6073177aafb95ead4de0722dbb339195fdc7e0a09599b"
+checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -2761,12 +2802,6 @@ version = "0.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1"
 
-[[package]]
-name = "drain_filter_polyfill"
-version = "0.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "669a445ee724c5c69b1b06fe0b63e70a1c84bc9bb7d9696cd4f4e3ec45050408"
-
 [[package]]
 name = "dtoa"
 version = "1.0.10"
@@ -3154,9 +3189,9 @@ dependencies = [
 
 [[package]]
 name = "file-operation"
-version = "0.8.6"
+version = "0.8.8"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "e820507a8045dbe4861c925a91ca6989cce6c7c09b4c3d90c4941df84ffc0aeb"
+checksum = "34664772b984b86dbccb14ab56722778cca458b38f6f324db3331141ef441e05"
 dependencies = [
  "tokio",
 ]
@@ -3491,19 +3526,19 @@ dependencies = [
 
 [[package]]
 name = "gherkin"
-version = "0.14.0"
+version = "0.15.0"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "20b79820c0df536d1f3a089a2fa958f61cb96ce9e0f3f8f507f5a31179567755"
+checksum = "70197ce7751bfe8bc828e3a855502d3a869a1e9416b58b10c4bde5cf8a0a3cb3"
 dependencies = [
- "heck 0.4.1",
+ "heck",
  "peg",
  "quote",
  "serde",
  "serde_json",
  "syn 2.0.111",
  "textwrap",
- "thiserror 1.0.69",
- "typed-builder 0.15.2",
+ "thiserror 2.0.17",
+ "typed-builder 0.23.2",
 ]
 
 [[package]]
@@ -3529,7 +3564,7 @@ dependencies = [
  "bstr",
  "log",
  "regex-automata",
- "regex-syntax 0.8.8",
+ "regex-syntax",
 ]
 
 [[package]]
@@ -3912,12 +3947,6 @@ dependencies = [
  "stable_deref_trait",
 ]
 
-[[package]]
-name = "heck"
-version = "0.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
-
 [[package]]
 name = "heck"
 version = "0.5.0"
@@ -4120,9 +4149,9 @@ dependencies = [
 
 [[package]]
 name = "hyper-util"
-version = "0.1.18"
+version = "0.1.19"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56"
+checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f"
 dependencies = [
  "base64 0.22.1",
  "bytes",
@@ -4305,9 +4334,9 @@ checksum = 
"7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a"
 
 [[package]]
 name = "icu_properties"
-version = "2.1.1"
+version = "2.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99"
+checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec"
 dependencies = [
  "icu_collections",
  "icu_locale_core",
@@ -4319,9 +4348,9 @@ dependencies = [
 
 [[package]]
 name = "icu_properties_data"
-version = "2.1.1"
+version = "2.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899"
+checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af"
 
 [[package]]
 name = "icu_provider"
@@ -4575,7 +4604,7 @@ dependencies = [
  "compio-tls",
  "compio-ws",
  "crossbeam",
- "derive_more 2.1.0",
+ "derive_more",
  "err_trail",
  "fast-async-mutex",
  "figment",
@@ -4905,7 +4934,7 @@ dependencies = [
  "chrono",
  "compio",
  "ctor",
- "derive_more 2.1.0",
+ "derive_more",
  "env_logger",
  "futures",
  "humantime",
@@ -5152,29 +5181,6 @@ version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388"
 
-[[package]]
-name = "lazy-regex"
-version = "3.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "191898e17ddee19e60bccb3945aa02339e81edd4a8c50e21fd4d48cdecda7b29"
-dependencies = [
- "lazy-regex-proc_macros",
- "once_cell",
- "regex",
-]
-
-[[package]]
-name = "lazy-regex-proc_macros"
-version = "3.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "c35dc8b0da83d1a9507e12122c80dea71a9c7c613014347392483a83ea593e04"
-dependencies = [
- "proc-macro2",
- "quote",
- "regex",
- "syn 2.0.111",
-]
-
 [[package]]
 name = "lazy_static"
 version = "1.5.0"
@@ -5280,9 +5286,9 @@ checksum = 
"37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091"
 
 [[package]]
 name = "libdbus-sys"
-version = "0.2.6"
+version = "0.2.7"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "5cbe856efeb50e4681f010e9aaa2bf0a644e10139e54cde10fc83a307c23bd9f"
+checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043"
 dependencies = [
  "cc",
  "pkg-config",
@@ -5383,9 +5389,9 @@ dependencies = [
 
 [[package]]
 name = "libz-rs-sys"
-version = "0.5.2"
+version = "0.5.4"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "840db8cf39d9ec4dd794376f38acc40d0fc65eec2a8f484f7fd375b84602becd"
+checksum = "15413ef615ad868d4d65dce091cb233b229419c7c0c4bcaa746c0901c49ff39c"
 dependencies = [
  "zlib-rs",
 ]
@@ -5478,7 +5484,7 @@ dependencies = [
  "lazy_static",
  "proc-macro2",
  "quote",
- "regex-syntax 0.8.8",
+ "regex-syntax",
  "rustc_version",
  "syn 2.0.111",
 ]
@@ -5647,9 +5653,9 @@ dependencies = [
 
 [[package]]
 name = "mio"
-version = "1.1.0"
+version = "1.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873"
+checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
 dependencies = [
  "libc",
  "log",
@@ -5738,15 +5744,24 @@ dependencies = [
  "minimal-lexical",
 ]
 
+[[package]]
+name = "nom"
+version = "8.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
+dependencies = [
+ "memchr",
+]
+
 [[package]]
 name = "nom_locate"
-version = "4.2.0"
+version = "5.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "1e3c83c053b0713da60c5b8de47fe8e494fe3ece5267b2f23090a07a053ba8f3"
+checksum = "0b577e2d69827c4740cba2b52efaad1c4cc7c73042860b199710b3575c68438d"
 dependencies = [
  "bytecount",
  "memchr",
- "nom",
+ "nom 8.0.0",
 ]
 
 [[package]]
@@ -6037,6 +6052,15 @@ dependencies = [
  "web-time",
 ]
 
+[[package]]
+name = "oid-registry"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7"
+dependencies = [
+ "asn1-rs",
+]
+
 [[package]]
 name = "once_cell"
 version = "1.21.3"
@@ -6381,7 +6405,7 @@ checksum = 
"914a1c2265c98e2446911282c6ac86d8524f495792c38c5bd884f80499c7538a"
 dependencies = [
  "parse-display-derive",
  "regex",
- "regex-syntax 0.8.8",
+ "regex-syntax",
 ]
 
 [[package]]
@@ -6393,7 +6417,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "regex",
- "regex-syntax 0.8.8",
+ "regex-syntax",
  "structmeta",
  "syn 2.0.111",
 ]
@@ -6784,7 +6808,7 @@ version = "3.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983"
 dependencies = [
- "toml_edit 0.23.7",
+ "toml_edit 0.23.9",
 ]
 
 [[package]]
@@ -7165,14 +7189,15 @@ dependencies = [
 
 [[package]]
 name = "rcgen"
-version = "0.14.5"
+version = "0.14.6"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "5fae430c6b28f1ad601274e78b7dffa0546de0b73b4cd32f46723c0c2a16f7a5"
+checksum = "3ec0a99f2de91c3cddc84b37e7db80e4d96b743e05607f647eb236fc0455907f"
 dependencies = [
  "pem",
  "ring",
  "rustls-pki-types",
  "time",
+ "x509-parser",
  "yasna",
 ]
 
@@ -7225,7 +7250,7 @@ dependencies = [
  "aho-corasick",
  "memchr",
  "regex-automata",
- "regex-syntax 0.8.8",
+ "regex-syntax",
 ]
 
 [[package]]
@@ -7236,7 +7261,7 @@ checksum = 
"5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
 dependencies = [
  "aho-corasick",
  "memchr",
- "regex-syntax 0.8.8",
+ "regex-syntax",
 ]
 
 [[package]]
@@ -7245,12 +7270,6 @@ version = "0.1.8"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "8d942b98df5e658f56f20d592c7f868833fe38115e65c33003d8cd224b0155da"
 
-[[package]]
-name = "regex-syntax"
-version = "0.7.5"
-source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
-
 [[package]]
 name = "regex-syntax"
 version = "0.8.8"
@@ -7544,6 +7563,40 @@ dependencies = [
  "zeroize",
 ]
 
+[[package]]
+name = "rust-embed"
+version = "8.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "947d7f3fad52b283d261c4c99a084937e2fe492248cb9a68a8435a861b8798ca"
+dependencies = [
+ "rust-embed-impl",
+ "rust-embed-utils",
+ "walkdir",
+]
+
+[[package]]
+name = "rust-embed-impl"
+version = "8.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "5fa2c8c9e8711e10f9c4fd2d64317ef13feaab820a4c51541f1a8c8e2e851ab2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "rust-embed-utils",
+ "syn 2.0.111",
+ "walkdir",
+]
+
+[[package]]
+name = "rust-embed-utils"
+version = "8.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "60b161f275cb337fe0a44d924a5f4df0ed69c2c39519858f931ce61c779d3475"
+dependencies = [
+ "sha2",
+ "walkdir",
+]
+
 [[package]]
 name = "rust-ini"
 version = "0.21.3"
@@ -7585,6 +7638,15 @@ dependencies = [
  "semver",
 ]
 
+[[package]]
+name = "rusticata-macros"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632"
+dependencies = [
+ "nom 7.1.3",
+]
+
 [[package]]
 name = "rustix"
 version = "1.1.2"
@@ -7787,11 +7849,10 @@ checksum = 
"1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
 
 [[package]]
 name = "sealed"
-version = "0.5.0"
+version = "0.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "f4a8caec23b7800fb97971a1c6ae365b6239aaeddfb934d6265f8505e795699d"
+checksum = "22f968c5ea23d555e670b449c1c5e7b2fc399fdaec1d304a17cd48e288abc107"
 dependencies = [
- "heck 0.4.1",
  "proc-macro2",
  "quote",
  "syn 2.0.111",
@@ -8100,7 +8161,7 @@ dependencies = [
  "cyper",
  "cyper-axum",
  "dashmap",
- "derive_more 2.1.0",
+ "derive_more",
  "dotenvy",
  "enum_dispatch",
  "err_trail",
@@ -8115,6 +8176,7 @@ dependencies = [
  "jsonwebtoken",
  "lending-iterator",
  "mimalloc",
+ "mime_guess",
  "moka",
  "nix",
  "opentelemetry",
@@ -8128,6 +8190,7 @@ dependencies = [
  "reqwest",
  "ring",
  "ringbuffer",
+ "rust-embed",
  "rustls",
  "rustls-pemfile",
  "send_wrapper",
@@ -8212,9 +8275,9 @@ dependencies = [
 
 [[package]]
 name = "simd-adler32"
-version = "0.3.7"
+version = "0.3.8"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
+checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
 
 [[package]]
 name = "simd-json"
@@ -8301,7 +8364,7 @@ version = "0.8.9"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "c1c97747dbf44bb1ca44a561ece23508e99cb592e862f22222dcf42f51d1e451"
 dependencies = [
- "heck 0.5.0",
+ "heck",
  "proc-macro2",
  "quote",
  "syn 2.0.111",
@@ -8433,7 +8496,7 @@ checksum = 
"19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b"
 dependencies = [
  "dotenvy",
  "either",
- "heck 0.5.0",
+ "heck",
  "hex",
  "once_cell",
  "proc-macro2",
@@ -8653,7 +8716,7 @@ version = "0.26.4"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
 dependencies = [
- "heck 0.5.0",
+ "heck",
  "proc-macro2",
  "quote",
  "rustversion",
@@ -8666,7 +8729,7 @@ version = "0.27.2"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7"
 dependencies = [
- "heck 0.5.0",
+ "heck",
  "proc-macro2",
  "quote",
  "syn 2.0.111",
@@ -8722,9 +8785,9 @@ dependencies = [
 
 [[package]]
 name = "synthez"
-version = "0.3.1"
+version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "a3d2c2202510a1e186e63e596d9318c91a8cbe85cd1a56a7be0c333e5f59ec8d"
+checksum = "6d8a928f38f1bc873f28e0d2ba8298ad65374a6ac2241dabd297271531a736cd"
 dependencies = [
  "syn 2.0.111",
  "synthez-codegen",
@@ -8733,9 +8796,9 @@ dependencies = [
 
 [[package]]
 name = "synthez-codegen"
-version = "0.3.1"
+version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "f724aa6d44b7162f3158a57bccd871a77b39a4aef737e01bcdff41f4772c7746"
+checksum = "8fb83b8df4238e11746984dfb3819b155cd270de0e25847f45abad56b3671047"
 dependencies = [
  "syn 2.0.111",
  "synthez-core",
@@ -8743,9 +8806,9 @@ dependencies = [
 
 [[package]]
 name = "synthez-core"
-version = "0.3.1"
+version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "78bfa6ec52465e2425fd43ce5bbbe0f0b623964f7c63feb6b10980e816c654ea"
+checksum = "906fba967105d822e7c7ed60477b5e76116724d33de68a585681fb253fc30d5c"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -9210,9 +9273,9 @@ dependencies = [
 
 [[package]]
 name = "toml_edit"
-version = "0.23.7"
+version = "0.23.9"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d"
+checksum = "5d7cbc3b4b49633d57a0509303158ca50de80ae32c265093b24c414705807832"
 dependencies = [
  "indexmap 2.12.1",
  "toml_datetime 0.7.3",
@@ -9485,15 +9548,6 @@ dependencies = [
  "rand 0.9.2",
 ]
 
-[[package]]
-name = "typed-builder"
-version = "0.15.2"
-source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "7fe83c85a85875e8c4cb9ce4a890f05b23d38cd0d47647db7895d3d2a79566d2"
-dependencies = [
- "typed-builder-macro 0.15.2",
-]
-
 [[package]]
 name = "typed-builder"
 version = "0.19.1"
@@ -9513,14 +9567,12 @@ dependencies = [
 ]
 
 [[package]]
-name = "typed-builder-macro"
-version = "0.15.2"
+name = "typed-builder"
+version = "0.23.2"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "29a3151c41d0b13e3d011f98adc24434560ef06673a155a6c7f66b9879eecce2"
+checksum = "31aa81521b70f94402501d848ccc0ecaa8f93c8eb6999eb9747e72287757ffda"
 dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.111",
+ "typed-builder-macro 0.23.2",
 ]
 
 [[package]]
@@ -9545,6 +9597,17 @@ dependencies = [
  "syn 2.0.111",
 ]
 
+[[package]]
+name = "typed-builder-macro"
+version = "0.23.2"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "076a02dc54dd46795c2e9c8282ed40bcfb1e22747e955de9389a1de28190fb26"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.111",
+]
+
 [[package]]
 name = "typenum"
 version = "1.19.0"
@@ -10608,6 +10671,24 @@ dependencies = [
  "tap",
 ]
 
+[[package]]
+name = "x509-parser"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "eb3e137310115a65136898d2079f003ce33331a6c4b0d51f1531d1be082b6425"
+dependencies = [
+ "asn1-rs",
+ "data-encoding",
+ "der-parser",
+ "lazy_static",
+ "nom 7.1.3",
+ "oid-registry",
+ "ring",
+ "rusticata-macros",
+ "thiserror 2.0.17",
+ "time",
+]
+
 [[package]]
 name = "xattr"
 version = "1.6.1"
@@ -10849,9 +10930,9 @@ dependencies = [
 
 [[package]]
 name = "zlib-rs"
-version = "0.5.2"
+version = "0.5.4"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "2f06ae92f42f5e5c42443fd094f245eb656abf56dd7cce9b8b263236565e00f2"
+checksum = "51f936044d677be1a1168fae1d03b583a285a5dd9d8cbf7b24c23aa1fc775235"
 
 [[package]]
 name = "zopfli"
diff --git a/Cargo.toml b/Cargo.toml
index 68679eada..87ecac08c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -161,7 +161,7 @@ reqwest = { version = "0.12.25", default-features = false, 
features = [
 reqwest-middleware = { version = "0.4.2", features = ["json"] }
 reqwest-retry = "0.8.0"
 reqwest-tracing = "0.5.8"
-rust-s3 = { version = "0.37.0", default-features = false, features = [
+rust-s3 = { version = "0.37.1", default-features = false, features = [
     "tokio-rustls-tls",
     "tags",
 ] }
diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md
index c81f89d2d..51b3e5d83 100644
--- a/DEPENDENCIES.md
+++ b/DEPENDENCIES.md
@@ -50,11 +50,14 @@ arrow-schema: 55.2.0, "Apache-2.0",
 arrow-select: 55.2.0, "Apache-2.0",
 arrow-string: 55.2.0, "Apache-2.0",
 as-any: 0.3.2, "Apache-2.0 OR MIT",
+asn1-rs: 0.7.1, "Apache-2.0 OR MIT",
+asn1-rs-derive: 0.6.0, "Apache-2.0 OR MIT",
+asn1-rs-impl: 0.2.0, "Apache-2.0 OR MIT",
 assert_cmd: 2.1.1, "Apache-2.0 OR MIT",
 astral-tokio-tar: 0.5.6, "Apache-2.0 OR MIT",
 async-broadcast: 0.7.2, "Apache-2.0 OR MIT",
 async-channel: 2.5.0, "Apache-2.0 OR MIT",
-async-compression: 0.4.34, "Apache-2.0 OR MIT",
+async-compression: 0.4.36, "Apache-2.0 OR MIT",
 async-dropper: 0.3.1, "MIT",
 async-dropper-derive: 0.3.1, "MIT",
 async-dropper-simple: 0.2.6, "MIT",
@@ -80,7 +83,7 @@ backon: 1.6.0, "Apache-2.0",
 base16ct: 0.2.0, "Apache-2.0 OR MIT",
 base64: 0.21.7, "Apache-2.0 OR MIT",
 base64: 0.22.1, "Apache-2.0 OR MIT",
-base64ct: 1.8.0, "Apache-2.0 OR MIT",
+base64ct: 1.8.1, "Apache-2.0 OR MIT",
 bdd: 0.0.1, "Apache-2.0",
 beef: 0.5.2, "Apache-2.0 OR MIT",
 bench-dashboard-frontend: 0.4.1, "Apache-2.0",
@@ -123,10 +126,10 @@ bytestring: 1.5.0, "Apache-2.0 OR MIT",
 bzip2: 0.6.1, "Apache-2.0 OR MIT",
 camino: 1.2.1, "Apache-2.0 OR MIT",
 cargo-platform: 0.1.9, "Apache-2.0 OR MIT",
-cargo-platform: 0.3.1, "Apache-2.0 OR MIT",
+cargo-platform: 0.3.2, "Apache-2.0 OR MIT",
 cargo_metadata: 0.19.2, "MIT",
 cargo_metadata: 0.23.1, "MIT",
-cc: 1.2.48, "Apache-2.0 OR MIT",
+cc: 1.2.49, "Apache-2.0 OR MIT",
 cesu8: 1.1.0, "Apache-2.0 OR MIT",
 cfg-if: 1.0.4, "Apache-2.0 OR MIT",
 cfg_aliases: 0.2.1, "MIT",
@@ -140,7 +143,7 @@ clap_complete: 4.5.61, "Apache-2.0 OR MIT",
 clap_derive: 4.5.49, "Apache-2.0 OR MIT",
 clap_lex: 0.7.6, "Apache-2.0 OR MIT",
 clock: 0.1.0, "N/A",
-cmake: 0.1.54, "Apache-2.0 OR MIT",
+cmake: 0.1.56, "Apache-2.0 OR MIT",
 cobs: 0.3.0, "Apache-2.0 OR MIT",
 colorchoice: 1.0.4, "Apache-2.0 OR MIT",
 colored: 3.0.0, "MPL-2.0",
@@ -158,11 +161,11 @@ compio-quic: 0.6.0, "MIT",
 compio-runtime: 0.10.1, "MIT",
 compio-tls: 0.8.0, "MIT",
 compio-ws: 0.2.0, "MIT",
-compression-codecs: 0.4.33, "Apache-2.0 OR MIT",
+compression-codecs: 0.4.35, "Apache-2.0 OR MIT",
 compression-core: 0.4.31, "Apache-2.0 OR MIT",
 concurrent-queue: 2.5.0, "Apache-2.0 OR MIT",
 consensus: 0.1.0, "Apache-2.0",
-console: 0.15.11, "MIT",
+console: 0.16.1, "MIT",
 console_error_panic_hook: 0.1.7, "Apache-2.0 OR MIT",
 const-oid: 0.9.6, "Apache-2.0 OR MIT",
 const-random: 0.1.18, "Apache-2.0 OR MIT",
@@ -196,14 +199,14 @@ ctor: 0.6.3, "Apache-2.0 OR MIT",
 ctor-proc-macro: 0.0.7, "Apache-2.0 OR MIT",
 ctr: 0.9.2, "Apache-2.0 OR MIT",
 ctrlc: 3.5.1, "Apache-2.0 OR MIT",
-cucumber: 0.21.1, "Apache-2.0 OR MIT",
-cucumber-codegen: 0.21.1, "Apache-2.0 OR MIT",
-cucumber-expressions: 0.3.0, "Apache-2.0 OR MIT",
+cucumber: 0.22.0, "Apache-2.0 OR MIT",
+cucumber-codegen: 0.22.0, "Apache-2.0 OR MIT",
+cucumber-expressions: 0.5.0, "Apache-2.0 OR MIT",
 curve25519-dalek: 4.1.3, "BSD-3-Clause",
 curve25519-dalek-derive: 0.1.1, "Apache-2.0 OR MIT",
 cyper: 0.7.1, "MIT",
 cyper-axum: 0.7.1, "MIT",
-cyper-core: 0.7.0, "MIT",
+cyper-core: 0.7.1, "MIT",
 darling: 0.20.11, "MIT",
 darling: 0.21.3, "MIT",
 darling_core: 0.20.11, "MIT",
@@ -213,17 +216,17 @@ darling_macro: 0.21.3, "MIT",
 dary_heap: 0.3.8, "Apache-2.0 OR MIT",
 dashmap: 6.1.0, "MIT",
 data-encoding: 2.9.0, "MIT",
-dbus: 0.9.9, "Apache-2.0 OR MIT",
+dbus: 0.9.10, "Apache-2.0 OR MIT",
 dbus-secret-service: 4.1.0, "Apache-2.0 OR MIT",
 deflate64: 0.1.10, "MIT",
 der: 0.7.10, "Apache-2.0 OR MIT",
+der-parser: 10.0.0, "Apache-2.0 OR MIT",
 deranged: 0.5.5, "Apache-2.0 OR MIT",
 derive-new: 0.7.0, "MIT",
 derive_arbitrary: 1.4.2, "Apache-2.0 OR MIT",
 derive_builder: 0.20.2, "Apache-2.0 OR MIT",
 derive_builder_core: 0.20.2, "Apache-2.0 OR MIT",
 derive_builder_macro: 0.20.2, "Apache-2.0 OR MIT",
-derive_more: 0.99.20, "MIT",
 derive_more: 2.1.0, "MIT",
 derive_more-impl: 2.1.0, "MIT",
 difflib: 0.4.0, "MIT",
@@ -235,13 +238,12 @@ dispatch2: 0.3.0, "Apache-2.0 OR MIT OR Zlib",
 displaydoc: 0.2.5, "Apache-2.0 OR MIT",
 dissimilar: 1.0.10, "Apache-2.0",
 dlopen2: 0.8.2, "MIT",
-dlopen2_derive: 0.4.2, "MIT",
+dlopen2_derive: 0.4.3, "MIT",
 dlv-list: 0.5.2, "Apache-2.0 OR MIT",
 docker_credential: 1.3.2, "Apache-2.0 OR MIT",
 document-features: 0.2.12, "Apache-2.0 OR MIT",
 dotenvy: 0.15.7, "MIT",
 downcast: 0.11.0, "MIT",
-drain_filter_polyfill: 0.1.3, "Apache-2.0 OR MIT",
 dtoa: 1.0.10, "Apache-2.0 OR MIT",
 dtor: 0.1.1, "Apache-2.0 OR MIT",
 dtor-proc-macro: 0.0.6, "Apache-2.0 OR MIT",
@@ -281,7 +283,7 @@ ff: 0.13.1, "Apache-2.0 OR MIT",
 fiat-crypto: 0.2.9, "Apache-2.0 OR BSD-1-Clause OR MIT",
 figlet-rs: 0.1.5, "Apache-2.0",
 figment: 0.10.19, "Apache-2.0 OR MIT",
-file-operation: 0.8.6, "MIT",
+file-operation: 0.8.8, "MIT",
 filetime: 0.2.26, "Apache-2.0 OR MIT",
 find-msvc-tools: 0.1.5, "Apache-2.0 OR MIT",
 flatbuffers: 25.9.23, "Apache-2.0",
@@ -317,7 +319,7 @@ generic-array: 0.14.7, "MIT",
 getrandom: 0.2.16, "Apache-2.0 OR MIT",
 getrandom: 0.3.4, "Apache-2.0 OR MIT",
 ghash: 0.5.1, "Apache-2.0 OR MIT",
-gherkin: 0.14.0, "Apache-2.0 OR MIT",
+gherkin: 0.15.0, "Apache-2.0 OR MIT",
 git2: 0.20.3, "Apache-2.0 OR MIT",
 globset: 0.4.18, "MIT OR Unlicense",
 globwalk: 0.9.1, "MIT",
@@ -349,7 +351,6 @@ hashbrown: 0.15.5, "Apache-2.0 OR MIT",
 hashbrown: 0.16.1, "Apache-2.0 OR MIT",
 hashlink: 0.10.0, "Apache-2.0 OR MIT",
 heapless: 0.7.17, "Apache-2.0 OR MIT",
-heck: 0.4.1, "Apache-2.0 OR MIT",
 heck: 0.5.0, "Apache-2.0 OR MIT",
 hermit-abi: 0.5.2, "Apache-2.0 OR MIT",
 hex: 0.4.3, "Apache-2.0 OR MIT",
@@ -370,7 +371,7 @@ hyper: 1.8.1, "MIT",
 hyper-named-pipe: 0.1.0, "Apache-2.0",
 hyper-rustls: 0.27.7, "Apache-2.0 OR ISC OR MIT",
 hyper-timeout: 0.5.2, "Apache-2.0 OR MIT",
-hyper-util: 0.1.18, "MIT",
+hyper-util: 0.1.19, "MIT",
 hyperlocal: 0.9.1, "MIT",
 iana-time-zone: 0.1.64, "Apache-2.0 OR MIT",
 iana-time-zone-haiku: 0.1.2, "Apache-2.0 OR MIT",
@@ -380,8 +381,8 @@ icu_collections: 2.1.1, "Unicode-3.0",
 icu_locale_core: 2.1.1, "Unicode-3.0",
 icu_normalizer: 2.1.1, "Unicode-3.0",
 icu_normalizer_data: 2.1.1, "Unicode-3.0",
-icu_properties: 2.1.1, "Unicode-3.0",
-icu_properties_data: 2.1.1, "Unicode-3.0",
+icu_properties: 2.1.2, "Unicode-3.0",
+icu_properties_data: 2.1.2, "Unicode-3.0",
 icu_provider: 2.1.1, "Unicode-3.0",
 ident_case: 1.0.1, "Apache-2.0 OR MIT",
 idna: 1.1.0, "Apache-2.0 OR MIT",
@@ -439,8 +440,6 @@ keyring: 3.6.3, "Apache-2.0 OR MIT",
 kqueue: 1.1.1, "MIT",
 kqueue-sys: 1.0.4, "MIT",
 language-tags: 0.3.2, "Apache-2.0 OR MIT",
-lazy-regex: 3.4.2, "MIT",
-lazy-regex-proc_macros: 3.4.2, "MIT",
 lazy_static: 1.5.0, "Apache-2.0 OR MIT",
 lending-iterator: 0.1.7, "Apache-2.0 OR MIT OR Zlib",
 lending-iterator-proc_macros: 0.1.7, "Apache-2.0 OR MIT OR Zlib",
@@ -452,7 +451,7 @@ lexical-write-float: 1.0.6, "Apache-2.0 OR MIT",
 lexical-write-integer: 1.0.6, "Apache-2.0 OR MIT",
 libbz2-rs-sys: 0.2.2, "bzip2-1.0.6",
 libc: 0.2.178, "Apache-2.0 OR MIT",
-libdbus-sys: 0.2.6, "Apache-2.0 OR MIT",
+libdbus-sys: 0.2.7, "Apache-2.0 OR MIT",
 libflate: 2.2.1, "MIT",
 libflate_lz77: 2.2.0, "MIT",
 libgit2-sys: 0.18.3+1.9.2, "Apache-2.0 OR MIT",
@@ -462,7 +461,7 @@ libm: 0.2.15, "MIT",
 libmimalloc-sys: 0.1.44, "MIT",
 libredox: 0.1.10, "MIT",
 libsqlite3-sys: 0.30.1, "MIT",
-libz-rs-sys: 0.5.2, "Zlib",
+libz-rs-sys: 0.5.4, "Zlib",
 libz-sys: 1.1.23, "Apache-2.0 OR MIT",
 linked-hash-map: 0.5.6, "Apache-2.0 OR MIT",
 linux-raw-sys: 0.11.0, "Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT",
@@ -493,7 +492,7 @@ mime: 0.3.17, "Apache-2.0 OR MIT",
 mime_guess: 2.0.5, "MIT",
 minimal-lexical: 0.2.1, "Apache-2.0 OR MIT",
 miniz_oxide: 0.8.9, "Apache-2.0 OR MIT OR Zlib",
-mio: 1.1.0, "MIT",
+mio: 1.1.1, "MIT",
 mockall: 0.14.0, "Apache-2.0 OR MIT",
 mockall_derive: 0.14.0, "Apache-2.0 OR MIT",
 moka: 0.12.11, "(Apache-2.0 OR MIT) AND Apache-2.0",
@@ -501,7 +500,8 @@ murmur3: 0.5.2, "Apache-2.0 OR MIT",
 never-say-never: 6.6.666, "Apache-2.0 OR MIT OR Zlib",
 nix: 0.30.1, "MIT",
 nom: 7.1.3, "MIT",
-nom_locate: 4.2.0, "MIT",
+nom: 8.0.0, "MIT",
+nom_locate: 5.0.0, "MIT",
 nonzero_ext: 0.3.0, "Apache-2.0",
 nonzero_lit: 0.1.2, "Apache-2.0 OR CC0-1.0 OR MIT",
 normalize-line-endings: 0.3.0, "Apache-2.0",
@@ -529,6 +529,7 @@ objc2-core-foundation: 0.3.2, "Apache-2.0 OR MIT OR Zlib",
 objc2-encode: 4.1.0, "MIT",
 objc2-io-kit: 0.3.2, "Apache-2.0 OR MIT OR Zlib",
 octocrab: 0.48.1, "Apache-2.0 OR MIT",
+oid-registry: 0.8.1, "Apache-2.0 OR MIT",
 once_cell: 1.21.3, "Apache-2.0 OR MIT",
 once_cell_polyfill: 1.70.2, "Apache-2.0 OR MIT",
 opaque-debug: 0.3.1, "Apache-2.0 OR MIT",
@@ -635,7 +636,7 @@ rand_xoshiro: 0.7.0, "Apache-2.0 OR MIT",
 raw-cpuid: 11.6.0, "MIT",
 rayon: 1.11.0, "Apache-2.0 OR MIT",
 rayon-core: 1.13.0, "Apache-2.0 OR MIT",
-rcgen: 0.14.5, "Apache-2.0 OR MIT",
+rcgen: 0.14.6, "Apache-2.0 OR MIT",
 redox_syscall: 0.5.18, "MIT",
 redox_users: 0.5.2, "MIT",
 ref-cast: 1.0.25, "Apache-2.0 OR MIT",
@@ -643,7 +644,6 @@ ref-cast-impl: 1.0.25, "Apache-2.0 OR MIT",
 regex: 1.12.2, "Apache-2.0 OR MIT",
 regex-automata: 0.4.13, "Apache-2.0 OR MIT",
 regex-lite: 0.1.8, "Apache-2.0 OR MIT",
-regex-syntax: 0.7.5, "Apache-2.0 OR MIT",
 regex-syntax: 0.8.8, "Apache-2.0 OR MIT",
 rend: 0.4.2, "MIT",
 reqsign: 0.16.5, "Apache-2.0",
@@ -663,10 +663,14 @@ rmcp-macros: 0.11.0, "MIT",
 roaring: 0.10.12, "Apache-2.0 OR MIT",
 route-recognizer: 0.3.1, "MIT",
 rsa: 0.9.9, "Apache-2.0 OR MIT",
+rust-embed: 8.9.0, "MIT",
+rust-embed-impl: 8.9.0, "MIT",
+rust-embed-utils: 8.9.0, "MIT",
 rust-ini: 0.21.3, "MIT",
 rust_decimal: 1.39.0, "MIT",
 rustc-hash: 2.1.1, "Apache-2.0 OR MIT",
 rustc_version: 0.4.1, "Apache-2.0 OR MIT",
+rusticata-macros: 4.1.0, "Apache-2.0 OR MIT",
 rustix: 1.1.2, "Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT",
 rustls: 0.23.35, "Apache-2.0 OR ISC OR MIT",
 rustls-native-certs: 0.8.2, "Apache-2.0 OR ISC OR MIT",
@@ -687,7 +691,7 @@ scoped-tls: 1.0.1, "Apache-2.0 OR MIT",
 scopeguard: 1.2.0, "Apache-2.0 OR MIT",
 sdd: 3.0.10, "Apache-2.0",
 seahash: 4.1.0, "MIT",
-sealed: 0.5.0, "Apache-2.0 OR MIT",
+sealed: 0.6.0, "Apache-2.0 OR MIT",
 sec1: 0.7.3, "Apache-2.0 OR MIT",
 secrecy: 0.10.3, "Apache-2.0 OR MIT",
 security-framework: 3.5.1, "Apache-2.0 OR MIT",
@@ -720,7 +724,7 @@ sharded-slab: 0.1.7, "MIT",
 shlex: 1.3.0, "Apache-2.0 OR MIT",
 signal-hook-registry: 1.4.7, "Apache-2.0 OR MIT",
 signature: 2.2.0, "Apache-2.0 OR MIT",
-simd-adler32: 0.3.7, "MIT",
+simd-adler32: 0.3.8, "MIT",
 simd-json: 0.17.0, "Apache-2.0 OR MIT",
 simdutf8: 0.1.5, "Apache-2.0 OR MIT",
 simple_asn1: 0.6.3, "ISC",
@@ -760,9 +764,9 @@ syn: 1.0.109, "Apache-2.0 OR MIT",
 syn: 2.0.111, "Apache-2.0 OR MIT",
 sync_wrapper: 1.0.2, "Apache-2.0",
 synstructure: 0.13.2, "MIT",
-synthez: 0.3.1, "BlueOak-1.0.0",
-synthez-codegen: 0.3.1, "BlueOak-1.0.0",
-synthez-core: 0.3.1, "BlueOak-1.0.0",
+synthez: 0.4.0, "BlueOak-1.0.0",
+synthez-codegen: 0.4.0, "BlueOak-1.0.0",
+synthez-core: 0.4.0, "BlueOak-1.0.0",
 sysinfo: 0.34.2, "MIT",
 sysinfo: 0.37.2, "MIT",
 tagptr: 0.2.0, "Apache-2.0 OR MIT",
@@ -803,7 +807,7 @@ toml_datetime: 0.6.11, "Apache-2.0 OR MIT",
 toml_datetime: 0.7.3, "Apache-2.0 OR MIT",
 toml_edit: 0.19.15, "Apache-2.0 OR MIT",
 toml_edit: 0.22.27, "Apache-2.0 OR MIT",
-toml_edit: 0.23.7, "Apache-2.0 OR MIT",
+toml_edit: 0.23.9, "Apache-2.0 OR MIT",
 toml_parser: 1.0.4, "Apache-2.0 OR MIT",
 toml_write: 0.1.2, "Apache-2.0 OR MIT",
 toml_writer: 1.0.4, "Apache-2.0 OR MIT",
@@ -825,12 +829,12 @@ trait-variant: 0.1.2, "Apache-2.0 OR MIT",
 try-lock: 0.2.5, "MIT",
 tungstenite: 0.28.0, "Apache-2.0 OR MIT",
 twox-hash: 2.1.2, "MIT",
-typed-builder: 0.15.2, "Apache-2.0 OR MIT",
 typed-builder: 0.19.1, "Apache-2.0 OR MIT",
 typed-builder: 0.20.1, "Apache-2.0 OR MIT",
-typed-builder-macro: 0.15.2, "Apache-2.0 OR MIT",
+typed-builder: 0.23.2, "Apache-2.0 OR MIT",
 typed-builder-macro: 0.19.1, "Apache-2.0 OR MIT",
 typed-builder-macro: 0.20.1, "Apache-2.0 OR MIT",
+typed-builder-macro: 0.23.2, "Apache-2.0 OR MIT",
 typenum: 1.19.0, "Apache-2.0 OR MIT",
 ucd-trie: 0.1.7, "Apache-2.0 OR MIT",
 ulid: 1.2.1, "MIT",
@@ -957,6 +961,7 @@ winnow: 0.7.14, "MIT",
 wit-bindgen: 0.46.0, "Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT",
 writeable: 0.6.2, "Unicode-3.0",
 wyz: 0.5.1, "MIT",
+x509-parser: 0.18.0, "Apache-2.0 OR MIT",
 xattr: 1.6.1, "Apache-2.0 OR MIT",
 yansi: 1.0.1, "Apache-2.0 OR MIT",
 yasna: 0.5.2, "Apache-2.0 OR MIT",
@@ -976,7 +981,7 @@ zerotrie: 0.2.3, "Unicode-3.0",
 zerovec: 0.11.5, "Unicode-3.0",
 zerovec-derive: 0.11.2, "Unicode-3.0",
 zip: 6.0.0, "MIT",
-zlib-rs: 0.5.2, "Zlib",
+zlib-rs: 0.5.4, "Zlib",
 zopfli: 0.8.3, "Apache-2.0",
 zstd: 0.13.3, "MIT",
 zstd-safe: 7.2.4, "Apache-2.0 OR MIT",
diff --git a/bdd/rust/Cargo.toml b/bdd/rust/Cargo.toml
index 4182a31d9..5f8fe779b 100644
--- a/bdd/rust/Cargo.toml
+++ b/bdd/rust/Cargo.toml
@@ -27,7 +27,7 @@ bdd = []
 
 [dev-dependencies]
 bytes = { workspace = true }
-cucumber = "0.21"
+cucumber = "0.22"
 iggy = { workspace = true }
 integration = { workspace = true }
 tokio = { workspace = true }
diff --git a/core/bench/dashboard/server/Cargo.toml 
b/core/bench/dashboard/server/Cargo.toml
index 217b8de00..7552ba3b1 100644
--- a/core/bench/dashboard/server/Cargo.toml
+++ b/core/bench/dashboard/server/Cargo.toml
@@ -30,7 +30,7 @@ bench-report = { workspace = true }
 chrono = { workspace = true, features = ["serde"] }
 clap = { workspace = true }
 dashmap = { workspace = true }
-file-operation = "0.8.6"
+file-operation = "0.8.8"
 notify = "8.2.0"
 octocrab = "0.48.1"
 serde = { workspace = true, features = ["derive"] }
diff --git a/core/common/Cargo.toml b/core/common/Cargo.toml
index 4da4e40b1..fbfe9b3a5 100644
--- a/core/common/Cargo.toml
+++ b/core/common/Cargo.toml
@@ -55,7 +55,7 @@ figment = { workspace = true }
 human-repr = { workspace = true }
 humantime = { workspace = true }
 once_cell = { workspace = true }
-rcgen = "0.14.5"
+rcgen = "0.14.6"
 rustls = { workspace = true }
 serde = { workspace = true }
 serde_json = { workspace = true }
diff --git a/core/configs/server.toml b/core/configs/server.toml
index 581763acf..b1a25b61d 100644
--- a/core/configs/server.toml
+++ b/core/configs/server.toml
@@ -53,7 +53,7 @@ allowed_origins = ["*"]
 
 # Lists allowed headers that can be used in CORS requests.
 # For example, ["content-type"] permits only the content-type header.
-allowed_headers = ["content-type"]
+allowed_headers = ["content-type", "authorization"]
 
 # Headers that browsers are allowed to access in CORS responses.
 # An empty array means no additional headers are exposed to browsers.
diff --git a/core/integration/Cargo.toml b/core/integration/Cargo.toml
index f7e5a1067..6d97946f4 100644
--- a/core/integration/Cargo.toml
+++ b/core/integration/Cargo.toml
@@ -47,7 +47,7 @@ libc = "0.2.178"
 log = { workspace = true }
 predicates = { workspace = true }
 rand = { workspace = true }
-rcgen = "0.14.5"
+rcgen = "0.14.6"
 reqwest = { workspace = true }
 rmcp = { version = "0.11.0", features = [
     "client",
diff --git a/core/server/Cargo.toml b/core/server/Cargo.toml
index 3688a4fb0..bb375bdf5 100644
--- a/core/server/Cargo.toml
+++ b/core/server/Cargo.toml
@@ -35,6 +35,7 @@ path = "src/main.rs"
 default = ["mimalloc"]
 disable-mimalloc = []
 mimalloc = ["dep:mimalloc"]
+iggy-web = ["dep:rust-embed", "dep:mime_guess"]
 
 [dependencies]
 ahash = { workspace = true }
@@ -74,6 +75,7 @@ iggy_common = { workspace = true }
 jsonwebtoken = { version = "10.2.0", features = ["rust_crypto"] }
 lending-iterator = "0.1.7"
 mimalloc = { workspace = true, optional = true }
+mime_guess = { version = "2.0", optional = true }
 moka = { version = "0.12.11", features = ["future"] }
 nix = { version = "0.30", features = ["fs", "resource"] }
 opentelemetry = { version = "0.31.0", features = ["trace", "logs"] }
@@ -100,6 +102,7 @@ rand = { workspace = true }
 reqwest = { workspace = true, features = ["rustls-tls-no-provider"] }
 ring = "0.17.14"
 ringbuffer = "0.16.0"
+rust-embed = { version = "8.9.0", optional = true }
 rustls = { workspace = true }
 rustls-pemfile = "2.2.0"
 send_wrapper = "0.6.0"
diff --git a/core/server/src/http/http_server.rs 
b/core/server/src/http/http_server.rs
index 21e7cbcdb..e83c2191d 100644
--- a/core/server/src/http/http_server.rs
+++ b/core/server/src/http/http_server.rs
@@ -117,6 +117,12 @@ pub async fn start_http_server(
 
     app = app.layer(middleware::from_fn(request_diagnostics));
 
+    #[cfg(feature = "iggy-web")]
+    {
+        app = app.merge(web::router());
+        info!("Web UI enabled at /ui");
+    }
+
     if !config.tls.enabled {
         let listener = TcpListener::bind(config.address.clone())
             .await
diff --git a/core/server/src/http/jwt/middleware.rs 
b/core/server/src/http/jwt/middleware.rs
index b2ed614d5..a47d83ad6 100644
--- a/core/server/src/http/jwt/middleware.rs
+++ b/core/server/src/http/jwt/middleware.rs
@@ -41,6 +41,7 @@ const PUBLIC_PATHS: &[&str] = &[
     "/users/login",
     "/users/refresh-token",
     "/personal-access-tokens/login",
+    "/ui",
 ];
 
 pub async fn jwt_auth(
diff --git a/core/server/src/http/mod.rs b/core/server/src/http/mod.rs
index 4cf663d36..1d6046ebb 100644
--- a/core/server/src/http/mod.rs
+++ b/core/server/src/http/mod.rs
@@ -34,5 +34,7 @@ pub mod streams;
 pub mod system;
 pub mod topics;
 pub mod users;
+#[cfg(feature = "iggy-web")]
+pub mod web;
 
 pub const COMPONENT: &str = "HTTP";
diff --git a/core/server/src/http/web.rs b/core/server/src/http/web.rs
new file mode 100644
index 000000000..76e740e70
--- /dev/null
+++ b/core/server/src/http/web.rs
@@ -0,0 +1,83 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use axum::Router;
+use axum::body::Body;
+use axum::extract::Path;
+use axum::http::{Response, StatusCode, header};
+use axum::response::IntoResponse;
+use axum::routing::get;
+use rust_embed::{Embed, EmbeddedFile};
+
+#[derive(Embed)]
+#[folder = "../../web/build/static/"]
+#[allow_missing = true]
+struct WebAssets;
+
+impl WebAssets {
+    fn get_file(path: &str) -> Option<EmbeddedFile> {
+        <Self as Embed>::get(path)
+    }
+}
+
+pub fn router() -> Router {
+    Router::new()
+        .route("/ui/{*wildcard}", get(serve_web_asset))
+        .route("/ui", get(serve_index))
+        .route("/ui/", get(serve_index))
+}
+
+async fn serve_index() -> impl IntoResponse {
+    serve_file("index.html")
+}
+
+async fn serve_web_asset(Path(wildcard): Path<String>) -> impl IntoResponse {
+    if let Some(response) = try_serve_file(&wildcard) {
+        return response;
+    }
+
+    if !wildcard.contains('.') {
+        return serve_file("index.html");
+    }
+
+    Response::builder()
+        .status(StatusCode::NOT_FOUND)
+        .body(Body::from("Not Found"))
+        .unwrap()
+}
+
+fn try_serve_file(path: &str) -> Option<Response<Body>> {
+    let asset = WebAssets::get_file(path)?;
+    let mime = mime_guess::from_path(path).first_or_octet_stream();
+
+    Some(
+        Response::builder()
+            .status(StatusCode::OK)
+            .header(header::CONTENT_TYPE, mime.as_ref())
+            .body(Body::from(asset.data.into_owned()))
+            .unwrap(),
+    )
+}
+
+fn serve_file(path: &str) -> Response<Body> {
+    try_serve_file(path).unwrap_or_else(|| {
+        Response::builder()
+            .status(StatusCode::NOT_FOUND)
+            .body(Body::from("Not Found"))
+            .unwrap()
+    })
+}
diff --git a/web/package-lock.json b/web/package-lock.json
index d8f1a6e49..d7876bcb5 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -25,6 +25,7 @@
         "@eslint/compat": "^2.0.0",
         "@eslint/js": "^9.39.1",
         "@sveltejs/adapter-node": "^5.4.0",
+        "@sveltejs/adapter-static": "^3.0.0",
         "@sveltejs/kit": "^2.48.5",
         "@sveltejs/vite-plugin-svelte": "^6.2.1",
         "@types/d3-interpolate": "^3.0.4",
@@ -42,7 +43,7 @@
         "prettier-plugin-svelte": "^3.4.0",
         "svelte": "^5.43.6",
         "svelte-check": "^4.3.4",
-        "sveltekit-superforms": "^2.28.1",
+        "sveltekit-superforms": "^2.14.0",
         "tailwindcss": "^4.1.17",
         "ts-toolbelt": "^9.6.0",
         "tslib": "^2.8.1",
@@ -65,20 +66,20 @@
       }
     },
     "node_modules/@ark/schema": {
-      "version": "0.54.0",
-      "resolved": "https://registry.npmjs.org/@ark/schema/-/schema-0.54.0.tgz";,
-      "integrity": 
"sha512-QloFou+ODfb5qXgPxX1EbJyRqZEwoElzvJ6VuuFVvWJQGoigBEUW3L0HejXG/B9v8K3VvDikuULp5ELSwZn8hg==",
+      "version": "0.56.0",
+      "resolved": "https://registry.npmjs.org/@ark/schema/-/schema-0.56.0.tgz";,
+      "integrity": 
"sha512-ECg3hox/6Z/nLajxXqNhgPtNdHWC9zNsDyskwO28WinoFEnWow4IsERNz9AnXRhTZJnYIlAJ4uGn3nlLk65vZA==",
       "dev": true,
       "license": "MIT",
       "optional": true,
       "dependencies": {
-        "@ark/util": "0.54.0"
+        "@ark/util": "0.56.0"
       }
     },
     "node_modules/@ark/util": {
-      "version": "0.54.0",
-      "resolved": "https://registry.npmjs.org/@ark/util/-/util-0.54.0.tgz";,
-      "integrity": 
"sha512-ugTfpEDGA6d2uU2/3M7uRiuPNYckQMhg2wfNMw8zZHXjPhuVCbXWZzIcBojuXuCN8j4MrNWNqQ9z7f8W/JiE8w==",
+      "version": "0.56.0",
+      "resolved": "https://registry.npmjs.org/@ark/util/-/util-0.56.0.tgz";,
+      "integrity": 
"sha512-BghfRC8b9pNs3vBoDJhcta0/c1J1rsoS1+HgVUreMFPdhz/CRAKReAu57YEllNaSy98rWAdY1gE+gFup7OXpgA==",
       "dev": true,
       "license": "MIT",
       "optional": true
@@ -802,22 +803,6 @@
         "esbuild-runner": ">= 2.2.2"
       }
     },
-    "node_modules/@gcornut/valibot-json-schema/node_modules/valibot": {
-      "version": "0.42.1",
-      "resolved": "https://registry.npmjs.org/valibot/-/valibot-0.42.1.tgz";,
-      "integrity": 
"sha512-3keXV29Ar5b//Hqi4MbSdV7lfVp6zuYLZuA9V1PvQUsXqogr+u5lvLPLk3A4f74VUXDnf/JfWMN6sB+koJ/FFw==",
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "peerDependencies": {
-        "typescript": ">=5"
-      },
-      "peerDependenciesMeta": {
-        "typescript": {
-          "optional": true
-        }
-      }
-    },
     "node_modules/@hapi/hoek": {
       "version": "9.3.0",
       "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz";,
@@ -1461,6 +1446,16 @@
         "@sveltejs/kit": "^2.4.0"
       }
     },
+    "node_modules/@sveltejs/adapter-static": {
+      "version": "3.0.10",
+      "resolved": 
"https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.10.tgz";,
+      "integrity": 
"sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew==",
+      "dev": true,
+      "license": "MIT",
+      "peerDependencies": {
+        "@sveltejs/kit": "^2.0.0"
+      }
+    },
     "node_modules/@sveltejs/kit": {
       "version": "2.49.0",
       "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.49.0.tgz";,
@@ -1867,9 +1862,9 @@
       "license": "MIT"
     },
     "node_modules/@types/validator": {
-      "version": "13.15.9",
-      "resolved": 
"https://registry.npmjs.org/@types/validator/-/validator-13.15.9.tgz";,
-      "integrity": 
"sha512-9ENIuq9PUX45M1QRtfJDprgfErED4fBiMPmjlPci4W9WiBelVtHYCjF3xkQNcSnmUeuruLS1kH6hSl5M1vz4Sw==",
+      "version": "13.15.10",
+      "resolved": 
"https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz";,
+      "integrity": 
"sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==",
       "dev": true,
       "license": "MIT",
       "optional": true
@@ -2271,35 +2266,27 @@
       }
     },
     "node_modules/arkregex": {
-      "version": "0.0.2",
-      "resolved": "https://registry.npmjs.org/arkregex/-/arkregex-0.0.2.tgz";,
-      "integrity": 
"sha512-ttjDUICBVoXD/m8bf7eOjx8XMR6yIT2FmmW9vsN0FCcFOygEZvvIX8zK98tTdXkzi0LkRi5CmadB44jFEIyDNA==",
+      "version": "0.0.5",
+      "resolved": "https://registry.npmjs.org/arkregex/-/arkregex-0.0.5.tgz";,
+      "integrity": 
"sha512-ncYjBdLlh5/QnVsAA8De16Tc9EqmYM7y/WU9j+236KcyYNUXogpz3sC4ATIZYzzLxwI+0sEOaQLEmLmRleaEXw==",
       "dev": true,
       "license": "MIT",
       "optional": true,
       "dependencies": {
-        "@ark/util": "0.53.0"
+        "@ark/util": "0.56.0"
       }
     },
-    "node_modules/arkregex/node_modules/@ark/util": {
-      "version": "0.53.0",
-      "resolved": "https://registry.npmjs.org/@ark/util/-/util-0.53.0.tgz";,
-      "integrity": 
"sha512-TGn4gLlA6dJcQiqrtCtd88JhGb2XBHo6qIejsDre+nxpGuUVW4G3YZGVrwjNBTO0EyR+ykzIo4joHJzOj+/cpA==",
-      "dev": true,
-      "license": "MIT",
-      "optional": true
-    },
     "node_modules/arktype": {
-      "version": "2.1.26",
-      "resolved": "https://registry.npmjs.org/arktype/-/arktype-2.1.26.tgz";,
-      "integrity": 
"sha512-zDwukKV6uTElKCAbIoQ9OU6shXE5ALjvZAqHErOSv6l0iLKlubELZ7AcevYLaWFYr5rmIN4Uv9+dIzInktSO1A==",
+      "version": "2.1.29",
+      "resolved": "https://registry.npmjs.org/arktype/-/arktype-2.1.29.tgz";,
+      "integrity": 
"sha512-jyfKk4xIOzvYNayqnD8ZJQqOwcrTOUbIU4293yrzAjA3O1dWh61j71ArMQ6tS/u4pD7vabSPe7nG3RCyoXW6RQ==",
       "dev": true,
       "license": "MIT",
       "optional": true,
       "dependencies": {
-        "@ark/schema": "0.54.0",
-        "@ark/util": "0.54.0",
-        "arkregex": "0.0.2"
+        "@ark/schema": "0.56.0",
+        "@ark/util": "0.56.0",
+        "arkregex": "0.0.5"
       }
     },
     "node_modules/autoprefixer": {
@@ -2520,16 +2507,16 @@
       }
     },
     "node_modules/class-validator": {
-      "version": "0.14.2",
-      "resolved": 
"https://registry.npmjs.org/class-validator/-/class-validator-0.14.2.tgz";,
-      "integrity": 
"sha512-3kMVRF2io8N8pY1IFIXlho9r8IPUUIfHe2hYVtiebvAzU2XeQFXTv+XI4WX+TnXmtwXMDcjngcpkiPM0O9PvLw==",
+      "version": "0.14.3",
+      "resolved": 
"https://registry.npmjs.org/class-validator/-/class-validator-0.14.3.tgz";,
+      "integrity": 
"sha512-rXXekcjofVN1LTOSw+u4u9WXVEUvNBVjORW154q/IdmYWy1nMbOU9aNtZB0t8m+FJQ9q91jlr2f9CwwUFdFMRA==",
       "dev": true,
       "license": "MIT",
       "optional": true,
       "dependencies": {
-        "@types/validator": "^13.11.8",
+        "@types/validator": "^13.15.3",
         "libphonenumber-js": "^1.11.1",
-        "validator": "^13.9.0"
+        "validator": "^13.15.20"
       }
     },
     "node_modules/clsx": {
@@ -2712,9 +2699,9 @@
       "optional": true
     },
     "node_modules/effect": {
-      "version": "3.19.3",
-      "resolved": "https://registry.npmjs.org/effect/-/effect-3.19.3.tgz";,
-      "integrity": 
"sha512-LodiPXiyUJWQ5LoMhUGbu0acD2ff5A5teJtUlLKDPVfoeWEBcZLlzK8BeVXpVa0f30UsdHouVCf0C/E0TxYMrA==",
+      "version": "3.19.12",
+      "resolved": "https://registry.npmjs.org/effect/-/effect-3.19.12.tgz";,
+      "integrity": 
"sha512-7F9RGTrCTC3D7nh9Zw+3VlJWwZgo5k33KA+476BAaD0rKIXKZsY/jQ+ipyhR/Avo239Fi6GqAVFs1mqM1IJ7yg==",
       "dev": true,
       "license": "MIT",
       "optional": true,
@@ -3591,9 +3578,9 @@
       }
     },
     "node_modules/libphonenumber-js": {
-      "version": "1.12.26",
-      "resolved": 
"https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.26.tgz";,
-      "integrity": 
"sha512-MagMOuqEXB2Pa90cWE+BoCmcKJx+de5uBIicaUkQ+uiEslZ0OBMNOkSZT/36syXNHu68UeayTxPm3DYM2IHoLQ==",
+      "version": "1.12.31",
+      "resolved": 
"https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.31.tgz";,
+      "integrity": 
"sha512-Z3IhgVgrqO1S5xPYM3K5XwbkDasU67/Vys4heW+lfSBALcUZjeIIzI8zCLifY+OCzSq+fpDdywMDa7z+4srJPQ==",
       "dev": true,
       "license": "MIT",
       "optional": true
@@ -5026,9 +5013,9 @@
       }
     },
     "node_modules/typebox": {
-      "version": "1.0.53",
-      "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.0.53.tgz";,
-      "integrity": 
"sha512-fCi3wnKP4owdhs+LRIUfkPzR4p5RLa3tM4O6gqO7TeAo6SsamvqA59yPb1GyKDcCgB/ClmifVJ9BqQ128a4uvQ==",
+      "version": "1.0.62",
+      "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.0.62.tgz";,
+      "integrity": 
"sha512-iWxUfvzC5tGZWFrhoPLUKQJgFif4aDVgyaBbCURFIswex8to1JsnwDtenM2GqNI1VC/vqNlHtGc0ZR5eh0vbww==",
       "dev": true,
       "license": "MIT",
       "optional": true
@@ -5140,9 +5127,9 @@
       }
     },
     "node_modules/valibot": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.1.0.tgz";,
-      "integrity": 
"sha512-Nk8lX30Qhu+9txPYTwM0cFlWLdPFsFr6LblzqIySfbZph9+BFsAHsNvHOymEviUepeIW6KFHzpX8TKhbptBXXw==",
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.2.0.tgz";,
+      "integrity": 
"sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==",
       "dev": true,
       "license": "MIT",
       "optional": true,
diff --git a/web/package.json b/web/package.json
index 63800cb17..95e232734 100644
--- a/web/package.json
+++ b/web/package.json
@@ -5,6 +5,7 @@
   "scripts": {
     "dev": "vite dev --port 3050",
     "build": "vite build",
+    "build:static": "STATIC_BUILD=true PUBLIC_IGGY_API_URL= vite build",
     "preview": "vite preview",
     "server": "vite server",
     "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
@@ -16,6 +17,7 @@
     "@eslint/compat": "^2.0.0",
     "@eslint/js": "^9.39.1",
     "@sveltejs/adapter-node": "^5.4.0",
+    "@sveltejs/adapter-static": "^3.0.0",
     "@sveltejs/kit": "^2.48.5",
     "@sveltejs/vite-plugin-svelte": "^6.2.1",
     "@types/d3-interpolate": "^3.0.4",
@@ -33,7 +35,7 @@
     "prettier-plugin-svelte": "^3.4.0",
     "svelte": "^5.43.6",
     "svelte-check": "^4.3.4",
-    "sveltekit-superforms": "^2.28.1",
+    "sveltekit-superforms": "^2.14.0",
     "tailwindcss": "^4.1.17",
     "ts-toolbelt": "^9.6.0",
     "tslib": "^2.8.1",
@@ -57,7 +59,8 @@
     "uuid": "^13.0.0"
   },
   "overrides": {
-    "cookie": "^0.7.0"
+    "cookie": "^0.7.0",
+    "valibot": "^1.2.0"
   },
   "type": "module"
 }
diff --git a/web/src/hooks.client.ts b/web/src/hooks.client.ts
index d3f825db2..e5fb6b240 100644
--- a/web/src/hooks.client.ts
+++ b/web/src/hooks.client.ts
@@ -17,12 +17,7 @@
  * under the License.
  */
 
-import type { HandleClientError } from '@sveltejs/kit';
+import { authStore } from '$lib/auth/authStore.svelte';
 
-export const handleError: HandleClientError = async ({ error }) => {
-  console.log('client error handler', error);
-  return {
-    message: 'Whoops!',
-    errorId: 1
-  };
-};
+// Initialize auth store when the app loads
+authStore.initialize();
diff --git a/web/src/hooks.server.ts b/web/src/hooks.server.ts
deleted file mode 100644
index 6285ec07a..000000000
--- a/web/src/hooks.server.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { env } from '$env/dynamic/public';
-import { checkIfPathnameIsPublic, typedRoute } from '$lib/types/appRoutes';
-import { tokens } from '$lib/utils/constants/tokens';
-import type { Handle } from '@sveltejs/kit';
-import { sequence } from '@sveltejs/kit/hooks';
-import { redirect } from '@sveltejs/kit';
-
-console.log(`Iggy API URL: ${env.PUBLIC_IGGY_API_URL}`);
-
-const accessTokenOkRedirects = [
-  {
-    from: '/dashboard/',
-    to: typedRoute('/dashboard/overview')
-  },
-  {
-    from: '/dashboard',
-    to: typedRoute('/dashboard/overview')
-  },
-  {
-    from: '/',
-    to: typedRoute('/dashboard/overview')
-  },
-  {
-    from: '/auth',
-    to: typedRoute('/dashboard/overview')
-  },
-  {
-    from: typedRoute('/auth/sign-in'),
-    to: typedRoute('/dashboard/overview')
-  }
-];
-
-const handleAuth: Handle = async ({ event, resolve }) => {
-  const cookies = event.cookies;
-  const isPublicPath = checkIfPathnameIsPublic(event.url.pathname);
-  const accessToken = cookies.get(tokens.accessToken);
-
-  if (!accessToken) {
-    if (isPublicPath) {
-      return resolve(event);
-    } else {
-      redirect(302, typedRoute('/auth/sign-in'));
-    }
-  }
-
-  const invalidPathRedirect = accessTokenOkRedirects.find((r) => r.from === 
event.url.pathname);
-  if (invalidPathRedirect) {
-    redirect(302, invalidPathRedirect.to);
-  }
-
-  return resolve(event);
-};
-
-export const handle = sequence(handleAuth);
diff --git a/web/src/lib/api/ApiSchema.ts b/web/src/lib/api/ApiSchema.ts
index 1b2dcbab2..a18f2dce5 100644
--- a/web/src/lib/api/ApiSchema.ts
+++ b/web/src/lib/api/ApiSchema.ts
@@ -183,4 +183,8 @@ type Stats = {
   path: '/stats';
 };
 
-export type ApiSchema = Users | Streams | Stats | Topics | Auth;
+type BaseApiSchema = Users | Streams | Stats | Topics | Auth;
+
+export type ApiSchema = BaseApiSchema & {
+  queryParams?: Record<string, string | number | boolean>;
+};
diff --git a/web/src/lib/api/clientApi.ts b/web/src/lib/api/clientApi.ts
new file mode 100644
index 000000000..fb1372371
--- /dev/null
+++ b/web/src/lib/api/clientApi.ts
@@ -0,0 +1,106 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { browser } from '$app/environment';
+import { goto } from '$app/navigation';
+import { resolve } from '$app/paths';
+import { env } from '$env/dynamic/public';
+import { authStore } from '$lib/auth/authStore.svelte';
+import { typedRoute } from '$lib/types/appRoutes';
+import { error } from '@sveltejs/kit';
+import { getJson } from './getJson';
+
+export interface ApiRequest {
+  path: string;
+  method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
+  body?: unknown;
+  queryParams?: Record<string, string>;
+}
+
+export async function clientApi<T = unknown>(args: ApiRequest): Promise<T> {
+  const { path, method, queryParams, body } = args;
+
+  const headers = new Headers();
+  headers.set('Content-Type', 'application/json');
+
+  const token = authStore.getAccessToken();
+  if (token) {
+    headers.set('Authorization', `Bearer ${token}`);
+  }
+
+  // Use PUBLIC_IGGY_API_URL if set, otherwise use relative path (for embedded 
mode)
+  const baseUrl = env.PUBLIC_IGGY_API_URL || '';
+  let fullUrl = `${baseUrl}${path}`;
+
+  if (queryParams) {
+    const query = new URLSearchParams(Object.entries(queryParams));
+    fullUrl += '?' + query.toString();
+  }
+
+  const response = await fetch(fullUrl, {
+    headers,
+    method,
+    ...(body ? { body: JSON.stringify(body) } : {})
+  });
+
+  const data = await getJson(response);
+
+  if (response.ok) {
+    return data as T;
+  }
+
+  // Handle errors
+  if (response.status === 401 || response.status === 403) {
+    if (browser) {
+      authStore.logout();
+    }
+    error(401, { message: 'Unauthorized' });
+  }
+
+  if (response.status === 404) {
+    error(404, { message: 'Not Found' });
+  }
+
+  if (response.status === 400) {
+    // Return the error data for form validation
+    throw { status: 400, data };
+  }
+
+  error(500, { message: 'Internal server error' });
+}
+
+/**
+ * Wrapper that handles errors gracefully for load functions
+ */
+export async function clientApiSafe<T = unknown>(
+  args: ApiRequest
+): Promise<{ data: T | null; error: string | null }> {
+  try {
+    const data = await clientApi<T>(args);
+    return { data, error: null };
+  } catch (e: any) {
+    if (e?.status === 401 || e?.status === 403) {
+      if (browser) {
+        goto(resolve(typedRoute('/auth/sign-in')));
+      }
+      return { data: null, error: 'Unauthorized' };
+    }
+    return { data: null, error: e?.message || 'Unknown error' };
+  }
+}
diff --git a/web/src/lib/api/fetchRouteApi.ts b/web/src/lib/api/fetchRouteApi.ts
index 894c094e2..fd461262e 100644
--- a/web/src/lib/api/fetchRouteApi.ts
+++ b/web/src/lib/api/fetchRouteApi.ts
@@ -17,15 +17,46 @@
  * under the License.
  */
 
+import { env } from '$env/dynamic/public';
+import { authStore } from '$lib/auth/authStore.svelte';
 import type { ApiSchema } from './ApiSchema';
+import { convertBigIntsToStrings } from './convertBigIntsToStrings';
 import { getJson } from './getJson';
 
 export const fetchRouteApi = async (
   arg: ApiSchema
 ): Promise<{ data: any; status: number; ok: boolean }> => {
   try {
-    const res = await fetch('/api/proxy', { body: JSON.stringify(arg), method: 
'POST' });
-    return (await getJson(res)) as any;
+    const { path, method, queryParams } = arg;
+
+    const headers = new Headers();
+    headers.set('Content-Type', 'application/json');
+
+    const token = authStore.getAccessToken();
+    if (token) {
+      headers.set('Authorization', `Bearer ${token}`);
+    }
+
+    // Use PUBLIC_IGGY_API_URL if set, otherwise use relative path (for 
embedded mode)
+    const baseUrl = env.PUBLIC_IGGY_API_URL || '';
+    let fullUrl = `${baseUrl}${path}`;
+
+    if (queryParams) {
+      const params = Object.entries(queryParams).map(([k, v]) => [k, 
String(v)]);
+      const query = new URLSearchParams(params);
+      fullUrl += '?' + query.toString();
+    }
+
+    const res = await fetch(fullUrl, {
+      headers,
+      method,
+      ...('body' in arg && arg.body ? { body: JSON.stringify(arg.body) } : {})
+    });
+
+    const data = await getJson(res);
+    const safeData = convertBigIntsToStrings(data);
+
+    return { data: safeData, status: res.status, ok: res.ok };
   } catch (err) {
     throw new Error('fetchRouteApi error');
   }
diff --git a/web/src/lib/api/handleFetchErrors.ts 
b/web/src/lib/api/handleFetchErrors.ts
index b3bb75f66..3ef5559fc 100644
--- a/web/src/lib/api/handleFetchErrors.ts
+++ b/web/src/lib/api/handleFetchErrors.ts
@@ -18,6 +18,7 @@
  */
 
 import { error, type Cookies, redirect } from '@sveltejs/kit';
+import { base } from '$app/paths';
 import { getJson } from './getJson';
 import { tokens } from '$lib/utils/constants/tokens';
 import { typedRoute } from '$lib/types/appRoutes';
@@ -57,15 +58,14 @@ export const handleFetchErrors = async (
       };
     },
     401: () => {
-      // TODO: Refresh token
       console.log(`handleErrorStatus: 401 ${response.url}`);
       removeCookies();
-      redirect(302, typedRoute('/auth/sign-in'));
+      redirect(302, `${base}${typedRoute('/auth/sign-in')}`);
     },
     403: () => {
       console.log(`handleErrorStatus: 403 ${response.url}`);
       removeCookies();
-      redirect(302, typedRoute('/auth/sign-in'));
+      redirect(302, `${base}${typedRoute('/auth/sign-in')}`);
     },
     404: () => {
       console.log(`handleErrorStatus: 404 ${response.url}`);
diff --git a/web/src/lib/auth/authStore.svelte.ts 
b/web/src/lib/auth/authStore.svelte.ts
new file mode 100644
index 000000000..fdd5511e0
--- /dev/null
+++ b/web/src/lib/auth/authStore.svelte.ts
@@ -0,0 +1,125 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { browser } from '$app/environment';
+import { goto } from '$app/navigation';
+import { resolve } from '$app/paths';
+import { typedRoute } from '$lib/types/appRoutes';
+import { tokens } from '$lib/utils/constants/tokens';
+import { jwtDecode } from 'jwt-decode';
+
+export interface AuthState {
+  isAuthenticated: boolean;
+  accessToken: string | null;
+  userId: number | null;
+}
+
+function createAuthStore() {
+  let state = $state<AuthState>({
+    isAuthenticated: false,
+    accessToken: null,
+    userId: null
+  });
+
+  function getTokenFromCookie(): string | null {
+    if (!browser) return null;
+    const match = document.cookie.match(new RegExp(`(^| 
)${tokens.accessToken}=([^;]+)`));
+    return match ? match[2] : null;
+  }
+
+  function setTokenCookie(token: string, expiry: number): void {
+    if (!browser) return;
+    const expires = new Date(expiry * 1000);
+    document.cookie = `${tokens.accessToken}=${token}; path=/; 
expires=${expires.toUTCString()}; SameSite=Lax`;
+  }
+
+  function clearTokenCookie(): void {
+    if (!browser) return;
+    document.cookie = `${tokens.accessToken}=; path=/; expires=Thu, 01 Jan 
1970 00:00:00 GMT`;
+  }
+
+  function initialize(): void {
+    const token = getTokenFromCookie();
+    if (token) {
+      try {
+        const decoded = jwtDecode(token);
+        const userId = decoded.sub ? parseInt(decoded.sub, 10) : null;
+        state = {
+          isAuthenticated: true,
+          accessToken: token,
+          userId
+        };
+      } catch {
+        clearTokenCookie();
+        state = { isAuthenticated: false, accessToken: null, userId: null };
+      }
+    }
+  }
+
+  function login(token: string, expiry: number): void {
+    setTokenCookie(token, expiry);
+    try {
+      const decoded = jwtDecode(token);
+      const userId = decoded.sub ? parseInt(decoded.sub, 10) : null;
+      state = {
+        isAuthenticated: true,
+        accessToken: token,
+        userId
+      };
+    } catch {
+      state = { isAuthenticated: true, accessToken: token, userId: null };
+    }
+  }
+
+  function logout(): void {
+    clearTokenCookie();
+    state = { isAuthenticated: false, accessToken: null, userId: null };
+    goto(resolve(typedRoute('/auth/sign-in')));
+  }
+
+  function getAccessToken(): string | null {
+    return state.accessToken || getTokenFromCookie();
+  }
+
+  // Initialize on creation if in browser
+  if (browser) {
+    initialize();
+  }
+
+  return {
+    get state() {
+      return state;
+    },
+    get isAuthenticated() {
+      return state.isAuthenticated;
+    },
+    get accessToken() {
+      return state.accessToken;
+    },
+    get userId() {
+      return state.userId;
+    },
+    initialize,
+    login,
+    logout,
+    getAccessToken
+  };
+}
+
+export const authStore = createAuthStore();
diff --git a/web/src/lib/components/Breadcrumbs.svelte 
b/web/src/lib/components/Breadcrumbs.svelte
index b90f70e2d..d83adef32 100644
--- a/web/src/lib/components/Breadcrumbs.svelte
+++ b/web/src/lib/components/Breadcrumbs.svelte
@@ -65,7 +65,7 @@
     return { path, label: segment };
   }
 
-  let parts = $derived(page.url.pathname.split('/').filter(Boolean).slice(1));
+  let parts = $derived(page.url.pathname.replace(/^\/ui/, 
'').split('/').filter(Boolean).slice(1));
 
   let crumbs = $derived(parts.map((segment, index) => 
formatPathSegment(segment, index, parts)));
 </script>
diff --git a/web/src/lib/components/Header.svelte 
b/web/src/lib/components/Header.svelte
index 4f4388565..4906aba8f 100644
--- a/web/src/lib/components/Header.svelte
+++ b/web/src/lib/components/Header.svelte
@@ -26,11 +26,12 @@
   import DropdownMenu from './DropdownMenu/DropdownMenu.svelte';
   import { theme } from './ThemeController.svelte';
   import { typedRoute } from '$lib/types/appRoutes';
+  import { resolve } from '$app/paths';
   import Button from './Button.svelte';
   import StopPropagation from './StopPropagation.svelte';
 
   interface Props {
-    user: User;
+    user: User | null;
   }
 
   let { user }: Props = $props();
@@ -63,7 +64,7 @@
       {#snippet children({ close: _close })}
         <div class="p-1 min-w-[150px] transition-all duration-200 flex 
flex-col text-sm text-color">
           <span class="flex items-center justify-between gap-2 border-b px-2 
py-3">
-            <span>{user.username}</span>
+            <span>{user?.username ?? 'User'}</span>
             <Icon
               name="user"
               class="w-[24px] h-[24px] stroke-black dark:stroke-white"
@@ -98,15 +99,13 @@
               />
             </label>
           </div>
-          <form class="w-full" method="POST" 
action={typedRoute('/auth/logout')}>
-            <button
-              class="flex w-full items-center justify-between gap-2 px-2 py-2 
hover:bg-shade-l300 dark:hover:bg-shade-d1000 rounded-md my-1 
dark:hover:text-white"
-            >
-              <span>Log Out</span>
-
-              <Icon name="logout" />
-            </button>
-          </form>
+          <a
+            href={resolve(typedRoute('/auth/logout'))}
+            class="flex w-full items-center justify-between gap-2 px-2 py-2 
hover:bg-shade-l300 dark:hover:bg-shade-d1000 rounded-md my-1 
dark:hover:text-white"
+          >
+            <span>Log Out</span>
+            <Icon name="logout" />
+          </a>
         </div>
       {/snippet}
     </DropdownMenu>
diff --git a/web/src/lib/components/Layouts/SettingsLayout.svelte 
b/web/src/lib/components/Layouts/SettingsLayout.svelte
index a2b28ba4a..ee7fd6912 100644
--- a/web/src/lib/components/Layouts/SettingsLayout.svelte
+++ b/web/src/lib/components/Layouts/SettingsLayout.svelte
@@ -41,25 +41,25 @@
       tab: 'server',
       icon: 'adjustments',
       name: 'Server',
-      href: typedRoute('/dashboard/settings/server')
+      href: resolve(typedRoute('/dashboard/settings/server'))
     },
     {
       tab: 'webUI',
       icon: 'settings',
       name: 'Web UI',
-      href: typedRoute('/dashboard/settings/webUI')
+      href: resolve(typedRoute('/dashboard/settings/webUI'))
     },
     {
       tab: 'users',
       icon: 'usersGroup',
       name: 'Users',
-      href: typedRoute('/dashboard/settings/users')
+      href: resolve(typedRoute('/dashboard/settings/users'))
     }
     // {
     //   name: 'Terminal',
     //   icon: 'terminal',
     //   tab: 'terminal',
-    //   href: typedRoute('/dashboard/settings/terminal')
+    //   href: resolve(typedRoute('/dashboard/settings/terminal'))
     // }
   ] satisfies { tab: Tabs; name: string; icon: iconType; href: string }[];
 </script>
@@ -74,7 +74,7 @@
   {#each tabs as { icon, name, href }, idx (idx)}
     {@const isActive = activeTab === href.split('/').slice(-1)[0]}
     <a
-      href={resolve(href)}
+      {href}
       class={twMerge('pb-3 relative group flex items-center  justify-start 
gap-2 text-color')}
     >
       <Icon name={icon} class="w-[15px] h-[15px]" />
diff --git a/web/src/lib/components/Logo/Logo.svelte 
b/web/src/lib/components/Logo/Logo.svelte
index dd561ef0a..002456b2d 100644
--- a/web/src/lib/components/Logo/Logo.svelte
+++ b/web/src/lib/components/Logo/Logo.svelte
@@ -18,10 +18,11 @@
 -->
 
 <script lang="ts">
+  import { base } from '$app/paths';
   let className = '';
   export { className as class };
-  const lightLogo = '/iggy-apache-lightbg.svg';
-  const darkLogo = '/iggy-apache-darkbg.svg';
+  const lightLogo = `${base}/iggy-apache-lightbg.svg`;
+  const darkLogo = `${base}/iggy-apache-darkbg.svg`;
 </script>
 
 <img src={lightLogo} class="{className} block dark:hidden" alt="Apache Iggy" />
diff --git a/web/src/lib/components/Navbar.svelte 
b/web/src/lib/components/Navbar.svelte
index fd0c1c307..55524b703 100644
--- a/web/src/lib/components/Navbar.svelte
+++ b/web/src/lib/components/Navbar.svelte
@@ -21,42 +21,41 @@
   import Icon from './Icon.svelte';
   import type { iconType } from './Icon.svelte';
   import { page } from '$app/state';
+  import { resolve } from '$app/paths';
   import { twMerge } from 'tailwind-merge';
   import { tooltip } from '$lib/actions/tooltip';
   import { typedRoute } from '$lib/types/appRoutes';
   import LogoType from '$lib/components/Logo/LogoType.svelte';
   import LogoMark from '$lib/components/Logo/LogoMark.svelte';
-  import { resolve } from '$app/paths';
-
   let navItems = $derived([
     {
       name: 'Overview',
       icon: 'home',
-      href: typedRoute('/dashboard/overview'),
+      href: resolve(typedRoute('/dashboard/overview')),
       active: page.url.pathname.includes(typedRoute('/dashboard/overview'))
     },
     {
       name: 'Streams',
       icon: 'stream',
-      href: typedRoute('/dashboard/streams'),
+      href: resolve(typedRoute('/dashboard/streams')),
       active: page.url.pathname.includes(typedRoute('/dashboard/streams'))
     },
     // {
     //   name: 'Clients',
     //   icon: 'clients',
-    //   href: typedRoute('/dashboard/clients'),
+    //   href: resolve(typedRoute('/dashboard/clients')),
     //   active: page.url.pathname.includes(typedRoute('/dashboard/clients'))
     // },
     // {
     //   name: 'Logs',
     //   icon: 'logs',
-    //   href: typedRoute('/dashboard/logs'),
+    //   href: resolve(typedRoute('/dashboard/logs')),
     //   active: page.url.pathname.includes(typedRoute('/dashboard/logs'))
     // },
     {
       name: 'Settings',
       icon: 'settings',
-      href: typedRoute('/dashboard/settings/webUI'),
+      href: resolve(typedRoute('/dashboard/settings/webUI')),
       active: page.url.pathname.includes('/dashboard/settings')
     }
   ] satisfies { name: string; icon: iconType; href: string; active: boolean 
}[]);
@@ -78,7 +77,7 @@
       <li>
         <div use:tooltip={{ placement: 'right' }}>
           <a
-            href={resolve(href)}
+            {href}
             data-trigger
             class={twMerge(
               'p-2 block rounded-xl transition-colors  ring-2 
ring-transparent',
diff --git a/web/src/lib/types/appRoutes.ts b/web/src/lib/types/appRoutes.ts
index 17823b40a..2078467db 100644
--- a/web/src/lib/types/appRoutes.ts
+++ b/web/src/lib/types/appRoutes.ts
@@ -32,7 +32,11 @@ type DashboardRoutes = `/dashboard/${
 type AuthRoutes = `/auth/${'sign-in' | 'logout'}`;
 
 export const typedRoute = <const T extends DashboardRoutes | 
AuthRoutes>(route: T) => route;
-export const publicRoutes = [typedRoute('/auth/sign-in'), '/auth/test'] as 
const;
+export const publicRoutes = ['/auth/sign-in', '/auth/logout', '/auth/test'] as 
const;
 
-export const checkIfPathnameIsPublic = (pathname: string) =>
-  (publicRoutes as ReadonlyArray<string>).includes(pathname);
+export const checkIfPathnameIsPublic = (pathname: string) => {
+  const pathWithoutBase = pathname.replace(/^\/ui/, '') || '/';
+  return (publicRoutes as ReadonlyArray<string>).some(
+    (route) => pathWithoutBase === route || pathWithoutBase.startsWith(route + 
'/')
+  );
+};
diff --git a/web/src/routes/+layout.ts b/web/src/routes/+layout.ts
new file mode 100644
index 000000000..7a0657895
--- /dev/null
+++ b/web/src/routes/+layout.ts
@@ -0,0 +1,64 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { browser } from '$app/environment';
+import { goto } from '$app/navigation';
+import { base, resolve } from '$app/paths';
+import { authStore } from '$lib/auth/authStore.svelte';
+import { checkIfPathnameIsPublic, typedRoute } from '$lib/types/appRoutes';
+import type { LayoutLoad } from './$types';
+
+// Enable client-side rendering for SPA mode
+export const ssr = false;
+export const prerender = false;
+
+export const load: LayoutLoad = async ({ url }) => {
+  if (browser) {
+    authStore.initialize();
+  }
+
+  const pathname = url.pathname;
+  const isPublicPath = checkIfPathnameIsPublic(pathname);
+  const isAuthenticated = authStore.getAccessToken() !== null;
+
+  const authRedirects = [
+    base,
+    `${base}/`,
+    `${base}/dashboard`,
+    `${base}/dashboard/`,
+    `${base}/auth`,
+    `${base}/auth/sign-in`
+  ];
+
+  if (browser) {
+    if (!isAuthenticated && !isPublicPath) {
+      goto(resolve(typedRoute('/auth/sign-in')));
+      return { isAuthenticated: false };
+    }
+
+    if (isAuthenticated && authRedirects.includes(pathname)) {
+      goto(resolve(typedRoute('/dashboard/overview')));
+      return { isAuthenticated: true };
+    }
+  }
+
+  return {
+    isAuthenticated
+  };
+};
diff --git a/web/src/routes/api/proxy/+server.ts 
b/web/src/routes/api/proxy/+server.ts
deleted file mode 100644
index 70d091b0e..000000000
--- a/web/src/routes/api/proxy/+server.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { fetchIggyApi } from '$lib/api/fetchApi';
-import { handleFetchErrors } from '$lib/api/handleFetchErrors';
-import { error, json, type RequestHandler } from '@sveltejs/kit';
-import { convertBigIntsToStrings } from '$lib/api/convertBigIntsToStrings';
-
-export const POST: RequestHandler = async ({ request, cookies }) => {
-  const { path, body, method, queryParams } = await request.json();
-
-  if (!path || !method) {
-    const message = `routes/api/proxy/+server.ts no path or body or method 
provided`;
-    console.error(message);
-    error(500, {
-      message
-    });
-  }
-
-  const result = await fetchIggyApi({ body, path, method, cookies, queryParams 
});
-
-  const { data, response } = await handleFetchErrors(result, cookies);
-
-  const safeData = convertBigIntsToStrings(data);
-
-  return json({ data: safeData, ok: response.ok, status: response.status });
-};
diff --git a/web/src/lib/components/Logo/Logo.svelte 
b/web/src/routes/auth/logout/+page.svelte
similarity index 64%
copy from web/src/lib/components/Logo/Logo.svelte
copy to web/src/routes/auth/logout/+page.svelte
index dd561ef0a..6401055af 100644
--- a/web/src/lib/components/Logo/Logo.svelte
+++ b/web/src/routes/auth/logout/+page.svelte
@@ -18,11 +18,18 @@
 -->
 
 <script lang="ts">
-  let className = '';
-  export { className as class };
-  const lightLogo = '/iggy-apache-lightbg.svg';
-  const darkLogo = '/iggy-apache-darkbg.svg';
+  import { onMount } from 'svelte';
+  import { goto } from '$app/navigation';
+  import { resolve } from '$app/paths';
+  import { authStore } from '$lib/auth/authStore.svelte';
+  import { typedRoute } from '$lib/types/appRoutes';
+
+  onMount(() => {
+    authStore.logout();
+    goto(resolve(typedRoute('/auth/sign-in')));
+  });
 </script>
 
-<img src={lightLogo} class="{className} block dark:hidden" alt="Apache Iggy" />
-<img src={darkLogo} class="{className} hidden dark:block" alt="Apache Iggy" />
+<div class="flex items-center justify-center h-full">
+  <span class="text-color">Logging out...</span>
+</div>
diff --git a/web/src/routes/auth/logout/+page.server.ts 
b/web/src/routes/auth/logout/+page.ts
similarity index 67%
copy from web/src/routes/auth/logout/+page.server.ts
copy to web/src/routes/auth/logout/+page.ts
index e2d7ac8af..5de770f46 100644
--- a/web/src/routes/auth/logout/+page.server.ts
+++ b/web/src/routes/auth/logout/+page.ts
@@ -17,21 +17,18 @@
  * under the License.
  */
 
+import { browser } from '$app/environment';
+import { goto } from '$app/navigation';
+import { resolve } from '$app/paths';
+import { authStore } from '$lib/auth/authStore.svelte';
 import { typedRoute } from '$lib/types/appRoutes';
-import { tokens } from '$lib/utils/constants/tokens.js';
-import { redirect, type Actions } from '@sveltejs/kit';
+import type { PageLoad } from './$types';
 
-export const actions = {
-  default({ cookies }) {
-    // eat the cookie
-    cookies.set(tokens.accessToken, '', {
-      path: '/',
-      expires: new Date(0)
-    });
-
-    console.log('deleting cookie');
-
-    // redirect the user
-    redirect(302, typedRoute('/auth/sign-in'));
+export const load: PageLoad = async () => {
+  if (browser) {
+    authStore.logout();
+    goto(resolve(typedRoute('/auth/sign-in')));
   }
-} satisfies Actions;
+
+  return {};
+};
diff --git a/web/src/routes/auth/sign-in/+page.server.ts 
b/web/src/routes/auth/sign-in/+page.server.ts
deleted file mode 100644
index f840df92f..000000000
--- a/web/src/routes/auth/sign-in/+page.server.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { fail, redirect } from '@sveltejs/kit';
-import type { Actions } from './$types';
-import { message, superValidate } from 'sveltekit-superforms/server';
-import { zod4 } from 'sveltekit-superforms/adapters';
-import { z } from 'zod';
-import { typedRoute } from '$lib/types/appRoutes';
-import { fetchIggyApi } from '$lib/api/fetchApi';
-import { tokens } from '$lib/utils/constants/tokens';
-import { getJson } from '$lib/api/getJson';
-
-const schema = z.object({
-  username: z.string().min(1),
-  password: z.string().min(4)
-});
-
-type FormSchema = z.infer<typeof schema>;
-
-export const load = async () => {
-  const form = await superValidate(zod4(schema));
-
-  return { form };
-};
-
-export const actions = {
-  default: async ({ request, cookies }) => {
-    const form = await superValidate(request, zod4(schema));
-
-    if (!form.valid) {
-      return fail(400, { form });
-    }
-
-    const { password, username } = form.data;
-
-    const result = await fetchIggyApi({
-      method: 'POST',
-      path: '/users/login',
-      body: { username, password }
-    });
-
-    console.log(result);
-
-    if (!(result instanceof Response) || !result.ok) {
-      return message(form, 'Username or password is not valid', { status: 403 
});
-    }
-
-    const { access_token } = (await getJson(result)) as any;
-
-    cookies.set(tokens.accessToken, access_token.token, {
-      path: '/',
-      httpOnly: true,
-      sameSite: 'lax',
-      secure: true,
-      expires: new Date(1000 * access_token.expiry)
-    });
-
-    redirect(302, typedRoute('/dashboard/overview'));
-  }
-} satisfies Actions;
diff --git a/web/src/routes/auth/sign-in/+page.svelte 
b/web/src/routes/auth/sign-in/+page.svelte
index 0183cf15e..31e0aca1b 100644
--- a/web/src/routes/auth/sign-in/+page.svelte
+++ b/web/src/routes/auth/sign-in/+page.svelte
@@ -18,40 +18,87 @@
 -->
 
 <script lang="ts">
+  import { goto } from '$app/navigation';
+  import { resolve } from '$app/paths';
+  import { env } from '$env/dynamic/public';
+  import { authStore } from '$lib/auth/authStore.svelte';
   import Button from '$lib/components/Button.svelte';
   import Checkbox from '$lib/components/Checkbox.svelte';
   import Icon from '$lib/components/Icon.svelte';
   import Input from '$lib/components/Input.svelte';
   import PasswordInput from '$lib/components/PasswordInput.svelte';
+  import { typedRoute } from '$lib/types/appRoutes';
   import { persistedStore } from '$lib/utils/persistedStore.js';
   import { onMount } from 'svelte';
-  import { superForm } from 'sveltekit-superforms/client';
 
-  interface Props {
-    data: any;
-  }
-
-  let { data }: Props = $props();
-  const { form, constraints, errors, message } = superForm(data.form, {});
+  let username = $state('');
+  let password = $state('');
+  let errorMessage = $state('');
+  let isLoading = $state(false);
+  let usernameError = $state('');
+  let passwordError = $state('');
 
   const remember = persistedStore('rememberMe', { rememberMe: true, username: 
'' });
 
   onMount(() => {
     if ($remember.rememberMe) {
-      $form.username = $remember.username;
+      username = $remember.username;
     }
   });
-</script>
 
-<form
-  method="POST"
-  onsubmit={() => {
+  async function handleSubmit(event: SubmitEvent) {
+    event.preventDefault();
+    errorMessage = '';
+    usernameError = '';
+    passwordError = '';
+
+    // Validate
+    if (!username) {
+      usernameError = 'Username is required';
+      return;
+    }
+    if (!password || password.length < 4) {
+      passwordError = 'Password must be at least 4 characters';
+      return;
+    }
+
+    // Remember username if checked
     if ($remember.rememberMe) {
-      $remember.username = $form.username;
+      $remember.username = username;
     } else {
       $remember.username = '';
     }
-  }}
+
+    isLoading = true;
+
+    try {
+      const baseUrl = env.PUBLIC_IGGY_API_URL || '';
+      const response = await fetch(`${baseUrl}/users/login`, {
+        method: 'POST',
+        headers: { 'Content-Type': 'application/json' },
+        body: JSON.stringify({ username, password })
+      });
+
+      if (!response.ok) {
+        errorMessage = 'Username or password is not valid';
+        isLoading = false;
+        return;
+      }
+
+      const data = await response.json();
+      const { access_token } = data;
+
+      authStore.login(access_token.token, access_token.expiry);
+      goto(resolve(typedRoute('/dashboard/overview')));
+    } catch (e) {
+      errorMessage = 'Failed to connect to server';
+      isLoading = false;
+    }
+  }
+</script>
+
+<form
+  onsubmit={handleSubmit}
   class="min-w-[350px] max-w-[400px] bg-white dark:bg-shade-d700 border 
text-color p-5 rounded-2xl card-shadow dark:shadow-lg flex flex-col gap-5"
 >
   <span class="mx-auto font-semibold">Admin sign in</span>
@@ -60,26 +107,23 @@
     label="Username"
     name="username"
     autocomplete="username"
-    errorMessage={Array.isArray($errors?.username)
-      ? $errors.username.join(',')
-      : String($errors?.username || '')}
-    bind:value={$form.username}
-    {...$constraints.username}
+    errorMessage={usernameError}
+    bind:value={username}
+    required
   />
 
   <PasswordInput
     label="Password"
     name="password"
     autocomplete="current-password"
-    errorMessage={Array.isArray($errors?.password)
-      ? $errors.password.join(',')
-      : String($errors?.password || '')}
-    bind:value={$form.password}
-    {...$constraints.password}
+    errorMessage={passwordError}
+    bind:value={password}
+    required
+    minlength={4}
   />
 
-  {#if $message}
-    <span class="text-sm mx-auto text-red">{$message}</span>
+  {#if errorMessage}
+    <span class="text-sm mx-auto text-red-500">{errorMessage}</span>
   {/if}
 
   <div class="flex justify-between items-center">
@@ -89,8 +133,8 @@
     </label>
   </div>
 
-  <Button variant="contained" size="lg" class="mt-7" type="submit">
-    <span> Login </span>
+  <Button variant="contained" size="lg" class="mt-7" type="submit" 
disabled={isLoading}>
+    <span>{isLoading ? 'Logging in...' : 'Login'}</span>
     <Icon name="login" class="w-[23px] h-[23px]" />
   </Button>
 
diff --git a/web/src/routes/auth/logout/+page.server.ts 
b/web/src/routes/auth/sign-in/+page.ts
similarity index 67%
rename from web/src/routes/auth/logout/+page.server.ts
rename to web/src/routes/auth/sign-in/+page.ts
index e2d7ac8af..5ca57c99b 100644
--- a/web/src/routes/auth/logout/+page.server.ts
+++ b/web/src/routes/auth/sign-in/+page.ts
@@ -17,21 +17,17 @@
  * under the License.
  */
 
+import { browser } from '$app/environment';
+import { goto } from '$app/navigation';
+import { resolve } from '$app/paths';
+import { authStore } from '$lib/auth/authStore.svelte';
 import { typedRoute } from '$lib/types/appRoutes';
-import { tokens } from '$lib/utils/constants/tokens.js';
-import { redirect, type Actions } from '@sveltejs/kit';
+import type { PageLoad } from './$types';
 
-export const actions = {
-  default({ cookies }) {
-    // eat the cookie
-    cookies.set(tokens.accessToken, '', {
-      path: '/',
-      expires: new Date(0)
-    });
-
-    console.log('deleting cookie');
-
-    // redirect the user
-    redirect(302, typedRoute('/auth/sign-in'));
+export const load: PageLoad = async () => {
+  if (browser && authStore.getAccessToken()) {
+    goto(resolve(typedRoute('/dashboard/overview')));
   }
-} satisfies Actions;
+
+  return {};
+};
diff --git a/web/src/routes/dashboard/+layout.server.ts 
b/web/src/routes/dashboard/+layout.ts
similarity index 50%
rename from web/src/routes/dashboard/+layout.server.ts
rename to web/src/routes/dashboard/+layout.ts
index 5d0c105f2..30b80d565 100644
--- a/web/src/routes/dashboard/+layout.server.ts
+++ b/web/src/routes/dashboard/+layout.ts
@@ -17,22 +17,47 @@
  * under the License.
  */
 
-import { fetchIggyApi } from '$lib/api/fetchApi';
-import { handleFetchErrors } from '$lib/api/handleFetchErrors';
+import { browser } from '$app/environment';
+import { goto } from '$app/navigation';
+import { resolve } from '$app/paths';
+import { clientApi } from '$lib/api/clientApi';
+import { authStore } from '$lib/auth/authStore.svelte';
 import { userDetailsMapper } from '$lib/domain/UserDetails';
-import type { LayoutServerLoad } from './$types';
+import { typedRoute } from '$lib/types/appRoutes';
 import { jwtDecode } from 'jwt-decode';
+import type { LayoutLoad } from './$types';
+
+export const load: LayoutLoad = async () => {
+  if (browser) {
+    authStore.initialize();
+  }
+
+  const token = authStore.getAccessToken();
+  if (browser && !token) {
+    goto(resolve(typedRoute('/auth/sign-in')));
+    return { user: null };
+  }
 
-export const load: LayoutServerLoad = async ({ cookies }) => {
   const getDetailedUser = async () => {
-    //always available here, auth hook prevents rendering this page without 
access_token
-    const accessToken = cookies.get('access_token')!;
-    const userId = jwtDecode(accessToken).sub!;
+    let userId = authStore.userId;
+
+    if (!userId && token) {
+      try {
+        const decoded = jwtDecode(token);
+        userId = decoded.sub ? parseInt(decoded.sub, 10) : null;
+      } catch {
+        return null;
+      }
+    }
 
-    const userResult = await fetchIggyApi({ method: 'GET', path: 
`/users/${+userId}`, cookies });
-    const { data } = await handleFetchErrors(userResult, cookies);
+    if (!userId) return null;
 
-    return userDetailsMapper(data);
+    try {
+      const data = await clientApi({ method: 'GET', path: `/users/${userId}` 
});
+      return userDetailsMapper(data);
+    } catch {
+      return null;
+    }
   };
 
   return {
diff --git a/web/src/routes/dashboard/overview/+page.server.ts 
b/web/src/routes/dashboard/overview/+page.ts
similarity index 76%
copy from web/src/routes/dashboard/overview/+page.server.ts
copy to web/src/routes/dashboard/overview/+page.ts
index 3eac60a15..5f88bb588 100644
--- a/web/src/routes/dashboard/overview/+page.server.ts
+++ b/web/src/routes/dashboard/overview/+page.ts
@@ -17,19 +17,16 @@
  * under the License.
  */
 
-import { fetchIggyApi } from '$lib/api/fetchApi';
-import { handleFetchErrors } from '$lib/api/handleFetchErrors';
+import { clientApi } from '$lib/api/clientApi';
 import { statsMapper } from '$lib/domain/Stats';
+import type { PageLoad } from './$types';
 
-export const load = async ({ cookies }) => {
-  const result = await fetchIggyApi({
+export const load: PageLoad = async () => {
+  const data = await clientApi({
     path: '/stats',
-    method: 'GET',
-    cookies
+    method: 'GET'
   });
 
-  const { data } = await handleFetchErrors(result, cookies);
-
   return {
     stats: statsMapper(data)
   };
diff --git a/web/src/routes/dashboard/settings/server/+page.server.ts 
b/web/src/routes/dashboard/settings/server/+page.server.ts
deleted file mode 100644
index bff846f2c..000000000
--- a/web/src/routes/dashboard/settings/server/+page.server.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { fetchIggyApi } from '$lib/api/fetchApi';
-import { handleFetchErrors } from '$lib/api/handleFetchErrors';
-import { statsMapper } from '$lib/domain/Stats';
-
-export const load = async ({ cookies }) => {
-  const getStats = async () => {
-    const result = await fetchIggyApi({
-      method: 'GET',
-      path: '/stats',
-      cookies
-    });
-
-    const { data } = await handleFetchErrors(result, cookies);
-    return statsMapper(data);
-  };
-
-  return {
-    serverStats: await getStats()
-  };
-};
diff --git a/web/src/routes/+layout.server.ts 
b/web/src/routes/dashboard/settings/server/+page.ts
similarity index 73%
rename from web/src/routes/+layout.server.ts
rename to web/src/routes/dashboard/settings/server/+page.ts
index 27bb9c06f..5bf4759bc 100644
--- a/web/src/routes/+layout.server.ts
+++ b/web/src/routes/dashboard/settings/server/+page.ts
@@ -17,10 +17,17 @@
  * under the License.
  */
 
-import type { LayoutServerLoad } from './$types';
+import { clientApi } from '$lib/api/clientApi';
+import { statsMapper } from '$lib/domain/Stats';
+import type { PageLoad } from './$types';
+
+export const load: PageLoad = async () => {
+  const data = await clientApi({
+    method: 'GET',
+    path: '/stats'
+  });
 
-export const load: LayoutServerLoad = async ({ locals }) => {
   return {
-    user: locals.user
+    serverStats: statsMapper(data)
   };
 };
diff --git a/web/src/routes/dashboard/settings/users/+page.server.ts 
b/web/src/routes/dashboard/settings/users/+page.ts
similarity index 63%
rename from web/src/routes/dashboard/settings/users/+page.server.ts
rename to web/src/routes/dashboard/settings/users/+page.ts
index 2f2344585..73fc12615 100644
--- a/web/src/routes/dashboard/settings/users/+page.server.ts
+++ b/web/src/routes/dashboard/settings/users/+page.ts
@@ -17,33 +17,28 @@
  * under the License.
  */
 
-import { fetchIggyApi } from '$lib/api/fetchApi';
-import { handleFetchErrors } from '$lib/api/handleFetchErrors';
+import { clientApi } from '$lib/api/clientApi';
 import { streamListMapper } from '$lib/domain/Stream';
-import { streamDetailsMapper } from '$lib/domain/StreamDetails.js';
+import { streamDetailsMapper } from '$lib/domain/StreamDetails';
+import { userMapper, type User } from '$lib/domain/User';
+import type { PageLoad } from './$types';
 
-import { userMapper, type User } from '$lib/domain/User.js';
-
-export const load = async ({ cookies }) => {
+export const load: PageLoad = async () => {
   const getUsers = async () => {
-    const result = await fetchIggyApi({
+    const data = await clientApi<any[]>({
       method: 'GET',
-      path: '/users',
-      cookies
+      path: '/users'
     });
 
-    const { data } = await handleFetchErrors(result, cookies);
-    return (data as any).map((item: any) => userMapper(item)) as User[];
+    return data.map((item: any) => userMapper(item)) as User[];
   };
 
   const getStreams = async () => {
-    const result = await fetchIggyApi({
+    const data = await clientApi<any[]>({
       method: 'GET',
-      path: '/streams',
-      cookies
+      path: '/streams'
     });
 
-    const { data } = await handleFetchErrors(result, cookies);
     const streams = streamListMapper(data);
 
     if (streams.length === 0) {
@@ -53,12 +48,10 @@ export const load = async ({ cookies }) => {
       };
     }
 
-    const streamDetailResult = await fetchIggyApi({
+    const streamDetailsData = await clientApi({
       method: 'GET',
-      path: `/streams/${streams[0].id}`,
-      cookies
+      path: `/streams/${streams[0].id}`
     });
-    const { data: streamDetailsData } = await 
handleFetchErrors(streamDetailResult, cookies);
 
     return {
       streams,
diff --git a/web/src/routes/dashboard/streams/+layout.server.ts 
b/web/src/routes/dashboard/streams/+layout.server.ts
deleted file mode 100644
index 0e0650ea2..000000000
--- a/web/src/routes/dashboard/streams/+layout.server.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { fetchIggyApi } from '$lib/api/fetchApi';
-import { handleFetchErrors } from '$lib/api/handleFetchErrors';
-import { streamMapper, type Stream } from '$lib/domain/Stream';
-import type { LayoutServerLoad } from './$types';
-
-export const load: LayoutServerLoad = async ({ cookies }) => {
-  const result = await fetchIggyApi({
-    method: 'GET',
-    path: '/streams',
-    cookies
-  });
-
-  const { data } = await handleFetchErrors(result, cookies);
-
-  return {
-    streams: ((data as any).map(streamMapper) as Stream[]).sort((a, b) => 
b.createdAt - a.createdAt)
-  };
-};
diff --git a/web/src/routes/dashboard/streams/+layout.svelte 
b/web/src/routes/dashboard/streams/+layout.svelte
index bc66222b4..99b8d91d0 100644
--- a/web/src/routes/dashboard/streams/+layout.svelte
+++ b/web/src/routes/dashboard/streams/+layout.svelte
@@ -41,7 +41,10 @@
   let filteredData = $derived(data.streams.filter((stream) => 
stream.name.includes(searchQuery)));
 
   onMount(() => {
-    if (data.streams.length > 0 && page.url.pathname === 
typedRoute('/dashboard/streams')) {
+    if (
+      data.streams.length > 0 &&
+      page.url.pathname === resolve(typedRoute('/dashboard/streams'))
+    ) {
       goto(resolve(typedRoute(`/dashboard/streams/${data.streams[0].id}`)));
     }
   });
diff --git a/web/src/routes/dashboard/overview/+page.server.ts 
b/web/src/routes/dashboard/streams/+layout.ts
similarity index 69%
copy from web/src/routes/dashboard/overview/+page.server.ts
copy to web/src/routes/dashboard/streams/+layout.ts
index 3eac60a15..711b9977c 100644
--- a/web/src/routes/dashboard/overview/+page.server.ts
+++ b/web/src/routes/dashboard/streams/+layout.ts
@@ -17,20 +17,17 @@
  * under the License.
  */
 
-import { fetchIggyApi } from '$lib/api/fetchApi';
-import { handleFetchErrors } from '$lib/api/handleFetchErrors';
-import { statsMapper } from '$lib/domain/Stats';
+import { clientApi } from '$lib/api/clientApi';
+import { streamMapper, type Stream } from '$lib/domain/Stream';
+import type { LayoutLoad } from './$types';
 
-export const load = async ({ cookies }) => {
-  const result = await fetchIggyApi({
-    path: '/stats',
+export const load: LayoutLoad = async () => {
+  const data = await clientApi<any[]>({
     method: 'GET',
-    cookies
+    path: '/streams'
   });
 
-  const { data } = await handleFetchErrors(result, cookies);
-
   return {
-    stats: statsMapper(data)
+    streams: (data.map(streamMapper) as Stream[]).sort((a, b) => b.createdAt - 
a.createdAt)
   };
 };
diff --git a/web/src/routes/dashboard/streams/[streamId=i32]/+page.server.ts 
b/web/src/routes/dashboard/streams/[streamId=i32]/+page.server.ts
deleted file mode 100644
index 3b22b2cbf..000000000
--- a/web/src/routes/dashboard/streams/[streamId=i32]/+page.server.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { fetchIggyApi } from '$lib/api/fetchApi.js';
-import { handleFetchErrors } from '$lib/api/handleFetchErrors';
-import { streamDetailsMapper } from '$lib/domain/StreamDetails';
-
-export const load = async ({ params, cookies }) => {
-  const getStreamDetails = async () => {
-    const result = await fetchIggyApi({
-      method: 'GET',
-      path: `/streams/${+params.streamId}`,
-      cookies
-    });
-
-    const { data } = await handleFetchErrors(result, cookies);
-
-    return streamDetailsMapper(data);
-  };
-
-  return {
-    streamDetails: await getStreamDetails()
-  };
-};
diff --git a/web/src/routes/dashboard/streams/[streamId=i32]/+page.svelte 
b/web/src/routes/dashboard/streams/[streamId=i32]/+page.svelte
index ee2da4431..a4869cea5 100644
--- a/web/src/routes/dashboard/streams/[streamId=i32]/+page.svelte
+++ b/web/src/routes/dashboard/streams/[streamId=i32]/+page.svelte
@@ -19,6 +19,7 @@
 
 <script lang="ts">
   import { page } from '$app/state';
+  import { resolve } from '$app/paths';
   import Button from '$lib/components/Button.svelte';
   import Icon from '$lib/components/Icon.svelte';
   import { openModal } from '$lib/components/Modals/AppModals.svelte';
@@ -85,7 +86,7 @@
   rowClass="grid grid-cols-[150px_3fr_2fr_2fr_2fr_2fr_3fr]"
   data={stream.topics}
   hrefBuilder={(topic) =>
-    typedRoute(`/dashboard/streams/${+(page.params.streamId || 
'')}/topics/${topic.id}`)}
+    resolve(typedRoute(`/dashboard/streams/${+(page.params.streamId || 
'')}/topics/${topic.id}`))}
   colNames={{
     id: 'ID',
     name: 'Name',
diff --git a/web/src/hooks.client.ts 
b/web/src/routes/dashboard/streams/[streamId=i32]/+page.ts
similarity index 70%
copy from web/src/hooks.client.ts
copy to web/src/routes/dashboard/streams/[streamId=i32]/+page.ts
index d3f825db2..8ac07677d 100644
--- a/web/src/hooks.client.ts
+++ b/web/src/routes/dashboard/streams/[streamId=i32]/+page.ts
@@ -17,12 +17,17 @@
  * under the License.
  */
 
-import type { HandleClientError } from '@sveltejs/kit';
+import { clientApi } from '$lib/api/clientApi';
+import { streamDetailsMapper } from '$lib/domain/StreamDetails';
+import type { PageLoad } from './$types';
+
+export const load: PageLoad = async ({ params }) => {
+  const data = await clientApi({
+    method: 'GET',
+    path: `/streams/${+params.streamId}`
+  });
 
-export const handleError: HandleClientError = async ({ error }) => {
-  console.log('client error handler', error);
   return {
-    message: 'Whoops!',
-    errorId: 1
+    streamDetails: streamDetailsMapper(data)
   };
 };
diff --git 
a/web/src/routes/dashboard/streams/[streamId=i32]/topics/[topicId=i32]/+page.server.ts
 
b/web/src/routes/dashboard/streams/[streamId=i32]/topics/[topicId=i32]/+page.server.ts
deleted file mode 100644
index 5b0c49451..000000000
--- 
a/web/src/routes/dashboard/streams/[streamId=i32]/topics/[topicId=i32]/+page.server.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { fetchIggyApi } from '$lib/api/fetchApi';
-import { handleFetchErrors } from '$lib/api/handleFetchErrors';
-import { topicDetailsMapper } from '$lib/domain/TopicDetails';
-
-export const load = async ({ params, cookies }) => {
-  const getTopic = async () => {
-    const result = await fetchIggyApi({
-      method: 'GET',
-      path: `/streams/${+params.streamId}/topics/${+params.topicId}`,
-      cookies
-    });
-
-    const { data } = await handleFetchErrors(result, cookies);
-
-    return topicDetailsMapper(data);
-  };
-
-  return {
-    topic: await getTopic()
-  };
-};
diff --git 
a/web/src/routes/dashboard/streams/[streamId=i32]/topics/[topicId=i32]/+page.svelte
 
b/web/src/routes/dashboard/streams/[streamId=i32]/topics/[topicId=i32]/+page.svelte
index 6cb4c4673..0a4601ede 100644
--- 
a/web/src/routes/dashboard/streams/[streamId=i32]/topics/[topicId=i32]/+page.svelte
+++ 
b/web/src/routes/dashboard/streams/[streamId=i32]/topics/[topicId=i32]/+page.svelte
@@ -92,8 +92,10 @@
   rowClass="grid grid-cols-[150px_1fr_1fr_1fr_1fr_1fr]"
   data={topic.partitions}
   hrefBuilder={(partition) =>
-    typedRoute(
-      `/dashboard/streams/${+(page.params.streamId || 
'')}/topics/${topic.id}/partitions/${partition.id}/messages`
+    resolve(
+      typedRoute(
+        `/dashboard/streams/${+(page.params.streamId || 
'')}/topics/${topic.id}/partitions/${partition.id}/messages`
+      )
     )}
   colNames={{
     id: 'ID',
diff --git a/web/src/routes/dashboard/overview/+page.server.ts 
b/web/src/routes/dashboard/streams/[streamId=i32]/topics/[topicId=i32]/+page.ts
similarity index 69%
rename from web/src/routes/dashboard/overview/+page.server.ts
rename to 
web/src/routes/dashboard/streams/[streamId=i32]/topics/[topicId=i32]/+page.ts
index 3eac60a15..78626a88a 100644
--- a/web/src/routes/dashboard/overview/+page.server.ts
+++ 
b/web/src/routes/dashboard/streams/[streamId=i32]/topics/[topicId=i32]/+page.ts
@@ -17,20 +17,17 @@
  * under the License.
  */
 
-import { fetchIggyApi } from '$lib/api/fetchApi';
-import { handleFetchErrors } from '$lib/api/handleFetchErrors';
-import { statsMapper } from '$lib/domain/Stats';
+import { clientApi } from '$lib/api/clientApi';
+import { topicDetailsMapper } from '$lib/domain/TopicDetails';
+import type { PageLoad } from './$types';
 
-export const load = async ({ cookies }) => {
-  const result = await fetchIggyApi({
-    path: '/stats',
+export const load: PageLoad = async ({ params }) => {
+  const data = await clientApi({
     method: 'GET',
-    cookies
+    path: `/streams/${+params.streamId}/topics/${+params.topicId}`
   });
 
-  const { data } = await handleFetchErrors(result, cookies);
-
   return {
-    stats: statsMapper(data)
+    topic: topicDetailsMapper(data)
   };
 };
diff --git 
a/web/src/routes/dashboard/streams/[streamId=i32]/topics/[topicId=i32]/partitions/[partitionId=i32]/messages/+page.server.ts
 
b/web/src/routes/dashboard/streams/[streamId=i32]/topics/[topicId=i32]/partitions/[partitionId=i32]/messages/+page.ts
similarity index 79%
rename from 
web/src/routes/dashboard/streams/[streamId=i32]/topics/[topicId=i32]/partitions/[partitionId=i32]/messages/+page.server.ts
rename to 
web/src/routes/dashboard/streams/[streamId=i32]/topics/[topicId=i32]/partitions/[partitionId=i32]/messages/+page.ts
index 7bf5ba315..b5bdcec87 100644
--- 
a/web/src/routes/dashboard/streams/[streamId=i32]/topics/[topicId=i32]/partitions/[partitionId=i32]/messages/+page.server.ts
+++ 
b/web/src/routes/dashboard/streams/[streamId=i32]/topics/[topicId=i32]/partitions/[partitionId=i32]/messages/+page.ts
@@ -17,22 +17,21 @@
  * under the License.
  */
 
-import { fetchIggyApi } from '$lib/api/fetchApi';
-import { handleFetchErrors } from '$lib/api/handleFetchErrors';
+import { clientApi } from '$lib/api/clientApi';
 import { partitionMessagesDetailsMapper } from '$lib/domain/MessageDetails';
 import { topicDetailsMapper } from '$lib/domain/TopicDetails';
+import type { PageLoad } from './$types';
 
 const MESSAGES_PER_PAGE = 20;
 
-export const load = async ({ params, cookies, url }) => {
-  const offset = url.searchParams.get('offset') || '0';
+export const load: PageLoad = async ({ params, url }) => {
   const direction = url.searchParams.get('direction') || 'desc';
 
   const getPartitionMessages = async () => {
-    const initialResult = await fetchIggyApi({
+    // First, get the initial offset to determine total messages
+    const initialData = await clientApi<any>({
       method: 'GET',
       path: `/streams/${+params.streamId}/topics/${+params.topicId}/messages`,
-      cookies,
       queryParams: {
         kind: 'offset',
         value: '0',
@@ -42,18 +41,15 @@ export const load = async ({ params, cookies, url }) => {
       }
     });
 
-    const { data: initialData } = await handleFetchErrors(initialResult, 
cookies);
-    const initialMessages = partitionMessagesDetailsMapper(initialData as any);
-
+    const initialMessages = partitionMessagesDetailsMapper(initialData);
     const totalMessages = initialMessages.currentOffset + 1;
     const offset =
       url.searchParams.get('offset') ??
       (direction === 'desc' ? Math.max(0, totalMessages - 
MESSAGES_PER_PAGE).toString() : '0');
 
-    const result = await fetchIggyApi({
+    const data = await clientApi<any>({
       method: 'GET',
       path: `/streams/${+params.streamId}/topics/${+params.topicId}/messages`,
-      cookies,
       queryParams: {
         kind: 'offset',
         value: offset.toString(),
@@ -63,23 +59,22 @@ export const load = async ({ params, cookies, url }) => {
       }
     });
 
-    const { data } = await handleFetchErrors(result, cookies);
-    return partitionMessagesDetailsMapper(data as any);
+    return partitionMessagesDetailsMapper(data);
   };
 
   const getTopic = async () => {
-    const result = await fetchIggyApi({
+    const data = await clientApi({
       method: 'GET',
-      path: `/streams/${+params.streamId}/topics/${+params.topicId}`,
-      cookies
+      path: `/streams/${+params.streamId}/topics/${+params.topicId}`
     });
 
-    const { data } = await handleFetchErrors(result, cookies);
     return topicDetailsMapper(data);
   };
 
   const [partitionMessages, topic] = await 
Promise.all([getPartitionMessages(), getTopic()]);
 
+  const offset = url.searchParams.get('offset') || '0';
+
   return {
     partitionMessages,
     topic,
diff --git a/web/svelte.config.js b/web/svelte.config.js
index 34cdf47c8..0685bbe46 100644
--- a/web/svelte.config.js
+++ b/web/svelte.config.js
@@ -17,15 +17,28 @@
  * under the License.
  */
 
-import adapter from '@sveltejs/adapter-node';
+import adapterNode from '@sveltejs/adapter-node';
+import adapterStatic from '@sveltejs/adapter-static';
 import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
 
+// Use static adapter when STATIC_BUILD env is set (for embedding in Rust 
server)
+const useStaticAdapter = process.env.STATIC_BUILD === 'true';
+
 /** @type {import('@sveltejs/kit').Config} */
 const config = {
   kit: {
-    adapter: adapter({
-      out: 'build'
-    }),
+    adapter: useStaticAdapter
+      ? adapterStatic({
+          pages: 'build/static',
+          assets: 'build/static',
+          fallback: 'index.html'
+        })
+      : adapterNode({
+          out: 'build'
+        }),
+    paths: {
+      base: useStaticAdapter ? '/ui' : ''
+    },
     csrf: {
       trustedOrigins: ['*']
     }

Reply via email to