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

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

commit e54f0bc69155e3cd8abab2ab2cbc7c1350895ef6
Author: spetz <[email protected]>
AuthorDate: Fri Dec 12 20:54:21 2025 +0100

    feat(server,web): embed Web UI into server behind iggy-web feature
---
 Cargo.lock                                         | 258 +++++++++------------
 Cargo.toml                                         |   2 +-
 DEPENDENCIES.md                                    |  38 ++-
 bdd/rust/Cargo.toml                                |   2 +-
 core/bench/dashboard/server/Cargo.toml             |   2 +-
 core/configs/server.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                        |  82 +++++++
 web/package-lock.json                              |  11 +
 web/package.json                                   |   2 +
 web/src/hooks.client.ts                            |  11 +-
 web/src/hooks.server.ts                            |  73 ------
 web/src/lib/api/clientApi.ts                       | 106 +++++++++
 web/src/lib/api/fetchRouteApi.ts                   |  36 ++-
 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 +-
 .../streams/[streamId=i32]/+page.ts}               |  13 +-
 .../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 +-
 48 files changed, 844 insertions(+), 712 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 8e8525388..a03e0847e 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]]
@@ -1050,7 +1050,7 @@ dependencies = [
  "charming",
  "colored",
  "derive-new",
- "derive_more 2.1.0",
+ "derive_more",
  "human-repr",
  "rand 0.9.2",
  "serde",
@@ -1619,7 +1619,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",
@@ -1930,15 +1930,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 +2242,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 +2269,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 +2285,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_locate",
  "regex",
- "regex-syntax 0.7.5",
+ "regex-syntax",
 ]
 
 [[package]]
@@ -2584,17 +2582,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"
@@ -2665,7 +2652,7 @@ dependencies = [
  "libc",
  "option-ext",
  "redox_users",
- "windows-sys 0.61.2",
+ "windows-sys 0.59.0",
 ]
 
 [[package]]
@@ -2761,12 +2748,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"
@@ -2972,7 +2953,7 @@ source = 
"registry+https://github.com/rust-lang/crates.io-index";
 checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
 dependencies = [
  "libc",
- "windows-sys 0.61.2",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
@@ -3154,9 +3135,9 @@ dependencies = [
 
 [[package]]
 name = "file-operation"
-version = "0.8.6"
+version = "0.8.7"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "e820507a8045dbe4861c925a91ca6989cce6c7c09b4c3d90c4941df84ffc0aeb"
+checksum = "be50c359f22a9fc2e61d5c3bf78a097a449d5c03b5d0f0b0c7a2a89c870b3cd3"
 dependencies = [
  "tokio",
 ]
@@ -3491,19 +3472,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 +3510,7 @@ dependencies = [
  "bstr",
  "log",
  "regex-automata",
- "regex-syntax 0.8.8",
+ "regex-syntax",
 ]
 
 [[package]]
@@ -3912,12 +3893,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"
@@ -4575,7 +4550,7 @@ dependencies = [
  "compio-tls",
  "compio-ws",
  "crossbeam",
- "derive_more 2.1.0",
+ "derive_more",
  "err_trail",
  "fast-async-mutex",
  "figment",
@@ -4905,7 +4880,7 @@ dependencies = [
  "chrono",
  "compio",
  "ctor",
- "derive_more 2.1.0",
+ "derive_more",
  "env_logger",
  "futures",
  "humantime",
@@ -5152,29 +5127,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"
@@ -5478,7 +5430,7 @@ dependencies = [
  "lazy_static",
  "proc-macro2",
  "quote",
- "regex-syntax 0.8.8",
+ "regex-syntax",
  "rustc_version",
  "syn 2.0.111",
 ]
@@ -5629,12 +5581,6 @@ dependencies = [
  "unicase",
 ]
 
-[[package]]
-name = "minimal-lexical"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
-
 [[package]]
 name = "miniz_oxide"
 version = "0.8.9"
@@ -5730,19 +5676,18 @@ dependencies = [
 
 [[package]]
 name = "nom"
-version = "7.1.3"
+version = "8.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
 dependencies = [
  "memchr",
- "minimal-lexical",
 ]
 
 [[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",
@@ -5827,7 +5772,7 @@ version = "0.50.3"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
 dependencies = [
- "windows-sys 0.61.2",
+ "windows-sys 0.59.0",
 ]
 
 [[package]]
@@ -6272,7 +6217,7 @@ source = 
"registry+https://github.com/rust-lang/crates.io-index";
 checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
 dependencies = [
  "libc",
- "windows-sys 0.61.2",
+ "windows-sys 0.45.0",
 ]
 
 [[package]]
@@ -6381,7 +6326,7 @@ checksum = 
"914a1c2265c98e2446911282c6ac86d8524f495792c38c5bd884f80499c7538a"
 dependencies = [
  "parse-display-derive",
  "regex",
- "regex-syntax 0.8.8",
+ "regex-syntax",
 ]
 
 [[package]]
@@ -6393,7 +6338,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "regex",
- "regex-syntax 0.8.8",
+ "regex-syntax",
  "structmeta",
  "syn 2.0.111",
 ]
@@ -6873,7 +6818,7 @@ source = 
"registry+https://github.com/rust-lang/crates.io-index";
 checksum = "9120690fafc389a67ba3803df527d0ec9cbbc9cc45e4cc20b332996dfb672425"
 dependencies = [
  "anyhow",
- "itertools 0.14.0",
+ "itertools 0.13.0",
  "proc-macro2",
  "quote",
  "syn 2.0.111",
@@ -7042,7 +6987,7 @@ dependencies = [
  "once_cell",
  "socket2 0.6.1",
  "tracing",
- "windows-sys 0.60.2",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
@@ -7225,7 +7170,7 @@ dependencies = [
  "aho-corasick",
  "memchr",
  "regex-automata",
- "regex-syntax 0.8.8",
+ "regex-syntax",
 ]
 
 [[package]]
@@ -7236,7 +7181,7 @@ checksum = 
"5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
 dependencies = [
  "aho-corasick",
  "memchr",
- "regex-syntax 0.8.8",
+ "regex-syntax",
 ]
 
 [[package]]
@@ -7245,12 +7190,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 +7483,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"
@@ -7595,7 +7568,7 @@ dependencies = [
  "errno",
  "libc",
  "linux-raw-sys",
- "windows-sys 0.61.2",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
@@ -7663,7 +7636,7 @@ dependencies = [
  "security-framework",
  "security-framework-sys",
  "webpki-root-certs",
- "windows-sys 0.61.2",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
@@ -7787,11 +7760,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",
@@ -7850,7 +7822,7 @@ source = 
"registry+https://github.com/rust-lang/crates.io-index";
 checksum = "5b55fb86dfd3a2f5f76ea78310a88f96c4ea21a3031f8d212443d56123fd0521"
 dependencies = [
  "libc",
- "windows-sys 0.61.2",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
@@ -8100,7 +8072,7 @@ dependencies = [
  "cyper",
  "cyper-axum",
  "dashmap",
- "derive_more 2.1.0",
+ "derive_more",
  "dotenvy",
  "enum_dispatch",
  "err_trail",
@@ -8115,6 +8087,7 @@ dependencies = [
  "jsonwebtoken",
  "lending-iterator",
  "mimalloc",
+ "mime_guess",
  "moka",
  "nix",
  "opentelemetry",
@@ -8128,6 +8101,7 @@ dependencies = [
  "reqwest",
  "ring",
  "ringbuffer",
+ "rust-embed",
  "rustls",
  "rustls-pemfile",
  "send_wrapper",
@@ -8301,7 +8275,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 +8407,7 @@ checksum = 
"19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b"
 dependencies = [
  "dotenvy",
  "either",
- "heck 0.5.0",
+ "heck",
  "hex",
  "once_cell",
  "proc-macro2",
@@ -8653,7 +8627,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 +8640,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 +8696,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 +8707,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 +8717,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",
@@ -8802,7 +8776,7 @@ dependencies = [
  "getrandom 0.3.4",
  "once_cell",
  "rustix",
- "windows-sys 0.61.2",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
@@ -9485,15 +9459,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 +9478,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 +9508,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"
@@ -10062,7 +10036,7 @@ version = "0.1.11"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
 dependencies = [
- "windows-sys 0.61.2",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
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..0e99b3c04 100644
--- a/DEPENDENCIES.md
+++ b/DEPENDENCIES.md
@@ -162,7 +162,7 @@ compression-codecs: 0.4.33, "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,9 +196,9 @@ 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",
@@ -223,7 +223,6 @@ 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",
@@ -241,7 +240,6 @@ 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 +279,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.7, "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 +315,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 +347,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",
@@ -439,8 +436,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",
@@ -491,7 +486,6 @@ miette-derive: 7.6.0, "Apache-2.0",
 mimalloc: 0.1.48, "MIT",
 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",
 mockall: 0.14.0, "Apache-2.0 OR MIT",
@@ -500,8 +494,8 @@ moka: 0.12.11, "(Apache-2.0 OR MIT) AND Apache-2.0",
 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",
@@ -643,7 +637,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,6 +656,9 @@ 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",
@@ -687,7 +683,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",
@@ -760,9 +756,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",
@@ -825,12 +821,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",
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..c3afed877 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.7"
 notify = "8.2.0"
 octocrab = "0.48.1"
 serde = { workspace = true, features = ["derive"] }
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/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..8de59f219
--- /dev/null
+++ b/core/server/src/http/web.rs
@@ -0,0 +1,82 @@
+// 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/"]
+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..8dd77898c 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",
@@ -1461,6 +1462,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";,
diff --git a/web/package.json b/web/package.json
index 63800cb17..cd91929f0 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",
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/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..aa36c3359 100644
--- a/web/src/lib/api/fetchRouteApi.ts
+++ b/web/src/lib/api/fetchRouteApi.ts
@@ -17,15 +17,45 @@
  * 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
+  arg: ApiSchema & { queryParams?: Record<string, string> }
 ): 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 query = new URLSearchParams(Object.entries(queryParams));
+      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%
copy from web/src/routes/+layout.server.ts
copy 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/routes/+layout.server.ts 
b/web/src/routes/dashboard/streams/[streamId=i32]/+page.ts
similarity index 70%
rename from web/src/routes/+layout.server.ts
rename to web/src/routes/dashboard/streams/[streamId=i32]/+page.ts
index 27bb9c06f..8ac07677d 100644
--- a/web/src/routes/+layout.server.ts
+++ b/web/src/routes/dashboard/streams/[streamId=i32]/+page.ts
@@ -17,10 +17,17 @@
  * under the License.
  */
 
-import type { LayoutServerLoad } from './$types';
+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 load: LayoutServerLoad = async ({ locals }) => {
   return {
-    user: locals.user
+    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