diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index f2947dbbb867bfff780f68840ef927b7bd457347..f70c6360c2609120af3b32b7fad230c47e90d07e 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -38,12 +38,12 @@ wasm-test:
     - tags
   script:
     - export PATH=/root/go/bin:$PATH
-    - echo > utils/utils_js.s
     - go mod vendor
     - unset SSH_PRIVATE_KEY
     - unset $(env | grep '=' | awk -F= '{print $1}' | grep -v PATH | grep -v GO | grep -v HOME)
-    - echo "WASM TESTS DISABLED FOR XX-4522, but will run them just so you can see output"
-    - GOOS=js GOARCH=wasm go test ./... -v || true
+    - rm vendor/gitlab.com/elixxir/wasm-utils/exception/throw_js.s
+    - mv vendor/gitlab.com/elixxir/wasm-utils/exception/throws.dev vendor/gitlab.com/elixxir/wasm-utils/exception/throws.go
+    - GOOS=js GOARCH=wasm go test ./... -v
 
 build:
   stage: build
@@ -69,9 +69,11 @@ build-workers:
     - GOOS=js GOARCH=wasm go build -ldflags '-w -s' -trimpath -o release/xxdk-channelsIndexedDkWorker.wasm ./indexedDb/impl/channels/...
     - GOOS=js GOARCH=wasm go build -ldflags '-w -s' -trimpath -o release/xxdk-dmIndexedDkWorker.wasm ./indexedDb/impl/dm/...
     - GOOS=js GOARCH=wasm go build -ldflags '-w -s' -trimpath -o release/xxdk-logFileWorker.wasm ./logging/workerThread/...
+    - GOOS=js GOARCH=wasm go build -ldflags '-w -s' -trimpath -o release/xxdk-stateIndexedDbWorker.wasm ./indexedDb/impl/state/...
     - cp indexedDb/impl/channels/channelsIndexedDbWorker.js release/
     - cp indexedDb/impl/dm/dmIndexedDbWorker.js release/
     - cp logging/workerThread/logFileWorker.js release/
+    - cp indexedDb/impl/state/stateIndexedDbWorker.js release/
   artifacts:
     paths:
       - release/
@@ -112,9 +114,11 @@ combine-artifacts:
     - 'curl --fail --location --header "PRIVATE-TOKEN: $GITLAB_ACCESS_TOKEN" --output release/xxdk-channelsIndexedDkWorker.wasm $CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/jobs/$BUILD_WORKERS_JOB_ID/artifacts/release/xxdk-channelsIndexedDkWorker.wasm'
     - 'curl --fail --location --header "PRIVATE-TOKEN: $GITLAB_ACCESS_TOKEN" --output release/xxdk-dmIndexedDkWorker.wasm $CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/jobs/$BUILD_WORKERS_JOB_ID/artifacts/release/xxdk-dmIndexedDkWorker.wasm'
     - 'curl --fail --location --header "PRIVATE-TOKEN: $GITLAB_ACCESS_TOKEN" --output release/xxdk-logFileWorker.wasm $CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/jobs/$BUILD_WORKERS_JOB_ID/artifacts/release/xxdk-logFileWorker.wasm'
+    - 'curl --fail --location --header "PRIVATE-TOKEN: $GITLAB_ACCESS_TOKEN" --output release/xxdk-stateIndexedDbWorker.wasm $CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/jobs/$BUILD_WORKERS_JOB_ID/artifacts/release/xxdk-stateIndexedDbWorker.wasm'
     - 'curl --fail --location --header "PRIVATE-TOKEN: $GITLAB_ACCESS_TOKEN" --output release/channelsIndexedDbWorker.js $CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/jobs/$BUILD_WORKERS_JOB_ID/artifacts/release/channelsIndexedDbWorker.js'
     - 'curl --fail --location --header "PRIVATE-TOKEN: $GITLAB_ACCESS_TOKEN" --output release/dmIndexedDbWorker.js $CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/jobs/$BUILD_WORKERS_JOB_ID/artifacts/release/dmIndexedDbWorker.js'
     - 'curl --fail --location --header "PRIVATE-TOKEN: $GITLAB_ACCESS_TOKEN" --output release/logFileWorker.js $CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/jobs/$BUILD_WORKERS_JOB_ID/artifacts/release/logFileWorker.js'
+    - 'curl --fail --location --header "PRIVATE-TOKEN: $GITLAB_ACCESS_TOKEN" --output release/stateIndexedDbWorker.js $CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/jobs/$BUILD_WORKERS_JOB_ID/artifacts/release/stateIndexedDbWorker.js'
     - ls release
   artifacts:
     paths:
diff --git a/Makefile b/Makefile
index f8c143a6a24f68f6b70d2bc64cc6028b56f4c9af..cd183023d09da89d457b2f7a4ec08d33ab75db15 100644
--- a/Makefile
+++ b/Makefile
@@ -11,18 +11,20 @@ build:
 	GOOS=js GOARCH=wasm go build ./...
 
 update_release:
+	GOFLAGS="" go get gitlab.com/elixxir/wasm-utils@release
 	GOFLAGS="" go get gitlab.com/xx_network/primitives@release
 	GOFLAGS="" go get gitlab.com/elixxir/primitives@release
 	GOFLAGS="" go get gitlab.com/xx_network/crypto@release
 	GOFLAGS="" go get gitlab.com/elixxir/crypto@release
-	GOFLAGS="" go get -d gitlab.com/elixxir/client/v4@release
+	GOFLAGS="" go get -d gitlab.com/elixxir/client/v4@project/HavenBeta
 
 update_master:
-	GOFLAGS="" go get -d gitlab.com/elixxir/client@master
-	GOFLAGS="" go get gitlab.com/elixxir/crypto@master
+	GOFLAGS="" go get gitlab.com/elixxir/wasm-utils@master
+	GOFLAGS="" go get gitlab.com/xx_network/primitives@master
 	GOFLAGS="" go get gitlab.com/elixxir/primitives@master
 	GOFLAGS="" go get gitlab.com/xx_network/crypto@master
-	GOFLAGS="" go get gitlab.com/xx_network/primitives@master
+	GOFLAGS="" go get gitlab.com/elixxir/crypto@master
+	GOFLAGS="" go get -d gitlab.com/elixxir/client/v4@master
 
 binary:
 	GOOS=js GOARCH=wasm go build -ldflags '-w -s' -trimpath -o xxdk.wasm main.go
@@ -30,15 +32,21 @@ binary:
 worker_binaries:
 	GOOS=js GOARCH=wasm go build -ldflags '-w -s' -trimpath -o xxdk-channelsIndexedDkWorker.wasm ./indexedDb/impl/channels/...
 	GOOS=js GOARCH=wasm go build -ldflags '-w -s' -trimpath -o xxdk-dmIndexedDkWorker.wasm ./indexedDb/impl/dm/...
+	GOOS=js GOARCH=wasm go build -ldflags '-w -s' -trimpath -o xxdk-stateIndexedDkWorker.wasm ./indexedDb/impl/state/...
 	GOOS=js GOARCH=wasm go build -ldflags '-w -s' -trimpath -o xxdk-logFileWorker.wasm ./logging/workerThread/...
 
 binaries: binary worker_binaries
 
+wasmException = "vendor/gitlab.com/elixxir/wasm-utils/exception"
+
 wasm_tests:
-	cp utils/utils_js.s utils/utils_js.s.bak
-	> utils/utils_js.s
+	cp $(wasmException)/throw_js.s $(wasmException)/throw_js.s.bak
+	cp $(wasmException)/throws.go $(wasmException)/throws.go.bak
+	> $(wasmException)/throw_js.s
+	cp $(wasmException)/throws.dev $(wasmException)/throws.go
 	-GOOS=js GOARCH=wasm go test -v ./...
-	mv utils/utils_js.s.bak utils/utils_js.s
+	mv $(wasmException)/throw_js.s.bak $(wasmException)/throw_js.s
+	mv $(wasmException)/throws.go.bak $(wasmException)/throws.go
 
 go_tests:
 	go test ./... -v
diff --git a/README.md b/README.md
index d1f59cd4392fce9f8b2b541862e69fa684400039..a898480b75ee28ead215556ad146c7e9a3cdf842 100644
--- a/README.md
+++ b/README.md
@@ -77,7 +77,7 @@ global.Go = class {
             go: {
                 // ...
                 // func Throw(exception string, message string)
-                'gitlab.com/elixxir/xxdk-wasm/utils.throw': (sp) => {
+                'gitlab.com/elixxir/wasm-utils/utils.throw': (sp) => {
                     const exception = loadString(sp + 8)
                     const message = loadString(sp + 24)
                     throw globalThis[exception](message)
diff --git a/go.mod b/go.mod
index 451c29d33b6508154a2f659053bfad943c183e30..55fa3b9d967a383db414693e960113256ec565e5 100644
--- a/go.mod
+++ b/go.mod
@@ -6,12 +6,15 @@ require (
 	github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2
 	github.com/hack-pad/go-indexeddb v0.2.0
 	github.com/pkg/errors v0.9.1
+	github.com/spf13/cobra v1.7.0
 	github.com/spf13/jwalterweatherman v1.1.0
-	gitlab.com/elixxir/client/v4 v4.6.4-0.20230426210017-366b4270492e
-	gitlab.com/elixxir/crypto v0.0.7-0.20230424221508-14c052d4b967
+	github.com/stretchr/testify v1.8.2
+	gitlab.com/elixxir/client/v4 v4.6.4-0.20230526193945-9f195a236f77
+	gitlab.com/elixxir/crypto v0.0.7-0.20230526183834-62f8f49617bc
 	gitlab.com/elixxir/primitives v0.0.3-0.20230214180039-9a25e2d3969c
+	gitlab.com/elixxir/wasm-utils v0.0.0-20230522231408-a43b2c1481b2
 	gitlab.com/xx_network/crypto v0.0.5-0.20230214003943-8a09396e95dd
-	gitlab.com/xx_network/primitives v0.0.4-0.20230310205521-c440e68e34c4
+	gitlab.com/xx_network/primitives v0.0.4-0.20230522171102-940cdd68e516
 	golang.org/x/crypto v0.5.0
 )
 
@@ -22,6 +25,7 @@ require (
 	github.com/badoux/checkmail v1.2.1 // indirect
 	github.com/cenkalti/backoff/v4 v4.1.3 // indirect
 	github.com/cloudflare/circl v1.2.0 // indirect
+	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect
 	github.com/elliotchance/orderedmap v1.4.0 // indirect
 	github.com/fsnotify/fsnotify v1.5.4 // indirect
@@ -31,12 +35,13 @@ require (
 	github.com/gorilla/websocket v1.5.0 // indirect
 	github.com/hashicorp/hcl v1.0.0 // indirect
 	github.com/improbable-eng/grpc-web v0.15.0 // indirect
-	github.com/inconshreveable/mousetrap v1.0.0 // indirect
+	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/jinzhu/inflection v1.0.0 // indirect
 	github.com/jinzhu/now v1.1.5 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
 	github.com/klauspost/compress v1.15.9 // indirect
 	github.com/klauspost/cpuid/v2 v2.1.0 // indirect
+	github.com/kr/pretty v0.3.0 // indirect
 	github.com/magiconair/properties v1.8.6 // indirect
 	github.com/mattn/go-isatty v0.0.14 // indirect
 	github.com/mattn/go-sqlite3 v1.14.15 // indirect
@@ -48,13 +53,13 @@ require (
 	github.com/pelletier/go-toml v1.9.5 // indirect
 	github.com/pelletier/go-toml/v2 v2.0.2 // indirect
 	github.com/pkg/profile v1.6.0 // indirect
+	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/rs/cors v1.8.2 // indirect
 	github.com/sethvargo/go-diceware v0.3.0 // indirect
 	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
 	github.com/soheilhy/cmux v0.1.5 // indirect
 	github.com/spf13/afero v1.9.2 // indirect
 	github.com/spf13/cast v1.5.0 // indirect
-	github.com/spf13/cobra v1.5.0 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
 	github.com/spf13/viper v1.12.0 // indirect
 	github.com/subosito/gotenv v1.4.0 // indirect
@@ -63,8 +68,8 @@ require (
 	github.com/tyler-smith/go-bip39 v1.1.0 // indirect
 	github.com/zeebo/blake3 v0.2.3 // indirect
 	gitlab.com/elixxir/bloomfilter v0.0.0-20230322223210-fa84f6842de8 // indirect
-	gitlab.com/elixxir/comms v0.0.4-0.20230322130755-e59b16bce601 // indirect
-	gitlab.com/elixxir/ekv v0.2.2 // indirect
+	gitlab.com/elixxir/comms v0.0.4-0.20230519211512-4a998f4b0938 // indirect
+	gitlab.com/elixxir/ekv v0.3.1-0.20230525213559-f9da13f4fce1 // indirect
 	gitlab.com/xx_network/comms v0.0.4-0.20230214180029-5387fb85736d // indirect
 	gitlab.com/xx_network/ring v0.0.3-0.20220902183151-a7d3b15bc981 // indirect
 	gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect
diff --git a/go.sum b/go.sum
index f6565c5bab2e9f653cad6d914e6912754fcd4198..b3aa49e12dccd8ce395670f167ca59d718d29bca 100644
--- a/go.sum
+++ b/go.sum
@@ -97,6 +97,7 @@ github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfc
 github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
 github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -266,8 +267,9 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
 github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/improbable-eng/grpc-web v0.15.0 h1:BN+7z6uNXZ1tQGcNAuaU1YjsLTApzkjt2tzCixLaUPQ=
 github.com/improbable-eng/grpc-web v0.15.0/go.mod h1:1sy9HKV4Jt9aEs9JSnkWlRJPuPtwNr0l57L4f878wP8=
-github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
 github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
 github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
@@ -303,8 +305,12 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxv
 github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
 github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
+github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/ktr0731/grpc-test v0.1.12 h1:Yha+zH2hB48huOfbsEMfyG7FeHCrVWq4fYmHfr3iH3U=
 github.com/ktr0731/grpc-web-go-client v0.2.8 h1:nUf9p+YWirmFwmH0mwtAWhuXvzovc+/3C/eAY2Fshnk=
 github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
@@ -422,6 +428,8 @@ github.com/prometheus/procfs v0.3.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O
 github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
+github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
 github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
 github.com/rs/cors v1.8.2 h1:KCooALfAYGs415Cwu5ABvv9n9509fSiG5SQJn/AQo4U=
 github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
@@ -450,8 +458,8 @@ github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcD
 github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
 github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
 github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
-github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
-github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
+github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
+github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
 github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
 github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
 github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
@@ -505,28 +513,34 @@ github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
 github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
 gitlab.com/elixxir/bloomfilter v0.0.0-20230322223210-fa84f6842de8 h1:uAFCyBkXprQoPkcDDfxXtaMyL5x+xSGrAWzR907xROQ=
 gitlab.com/elixxir/bloomfilter v0.0.0-20230322223210-fa84f6842de8/go.mod h1:1X8gRIAPDisS3W6Vtr/ymiUmZMJUIwDV1o5DEOo/pzw=
-gitlab.com/elixxir/client/v4 v4.6.3 h1:oUsm5cn2Vnfqz+xwGYKrqFkPNN3sDAyp00EPGhUIA5E=
-gitlab.com/elixxir/client/v4 v4.6.3/go.mod h1:G+lN+LvQPGcm5BQnrhnqT1xiRIAzH3OffAM+5oI9SUg=
-gitlab.com/elixxir/client/v4 v4.6.4-0.20230426210017-366b4270492e h1:Fvy4IBGtZLmqFXY78ucML8JiQhHCijpmp1uyQRSs8Ug=
-gitlab.com/elixxir/client/v4 v4.6.4-0.20230426210017-366b4270492e/go.mod h1:8r/PC1nVifMmOYcK1ko0UkaAgfi+J/OVxSBj6LkwrO4=
-gitlab.com/elixxir/comms v0.0.4-0.20230322130755-e59b16bce601 h1:l9ZVDOXf0fvbFnNXWmwnsEIvUIUL5fy3mFexrYg8dx4=
-gitlab.com/elixxir/comms v0.0.4-0.20230322130755-e59b16bce601/go.mod h1:z+qW0D9VpY5QKTd7wRlb5SK4kBNqLYsa4DXBcUXue9Q=
-gitlab.com/elixxir/crypto v0.0.7-0.20230413162806-a99ec4bfea32 h1:Had0F7rMPgJJ2BUZoFNgeJq33md9RpV15nvd08Uxdzc=
-gitlab.com/elixxir/crypto v0.0.7-0.20230413162806-a99ec4bfea32/go.mod h1:/SLOlvkYVVJf6IU+vEjMLnS7cjjcoTlPV45g6tv6INc=
-gitlab.com/elixxir/crypto v0.0.7-0.20230424221508-14c052d4b967 h1:yKGoNe9xtHROwbep7yGYhTvbxm4cSRycduAkEArTE9s=
-gitlab.com/elixxir/crypto v0.0.7-0.20230424221508-14c052d4b967/go.mod h1:/SLOlvkYVVJf6IU+vEjMLnS7cjjcoTlPV45g6tv6INc=
-gitlab.com/elixxir/ekv v0.2.1 h1:dtwbt6KmAXG2Tik5d60iDz2fLhoFBgWwST03p7T+9Is=
-gitlab.com/elixxir/ekv v0.2.1/go.mod h1:USLD7xeDnuZEavygdrgzNEwZXeLQJK/w1a+htpN+JEU=
-gitlab.com/elixxir/ekv v0.2.2 h1:hzb3JLTFJXETaSvWoK1xJ89K6W00uFprcoAdogPZCEM=
-gitlab.com/elixxir/ekv v0.2.2/go.mod h1:USLD7xeDnuZEavygdrgzNEwZXeLQJK/w1a+htpN+JEU=
+gitlab.com/elixxir/client/v4 v4.6.4-0.20230525191042-3795029e4315 h1:rOgY9KPwq0wCQGM2VWzHQLidUwo/igqEbHvrfrX14NE=
+gitlab.com/elixxir/client/v4 v4.6.4-0.20230525191042-3795029e4315/go.mod h1:1+FU4spF6kwSA84AnFq0i6j4jsAICuvVfp6ACh00K0U=
+gitlab.com/elixxir/client/v4 v4.6.4-0.20230526074923-c0fb2a45c312 h1:yUyz1L/rzodZxUU45CeH7mfMfmKAs706+s2j2ZGUSwE=
+gitlab.com/elixxir/client/v4 v4.6.4-0.20230526074923-c0fb2a45c312/go.mod h1:fegbuF1/6a+H3QgsoMG8teLnyuKtDxkELMw8pn5WlZ8=
+gitlab.com/elixxir/client/v4 v4.6.4-0.20230526185452-5da5d4f474f6 h1:qAIeh68suM7rLJ5RHt1AvZoYmehIwPLGeDzm6SMc0R4=
+gitlab.com/elixxir/client/v4 v4.6.4-0.20230526185452-5da5d4f474f6/go.mod h1:0fEqbELJdwTNRsrvgecZbGsJSX2TqHUst/LSBKo/cL0=
+gitlab.com/elixxir/client/v4 v4.6.4-0.20230526193945-9f195a236f77 h1:qCxO7ZuUGXh54iCi8hlzl3OBBI+B1MzngCZAWCv+S9c=
+gitlab.com/elixxir/client/v4 v4.6.4-0.20230526193945-9f195a236f77/go.mod h1:0fEqbELJdwTNRsrvgecZbGsJSX2TqHUst/LSBKo/cL0=
+gitlab.com/elixxir/comms v0.0.4-0.20230519211512-4a998f4b0938 h1:f27+QUFiGWrprKm+fstOg3ABkYLpWcZi3+8Lf5eDnqY=
+gitlab.com/elixxir/comms v0.0.4-0.20230519211512-4a998f4b0938/go.mod h1:z+qW0D9VpY5QKTd7wRlb5SK4kBNqLYsa4DXBcUXue9Q=
+gitlab.com/elixxir/crypto v0.0.7-0.20230522162218-45433d877235 h1:0BySdXTzRWxzH8k5RiNNMmmn2lpuQWLVcDDA/7ehyqc=
+gitlab.com/elixxir/crypto v0.0.7-0.20230522162218-45433d877235/go.mod h1:IYInxKr5Q7EH3oNhg1QX1/sTTRNi7L0JkcyfdRegoio=
+gitlab.com/elixxir/crypto v0.0.7-0.20230526183834-62f8f49617bc h1:Rl8q37axi4XVuuDfXP+bYc9iAcVb3O9jyYWuQTV5+Z8=
+gitlab.com/elixxir/crypto v0.0.7-0.20230526183834-62f8f49617bc/go.mod h1:IYInxKr5Q7EH3oNhg1QX1/sTTRNi7L0JkcyfdRegoio=
+gitlab.com/elixxir/ekv v0.3.1-0.20230525165450-f444c687504b h1:hf28yepO93tCacx1bUAh8vVFkBUEuBaJhOjifBxEQK4=
+gitlab.com/elixxir/ekv v0.3.1-0.20230525165450-f444c687504b/go.mod h1:EMaUQrsOxvEPQ0/8V/PSkGqFmEC2axBG/uqY0oW2uJM=
+gitlab.com/elixxir/ekv v0.3.1-0.20230525213559-f9da13f4fce1 h1:8XBo6QQBXXGCTrgXHFuqPL21mROLKLAoO3X9xR5TwA0=
+gitlab.com/elixxir/ekv v0.3.1-0.20230525213559-f9da13f4fce1/go.mod h1:UStTZ9d1UVn9Ahyb49lrbPKyr/Wb8xFWqMXbDgIqQhE=
 gitlab.com/elixxir/primitives v0.0.3-0.20230214180039-9a25e2d3969c h1:muG8ff95woeVVwQoJHCEclxBFB22lc7EixPylEkYDRU=
 gitlab.com/elixxir/primitives v0.0.3-0.20230214180039-9a25e2d3969c/go.mod h1:phun4PLkHJA6wcL4JIhhxZztrmCyJHWPNppBP3DUD2Y=
+gitlab.com/elixxir/wasm-utils v0.0.0-20230522231408-a43b2c1481b2 h1:GQb350yPBkWRkPRgNSVFF0ZZDOAlXWIKQBI/1Ff6biU=
+gitlab.com/elixxir/wasm-utils v0.0.0-20230522231408-a43b2c1481b2/go.mod h1:wB7Vh/7LWUm8wYRBSd+6lxfpk4CnDaHTkLCIVKfL2TA=
 gitlab.com/xx_network/comms v0.0.4-0.20230214180029-5387fb85736d h1:AZf2h0fxyO1KxhZPP9//jG3Swb2BcuKbxtNXJgooLss=
 gitlab.com/xx_network/comms v0.0.4-0.20230214180029-5387fb85736d/go.mod h1:8cwPyH6G8C4qf/U5KDghn1ksOh79MrNqthjKDrfvbXY=
 gitlab.com/xx_network/crypto v0.0.5-0.20230214003943-8a09396e95dd h1:IleH6U5D/c2zF6YL/z3cBKqBPnI5ApNMCtU7ia4t228=
 gitlab.com/xx_network/crypto v0.0.5-0.20230214003943-8a09396e95dd/go.mod h1:PPPaFoY5Ze1qft9D0a24UHAwlvWEc2GbraihXvKYkf4=
-gitlab.com/xx_network/primitives v0.0.4-0.20230310205521-c440e68e34c4 h1:g8dsLA3tMjoix/9kZl+ELxGt/cTuuPopqUagawPwYpk=
-gitlab.com/xx_network/primitives v0.0.4-0.20230310205521-c440e68e34c4/go.mod h1:ABtt5oK+Sl1Q9l3qWK9efxmLKtNMSskpIjbe6IvB9sQ=
+gitlab.com/xx_network/primitives v0.0.4-0.20230522171102-940cdd68e516 h1:+mhYGiDuVGlTzZMvM7bAybGU85oSXffwIgYxeeBkJmE=
+gitlab.com/xx_network/primitives v0.0.4-0.20230522171102-940cdd68e516/go.mod h1:ABtt5oK+Sl1Q9l3qWK9efxmLKtNMSskpIjbe6IvB9sQ=
 gitlab.com/xx_network/ring v0.0.3-0.20220902183151-a7d3b15bc981 h1:1s0vX9BbkiD0IVXwr3LOaTBcq1wBrWcUWMBK0s8r0Z0=
 gitlab.com/xx_network/ring v0.0.3-0.20220902183151-a7d3b15bc981/go.mod h1:aLzpP2TiZTQut/PVHR40EJAomzugDdHXetbieRClXIM=
 gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec h1:FpfFs4EhNehiVfzQttTuxanPIT43FtkkCFypIod8LHo=
@@ -915,6 +929,7 @@ google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
diff --git a/indexedDb/impl/channels/callbacks.go b/indexedDb/impl/channels/callbacks.go
index df560535a779f2e6c355c6e49a66d4b45c2047e2..2a901597117fac367e360ee2f80a2567fcab036f 100644
--- a/indexedDb/impl/channels/callbacks.go
+++ b/indexedDb/impl/channels/callbacks.go
@@ -10,8 +10,9 @@
 package main
 
 import (
-	"crypto/ed25519"
 	"encoding/json"
+	"time"
+
 	"github.com/pkg/errors"
 	jww "github.com/spf13/jwalterweatherman"
 	"gitlab.com/elixxir/client/v4/channels"
@@ -24,7 +25,6 @@ import (
 	"gitlab.com/elixxir/xxdk-wasm/worker"
 	"gitlab.com/xx_network/crypto/csprng"
 	"gitlab.com/xx_network/primitives/id"
-	"time"
 )
 
 var zeroUUID = []byte{0, 0, 0, 0, 0, 0, 0, 0}
@@ -32,24 +32,24 @@ var zeroUUID = []byte{0, 0, 0, 0, 0, 0, 0, 0}
 // manager handles the event model and the message callbacks, which is used to
 // send information between the event model and the main thread.
 type manager struct {
-	mh    *worker.ThreadManager
+	wtm   *worker.ThreadManager
 	model channels.EventModel
 }
 
 // registerCallbacks registers all the reception callbacks to manage messages
 // from the main thread for the channels.EventModel.
 func (m *manager) registerCallbacks() {
-	m.mh.RegisterCallback(wChannels.NewWASMEventModelTag, m.newWASMEventModelCB)
-	m.mh.RegisterCallback(wChannels.JoinChannelTag, m.joinChannelCB)
-	m.mh.RegisterCallback(wChannels.LeaveChannelTag, m.leaveChannelCB)
-	m.mh.RegisterCallback(wChannels.ReceiveMessageTag, m.receiveMessageCB)
-	m.mh.RegisterCallback(wChannels.ReceiveReplyTag, m.receiveReplyCB)
-	m.mh.RegisterCallback(wChannels.ReceiveReactionTag, m.receiveReactionCB)
-	m.mh.RegisterCallback(wChannels.UpdateFromUUIDTag, m.updateFromUUIDCB)
-	m.mh.RegisterCallback(wChannels.UpdateFromMessageIDTag, m.updateFromMessageIDCB)
-	m.mh.RegisterCallback(wChannels.GetMessageTag, m.getMessageCB)
-	m.mh.RegisterCallback(wChannels.DeleteMessageTag, m.deleteMessageCB)
-	m.mh.RegisterCallback(wChannels.MuteUserTag, m.muteUserCB)
+	m.wtm.RegisterCallback(wChannels.NewWASMEventModelTag, m.newWASMEventModelCB)
+	m.wtm.RegisterCallback(wChannels.JoinChannelTag, m.joinChannelCB)
+	m.wtm.RegisterCallback(wChannels.LeaveChannelTag, m.leaveChannelCB)
+	m.wtm.RegisterCallback(wChannels.ReceiveMessageTag, m.receiveMessageCB)
+	m.wtm.RegisterCallback(wChannels.ReceiveReplyTag, m.receiveReplyCB)
+	m.wtm.RegisterCallback(wChannels.ReceiveReactionTag, m.receiveReactionCB)
+	m.wtm.RegisterCallback(wChannels.UpdateFromUUIDTag, m.updateFromUUIDCB)
+	m.wtm.RegisterCallback(wChannels.UpdateFromMessageIDTag, m.updateFromMessageIDCB)
+	m.wtm.RegisterCallback(wChannels.GetMessageTag, m.getMessageCB)
+	m.wtm.RegisterCallback(wChannels.DeleteMessageTag, m.deleteMessageCB)
+	m.wtm.RegisterCallback(wChannels.MuteUserTag, m.muteUserCB)
 }
 
 // newWASMEventModelCB is the callback for NewWASMEventModel. Returns an empty
@@ -71,8 +71,7 @@ func (m *manager) newWASMEventModelCB(data []byte) ([]byte, error) {
 			"failed to JSON unmarshal Cipher from main thread: %+v", err)
 	}
 
-	m.model, err = NewWASMEventModel(msg.DatabaseName, encryption,
-		m.messageReceivedCallback, m.deletedMessageCallback, m.mutedUserCallback)
+	m.model, err = NewWASMEventModel(msg.DatabaseName, encryption, m)
 	if err != nil {
 		return []byte(err.Error()), nil
 	}
@@ -80,47 +79,12 @@ func (m *manager) newWASMEventModelCB(data []byte) ([]byte, error) {
 	return []byte{}, nil
 }
 
-// messageReceivedCallback sends calls to the channels.MessageReceivedCallback
-// in the main thread.
-//
-// storeEncryptionStatus adhere to the channels.MessageReceivedCallback type.
-func (m *manager) messageReceivedCallback(
-	uuid uint64, channelID *id.ID, update bool) {
-	// Package parameters for sending
-	msg := &wChannels.MessageReceivedCallbackMessage{
-		UUID:      uuid,
-		ChannelID: channelID,
-		Update:    update,
-	}
-	data, err := json.Marshal(msg)
-	if err != nil {
-		jww.ERROR.Printf("Could not JSON marshal %T: %+v", msg, err)
-		return
-	}
-
-	// Send it to the main thread
-	m.mh.SendMessage(wChannels.MessageReceivedCallbackTag, data)
-}
-
-// deletedMessageCallback sends calls to the channels.DeletedMessageCallback in
-// the main thread.
-//
-// storeEncryptionStatus adhere to the channels.MessageReceivedCallback type.
-func (m *manager) deletedMessageCallback(messageID message.ID) {
-	m.mh.SendMessage(wChannels.DeletedMessageCallbackTag, messageID.Marshal())
-}
-
-// mutedUserCallback sends calls to the channels.MutedUserCallback in the main
-// thread.
-//
-// storeEncryptionStatus adhere to the channels.MessageReceivedCallback type.
-func (m *manager) mutedUserCallback(
-	channelID *id.ID, pubKey ed25519.PublicKey, unmute bool) {
+// EventUpdate implements [bindings.ChannelUICallbacks.EventUpdate].
+func (m *manager) EventUpdate(eventType int64, jsonData []byte) {
 	// Package parameters for sending
-	msg := &wChannels.MuteUserMessage{
-		ChannelID: channelID,
-		PubKey:    pubKey,
-		Unmute:    unmute,
+	msg := &wChannels.EventUpdateCallbackMessage{
+		EventType: eventType,
+		JsonData:  jsonData,
 	}
 	data, err := json.Marshal(msg)
 	if err != nil {
@@ -129,7 +93,7 @@ func (m *manager) mutedUserCallback(
 	}
 
 	// Send it to the main thread
-	m.mh.SendMessage(wChannels.MutedUserCallbackTag, data)
+	m.wtm.SendMessage(wChannels.EventUpdateCallbackTag, data)
 }
 
 // joinChannelCB is the callback for wasmModel.JoinChannel. Always returns nil;
@@ -368,7 +332,12 @@ func (m *manager) muteUserCB(data []byte) ([]byte, error) {
 			"failed to JSON unmarshal %T from main thread: %+v", msg, err)
 	}
 
-	m.model.MuteUser(msg.ChannelID, msg.PubKey, msg.Unmute)
+	channelID := id.ID{}
+	err = channelID.UnmarshalJSON(msg.ChannelID)
+	if err != nil {
+		return nil, err
+	}
+	m.model.MuteUser(&channelID, msg.PubKey, msg.Unmute)
 
 	return nil, nil
 }
diff --git a/indexedDb/impl/channels/channelsIndexedDbWorker.js b/indexedDb/impl/channels/channelsIndexedDbWorker.js
index 9e69bdd70eddebc9f82b23d04d823221ad3c1622..c109cba89735425f23c0d4d436532a3e3eb9a852 100644
--- a/indexedDb/impl/channels/channelsIndexedDbWorker.js
+++ b/indexedDb/impl/channels/channelsIndexedDbWorker.js
@@ -7,11 +7,15 @@
 
 importScripts('wasm_exec.js');
 
+const isReady = new Promise((resolve) => {
+    self.onWasmInitialized = resolve;
+});
+
 const go = new Go();
 const binPath = 'xxdk-channelsIndexedDkWorker.wasm'
-WebAssembly.instantiateStreaming(fetch(binPath), go.importObject).then((result) => {
+WebAssembly.instantiateStreaming(fetch(binPath), go.importObject).then(async (result) => {
     go.run(result.instance);
-    LogLevel(1);
+    await isReady;
 }).catch((err) => {
     console.error(err);
 });
\ No newline at end of file
diff --git a/indexedDb/impl/channels/fileTransferImpl.go b/indexedDb/impl/channels/fileTransferImpl.go
index d75dc78b6dddd5825d7c1b157dddadd75d62698d..4e30a7f91933d7cae8bc76a0efd06000be5dc3ef 100644
--- a/indexedDb/impl/channels/fileTransferImpl.go
+++ b/indexedDb/impl/channels/fileTransferImpl.go
@@ -15,8 +15,8 @@ import (
 	"gitlab.com/elixxir/client/v4/channels"
 	cft "gitlab.com/elixxir/client/v4/channelsFileTransfer"
 	"gitlab.com/elixxir/crypto/fileTransfer"
+	"gitlab.com/elixxir/wasm-utils/utils"
 	"gitlab.com/elixxir/xxdk-wasm/indexedDb/impl"
-	"gitlab.com/elixxir/xxdk-wasm/utils"
 	"strings"
 	"time"
 )
diff --git a/indexedDb/impl/channels/implementation.go b/indexedDb/impl/channels/implementation.go
index 415aca68d4e0c4e9dd7269a055bd9c3322770922..e755acbafb3f750e577305fb9e241c537f30febe 100644
--- a/indexedDb/impl/channels/implementation.go
+++ b/indexedDb/impl/channels/implementation.go
@@ -21,28 +21,24 @@ import (
 	"github.com/pkg/errors"
 	jww "github.com/spf13/jwalterweatherman"
 
+	"gitlab.com/elixxir/client/v4/bindings"
 	"gitlab.com/elixxir/client/v4/channels"
 	"gitlab.com/elixxir/client/v4/cmix/rounds"
 	cryptoBroadcast "gitlab.com/elixxir/crypto/broadcast"
 	cryptoChannel "gitlab.com/elixxir/crypto/channel"
 	"gitlab.com/elixxir/crypto/message"
+	"gitlab.com/elixxir/wasm-utils/utils"
 	"gitlab.com/elixxir/xxdk-wasm/indexedDb/impl"
-	wChannels "gitlab.com/elixxir/xxdk-wasm/indexedDb/worker/channels"
-	"gitlab.com/elixxir/xxdk-wasm/utils"
 	"gitlab.com/xx_network/primitives/id"
 )
 
-// wasmModel implements [channels.EventModel] interface, which uses the channels
-// system passed an object that adheres to in order to get events on the
-// channel.
+// wasmModel implements [channels.EventModel] interface backed by IndexedDb.
 // NOTE: This model is NOT thread safe - it is the responsibility of the
 // caller to ensure that its methods are called sequentially.
 type wasmModel struct {
-	db                *idb.Database
-	cipher            cryptoChannel.Cipher
-	receivedMessageCB wChannels.MessageReceivedCallback
-	deletedMessageCB  wChannels.DeletedMessageCallback
-	mutedUserCB       wChannels.MutedUserCallback
+	db          *idb.Database
+	cipher      cryptoChannel.Cipher
+	eventUpdate func(eventType int64, jsonMarshallable any)
 }
 
 // JoinChannel is called whenever a channel is joined locally.
@@ -121,19 +117,19 @@ func (w *wasmModel) deleteMsgByChannel(channelID *id.ID) error {
 			"Unable to get Index: %+v", err)
 	}
 
-	// Perform the operation
+	// Set up the operation
 	keyRange, err := idb.NewKeyRangeOnly(impl.EncodeBytes(channelID.Marshal()))
 	cursorRequest, err := index.OpenCursorRange(keyRange, idb.CursorNext)
 	if err != nil {
 		return errors.WithMessagef(parentErr, "Unable to open Cursor: %+v", err)
 	}
-	ctx, cancel := impl.NewContext()
-	err = cursorRequest.Iter(ctx,
+
+	// Perform the operation
+	err = impl.SendCursorRequest(cursorRequest,
 		func(cursor *idb.CursorWithValue) error {
 			_, err := cursor.Delete()
 			return err
 		})
-	cancel()
 	if err != nil {
 		return errors.WithMessagef(parentErr,
 			"Unable to delete Message data: %+v", err)
@@ -161,8 +157,10 @@ func (w *wasmModel) ReceiveMessage(channelID *id.ID, messageID message.ID,
 		}
 	}
 
+	channelIDBytes := channelID.Marshal()
+
 	msgToInsert := buildMessage(
-		channelID.Marshal(), messageID.Bytes(), nil, nickname,
+		channelIDBytes, messageID.Bytes(), nil, nickname,
 		textBytes, pubKey, dmToken, codeset, timestamp, lease, round.ID, mType,
 		false, hidden, status)
 
@@ -172,7 +170,11 @@ func (w *wasmModel) ReceiveMessage(channelID *id.ID, messageID message.ID,
 		return 0
 	}
 
-	go w.receivedMessageCB(uuid, channelID, false)
+	go w.eventUpdate(bindings.MessageReceived, bindings.MessageReceivedJson{
+		Uuid:      int64(uuid),
+		ChannelID: channelID,
+		Update:    false,
+	})
 	return uuid
 }
 
@@ -199,7 +201,9 @@ func (w *wasmModel) ReceiveReply(channelID *id.ID, messageID,
 		}
 	}
 
-	msgToInsert := buildMessage(channelID.Marshal(), messageID.Bytes(),
+	channelIDBytes := channelID.Marshal()
+
+	msgToInsert := buildMessage(channelIDBytes, messageID.Bytes(),
 		replyTo.Bytes(), nickname, textBytes, pubKey, dmToken, codeset,
 		timestamp, lease, round.ID, mType, hidden, false, status)
 
@@ -208,7 +212,12 @@ func (w *wasmModel) ReceiveReply(channelID *id.ID, messageID,
 		jww.ERROR.Printf("Failed to receive reply: %+v", err)
 		return 0
 	}
-	go w.receivedMessageCB(uuid, channelID, false)
+
+	go w.eventUpdate(bindings.MessageReceived, bindings.MessageReceivedJson{
+		Uuid:      int64(uuid),
+		ChannelID: channelID,
+		Update:    false,
+	})
 	return uuid
 }
 
@@ -235,8 +244,9 @@ func (w *wasmModel) ReceiveReaction(channelID *id.ID, messageID,
 		}
 	}
 
+	channelIDBytes := channelID.Marshal()
 	msgToInsert := buildMessage(
-		channelID.Marshal(), messageID.Bytes(), reactionTo.Bytes(), nickname,
+		channelIDBytes, messageID.Bytes(), reactionTo.Bytes(), nickname,
 		textBytes, pubKey, dmToken, codeset, timestamp, lease, round.ID, mType,
 		false, hidden, status)
 
@@ -245,7 +255,12 @@ func (w *wasmModel) ReceiveReaction(channelID *id.ID, messageID,
 		jww.ERROR.Printf("Failed to receive reaction: %+v", err)
 		return 0
 	}
-	go w.receivedMessageCB(uuid, channelID, false)
+
+	go w.eventUpdate(bindings.MessageReceived, bindings.MessageReceivedJson{
+		Uuid:      int64(uuid),
+		ChannelID: channelID,
+		Update:    false,
+	})
 	return uuid
 }
 
@@ -392,9 +407,17 @@ func (w *wasmModel) updateMessage(currentMsg *Message, messageID *message.ID,
 	if err != nil {
 		return 0, err
 	}
-	channelID := &id.ID{}
-	copy(channelID[:], currentMsg.ChannelID)
-	go w.receivedMessageCB(uuid, channelID, true)
+
+	channelID, err := id.Unmarshal(currentMsg.ChannelID)
+	if err != nil {
+		return 0, err
+	}
+
+	go w.eventUpdate(bindings.MessageReceived, bindings.MessageReceivedJson{
+		Uuid:      int64(uuid),
+		ChannelID: channelID,
+		Update:    true,
+	})
 
 	return uuid, nil
 }
@@ -415,7 +438,8 @@ func (w *wasmModel) upsertMessage(msg *Message) (uint64, error) {
 	// Store message to database
 	msgIdObj, err := impl.Put(w.db, messageStoreName, messageObj)
 	if err != nil {
-		return 0, errors.Errorf("Unable to put Message: %+v", err)
+		return 0, errors.Errorf("Unable to put Message: %+v\n%s",
+			err, newMessageJson)
 	}
 
 	uuid := msgIdObj.Int()
@@ -491,7 +515,8 @@ func (w *wasmModel) DeleteMessage(messageID message.ID) error {
 		return err
 	}
 
-	go w.deletedMessageCB(messageID)
+	go w.eventUpdate(bindings.MessageDeleted,
+		bindings.MessageDeletedJson{MessageID: messageID})
 
 	return nil
 }
@@ -499,7 +524,12 @@ func (w *wasmModel) DeleteMessage(messageID message.ID) error {
 // MuteUser is called whenever a user is muted or unmuted.
 func (w *wasmModel) MuteUser(
 	channelID *id.ID, pubKey ed25519.PublicKey, unmute bool) {
-	go w.mutedUserCB(channelID, pubKey, unmute)
+
+	go w.eventUpdate(bindings.UserMuted, bindings.UserMutedJson{
+		ChannelID: channelID,
+		PubKey:    pubKey,
+		Unmute:    unmute,
+	})
 }
 
 // valueToMessage is a helper for converting js.Value to Message.
diff --git a/indexedDb/impl/channels/implementation_test.go b/indexedDb/impl/channels/implementation_test.go
index 4b22b8300f5549a5dc2f3902f7ed95277777555f..f596065dcc47aa39e3b5aeda7b97a1f389071977 100644
--- a/indexedDb/impl/channels/implementation_test.go
+++ b/indexedDb/impl/channels/implementation_test.go
@@ -11,12 +11,9 @@ package main
 
 import (
 	"bytes"
-	"crypto/ed25519"
 	"encoding/json"
 	"errors"
 	"fmt"
-	cft "gitlab.com/elixxir/client/v4/channelsFileTransfer"
-	"gitlab.com/elixxir/crypto/fileTransfer"
 	"os"
 	"strconv"
 	"testing"
@@ -24,14 +21,17 @@ import (
 
 	"github.com/hack-pad/go-indexeddb/idb"
 	jww "github.com/spf13/jwalterweatherman"
+	"github.com/stretchr/testify/require"
 
 	"gitlab.com/elixxir/client/v4/channels"
+	cft "gitlab.com/elixxir/client/v4/channelsFileTransfer"
 	"gitlab.com/elixxir/client/v4/cmix/rounds"
 	cryptoBroadcast "gitlab.com/elixxir/crypto/broadcast"
 	cryptoChannel "gitlab.com/elixxir/crypto/channel"
+	"gitlab.com/elixxir/crypto/fileTransfer"
 	"gitlab.com/elixxir/crypto/message"
+	"gitlab.com/elixxir/wasm-utils/storage"
 	"gitlab.com/elixxir/xxdk-wasm/indexedDb/impl"
-	"gitlab.com/elixxir/xxdk-wasm/storage"
 	"gitlab.com/xx_network/crypto/csprng"
 	"gitlab.com/xx_network/primitives/id"
 	"gitlab.com/xx_network/primitives/netTime"
@@ -42,15 +42,14 @@ func TestMain(m *testing.M) {
 	os.Exit(m.Run())
 }
 
-func dummyReceivedMessageCB(uint64, *id.ID, bool)      {}
-func dummyDeletedMessageCB(message.ID)                 {}
-func dummyMutedUserCB(*id.ID, ed25519.PublicKey, bool) {}
+type dummyCbs struct{}
+
+func (c *dummyCbs) EventUpdate(int64, []byte) {}
 
 // Happy path test for receiving, updating, getting, and deleting a File.
 func TestWasmModel_ReceiveFile(t *testing.T) {
 	testString := "TestWasmModel_ReceiveFile"
-	m, err := newWASMModel(testString, nil,
-		dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB)
+	m, err := newWASMModel(testString, nil, &dummyCbs{})
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -137,7 +136,7 @@ func TestWasmModel_GetMessage(t *testing.T) {
 			testMsgId := message.DeriveChannelMessageID(&id.ID{1}, 0, []byte(testString))
 
 			eventModel, err := newWASMModel(testString, c,
-				dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB)
+				&dummyCbs{})
 			if err != nil {
 				t.Fatal(err)
 			}
@@ -167,8 +166,7 @@ func TestWasmModel_DeleteMessage(t *testing.T) {
 	storage.GetLocalStorage().Clear()
 	testString := "TestWasmModel_DeleteMessage"
 	testMsgId := message.DeriveChannelMessageID(&id.ID{1}, 0, []byte(testString))
-	eventModel, err := newWASMModel(testString, nil, dummyReceivedMessageCB,
-		dummyDeletedMessageCB, dummyMutedUserCB)
+	eventModel, err := newWASMModel(testString, nil, &dummyCbs{})
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -225,13 +223,17 @@ func Test_wasmModel_UpdateSentStatus(t *testing.T) {
 			testMsgId := message.DeriveChannelMessageID(
 				&id.ID{1}, 0, []byte(testString))
 			eventModel, err2 := newWASMModel(testString, c,
-				dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB)
+				&dummyCbs{})
 			if err2 != nil {
 				t.Fatal(err)
 			}
 
+			cid, err := id.NewRandomID(csprng.NewSystemRNG(),
+				id.DummyUser.GetType())
+			require.NoError(t, err)
+
 			// Store a test message
-			testMsg := buildMessage([]byte(testString), testMsgId.Bytes(), nil,
+			testMsg := buildMessage(cid.Bytes(), testMsgId.Bytes(), nil,
 				testString, []byte(testString), []byte{8, 6, 7, 5}, 0, 0,
 				netTime.Now(), time.Second, 0, 0, false, false, channels.Sent)
 			uuid, err2 := eventModel.upsertMessage(testMsg)
@@ -292,8 +294,7 @@ func Test_wasmModel_JoinChannel_LeaveChannel(t *testing.T) {
 		}
 		t.Run("Test_wasmModel_JoinChannel_LeaveChannel"+cs, func(t *testing.T) {
 			storage.GetLocalStorage().Clear()
-			eventModel, err2 := newWASMModel("test", c, dummyReceivedMessageCB,
-				dummyDeletedMessageCB, dummyMutedUserCB)
+			eventModel, err2 := newWASMModel("test", c, &dummyCbs{})
 			if err2 != nil {
 				t.Fatal(err2)
 			}
@@ -347,7 +348,7 @@ func Test_wasmModel_UUIDTest(t *testing.T) {
 			storage.GetLocalStorage().Clear()
 			testString := "testHello" + cs
 			eventModel, err2 := newWASMModel(testString, c,
-				dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB)
+				&dummyCbs{})
 			if err2 != nil {
 				t.Fatal(err2)
 			}
@@ -394,7 +395,7 @@ func Test_wasmModel_DuplicateReceives(t *testing.T) {
 		t.Run(testString, func(t *testing.T) {
 			storage.GetLocalStorage().Clear()
 			eventModel, err := newWASMModel(testString, c,
-				dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB)
+				&dummyCbs{})
 			if err != nil {
 				t.Fatal(err)
 			}
@@ -443,7 +444,7 @@ func Test_wasmModel_deleteMsgByChannel(t *testing.T) {
 			totalMessages := 10
 			expectedMessages := 5
 			eventModel, err := newWASMModel(testString, c,
-				dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB)
+				&dummyCbs{})
 			if err != nil {
 				t.Fatal(err)
 			}
@@ -514,7 +515,7 @@ func TestWasmModel_receiveHelper_UniqueIndex(t *testing.T) {
 			storage.GetLocalStorage().Clear()
 			testString := fmt.Sprintf("test_receiveHelper_UniqueIndex_%d", i)
 			eventModel, err := newWASMModel(testString, c,
-				dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB)
+				&dummyCbs{})
 			if err != nil {
 				t.Fatal(err)
 			}
diff --git a/indexedDb/impl/channels/init.go b/indexedDb/impl/channels/init.go
index 363440d541029007a6c167d94f8a1fcd66edf4fd..48f5359f9b61c67b42533a42fa37cec4c3c917e7 100644
--- a/indexedDb/impl/channels/init.go
+++ b/indexedDb/impl/channels/init.go
@@ -10,49 +10,46 @@
 package main
 
 import (
+	"encoding/json"
 	"syscall/js"
 
 	"github.com/hack-pad/go-indexeddb/idb"
 	jww "github.com/spf13/jwalterweatherman"
 
+	"gitlab.com/elixxir/client/v4/bindings"
 	"gitlab.com/elixxir/client/v4/channels"
 	cryptoChannel "gitlab.com/elixxir/crypto/channel"
 	"gitlab.com/elixxir/xxdk-wasm/indexedDb/impl"
-	wChannels "gitlab.com/elixxir/xxdk-wasm/indexedDb/worker/channels"
 )
 
 // currentVersion is the current version of the IndexedDb runtime. Used for
 // migration purposes.
-const currentVersion uint = 2
+const currentVersion uint = 1
 
 // NewWASMEventModel returns a [channels.EventModel] backed by a wasmModel.
 // The name should be a base64 encoding of the users public key. Returns the
 // EventModel based on IndexedDb and the database name as reported by IndexedDb.
 func NewWASMEventModel(databaseName string, encryption cryptoChannel.Cipher,
-	messageReceivedCB wChannels.MessageReceivedCallback,
-	deletedMessageCB wChannels.DeletedMessageCallback,
-	mutedUserCB wChannels.MutedUserCallback) (channels.EventModel, error) {
-	return newWASMModel(databaseName, encryption, messageReceivedCB,
-		deletedMessageCB, mutedUserCB)
+	uiCallbacks bindings.ChannelUICallbacks) (channels.EventModel, error) {
+	return newWASMModel(databaseName, encryption, uiCallbacks)
 }
 
 // newWASMModel creates the given [idb.Database] and returns a wasmModel.
 func newWASMModel(databaseName string, encryption cryptoChannel.Cipher,
-	messageReceivedCB wChannels.MessageReceivedCallback,
-	deletedMessageCB wChannels.DeletedMessageCallback,
-	mutedUserCB wChannels.MutedUserCallback) (*wasmModel, error) {
+	uiCallbacks bindings.ChannelUICallbacks) (*wasmModel, error) {
 	// Attempt to open database object
 	ctx, cancel := impl.NewContext()
 	defer cancel()
 	openRequest, err := idb.Global().Open(ctx, databaseName, currentVersion,
 		func(db *idb.Database, oldVersion, newVersion uint) error {
 			if oldVersion == newVersion {
-				jww.INFO.Printf("IndexDb version is current: v%d", newVersion)
+				jww.INFO.Printf("IndexDb version for %s is current: v%d",
+					databaseName, newVersion)
 				return nil
 			}
 
-			jww.INFO.Printf("IndexDb upgrade required: v%d -> v%d",
-				oldVersion, newVersion)
+			jww.INFO.Printf("IndexDb upgrade required for %s: v%d -> v%d",
+				databaseName, oldVersion, newVersion)
 
 			if oldVersion == 0 && newVersion >= 1 {
 				err := v1Upgrade(db)
@@ -62,14 +59,6 @@ func newWASMModel(databaseName string, encryption cryptoChannel.Cipher,
 				oldVersion = 1
 			}
 
-			if oldVersion == 1 && newVersion >= 2 {
-				err := v2Upgrade(db)
-				if err != nil {
-					return err
-				}
-				oldVersion = 2
-			}
-
 			// if oldVersion == 1 && newVersion >= 2 { v2Upgrade(), oldVersion = 2 }
 			return nil
 		})
@@ -81,16 +70,22 @@ func newWASMModel(databaseName string, encryption cryptoChannel.Cipher,
 	db, err := openRequest.Await(ctx)
 	if err != nil {
 		return nil, err
+	} else if ctx.Err() != nil {
+		return nil, ctx.Err()
 	}
 
 	wrapper := &wasmModel{
-		db:                db,
-		cipher:            encryption,
-		receivedMessageCB: messageReceivedCB,
-		deletedMessageCB:  deletedMessageCB,
-		mutedUserCB:       mutedUserCB,
+		db:     db,
+		cipher: encryption,
+		eventUpdate: func(eventType int64, jsonMarshallable any) {
+			data, err := json.Marshal(jsonMarshallable)
+			if err != nil {
+				jww.FATAL.Panicf("Failed to JSON marshal %T for EventUpdate "+
+					"callback: %+v", jsonMarshallable, err)
+			}
+			uiCallbacks.EventUpdate(eventType, data)
+		},
 	}
-
 	return wrapper, nil
 }
 
@@ -149,15 +144,8 @@ func v1Upgrade(db *idb.Database) error {
 		return err
 	}
 
-	return nil
-}
-
-// v1Upgrade performs the v1 -> v2 database upgrade.
-//
-// This can never be changed without permanently breaking backwards
-// compatibility.
-func v2Upgrade(db *idb.Database) error {
-	_, err := db.CreateObjectStore(fileStoreName, idb.ObjectStoreOptions{
+	// Build File ObjectStore
+	_, err = db.CreateObjectStore(fileStoreName, idb.ObjectStoreOptions{
 		KeyPath:       js.ValueOf(pkeyName),
 		AutoIncrement: false,
 	})
diff --git a/indexedDb/impl/channels/main.go b/indexedDb/impl/channels/main.go
index 5290d0c89b6eedf92a1571aea04ff5b7fbfdc668..b84f13dc205ecd8bfe2466ea9ecb827770f31754 100644
--- a/indexedDb/impl/channels/main.go
+++ b/indexedDb/impl/channels/main.go
@@ -11,32 +11,70 @@ package main
 
 import (
 	"fmt"
+	"os"
+	"syscall/js"
+
+	"github.com/spf13/cobra"
 	jww "github.com/spf13/jwalterweatherman"
+
 	"gitlab.com/elixxir/xxdk-wasm/logging"
-	"gitlab.com/elixxir/xxdk-wasm/wasm"
 	"gitlab.com/elixxir/xxdk-wasm/worker"
-	"syscall/js"
 )
 
 // SEMVER is the current semantic version of the xxDK channels web worker.
 const SEMVER = "0.1.0"
 
-func init() {
-	// Set up Javascript console listener set at level INFO
-	ll := logging.NewJsConsoleLogListener(jww.LevelInfo)
-	logging.AddLogListener(ll.Listen)
-	jww.SetStdoutThreshold(jww.LevelFatal + 1)
-	jww.INFO.Printf("xxDK channels web worker version: v%s", SEMVER)
+func main() {
+	// Set to os.Args because the default is os.Args[1:] and in WASM, args start
+	// at 0, not 1.
+	channelsCmd.SetArgs(os.Args)
+
+	err := channelsCmd.Execute()
+	if err != nil {
+		fmt.Println(err)
+		os.Exit(1)
+	}
 }
 
-func main() {
-	jww.INFO.Print("[WW] Starting xxDK WebAssembly Channels Database Worker.")
+var channelsCmd = &cobra.Command{
+	Use:     "channelsIndexedDbWorker",
+	Short:   "IndexedDb database for channels.",
+	Example: "const go = new Go();\ngo.argv = [\"--logLevel=1\"]",
+	Run: func(cmd *cobra.Command, args []string) {
+		// Start logger first to capture all logging events
+		err := logging.EnableLogging(logLevel, -1, 0, "", "")
+		if err != nil {
+			fmt.Printf("Failed to intialize logging: %+v", err)
+			os.Exit(1)
+		}
 
-	js.Global().Set("LogLevel", js.FuncOf(wasm.LogLevel))
+		jww.INFO.Printf("xxDK channels web worker version: v%s", SEMVER)
 
-	m := &manager{mh: worker.NewThreadManager("ChannelsIndexedDbWorker", true)}
-	m.registerCallbacks()
-	m.mh.SignalReady()
-	<-make(chan bool)
-	fmt.Println("[WW] Closing xxDK WebAssembly Channels Database Worker.")
+		jww.INFO.Print("[WW] Starting xxDK WebAssembly Channels Database Worker.")
+		m := &manager{
+			wtm: worker.NewThreadManager("ChannelsIndexedDbWorker", true),
+		}
+		m.registerCallbacks()
+		m.wtm.SignalReady()
+
+		// Indicate to the Javascript caller that the WASM is ready by resolving
+		// a promise created by the caller.
+		js.Global().Get("onWasmInitialized").Invoke()
+
+		<-make(chan bool)
+		fmt.Println("[WW] Closing xxDK WebAssembly Channels Database Worker.")
+		os.Exit(0)
+	},
+}
+
+var (
+	logLevel jww.Threshold
+)
+
+func init() {
+	// Initialize all startup flags
+	channelsCmd.Flags().IntVarP((*int)(&logLevel), "logLevel", "l", 2,
+		"Sets the log level output when outputting to the Javascript console. "+
+			"0 = TRACE, 1 = DEBUG, 2 = INFO, 3 = WARN, 4 = ERROR, "+
+			"5 = CRITICAL, 6 = FATAL, -1 = disabled.")
 }
diff --git a/indexedDb/impl/dm/callbacks.go b/indexedDb/impl/dm/callbacks.go
index 5fe03874609ddfb4c92a4680494ac6c475568ae3..380f04d9953fdda65c4b0d80a66e7b40a7edc2b1 100644
--- a/indexedDb/impl/dm/callbacks.go
+++ b/indexedDb/impl/dm/callbacks.go
@@ -29,24 +29,24 @@ var zeroUUID = []byte{0, 0, 0, 0, 0, 0, 0, 0}
 // manager handles the event model and the message callbacks, which is used to
 // send information between the event model and the main thread.
 type manager struct {
-	mh    *worker.ThreadManager
+	wtm   *worker.ThreadManager
 	model dm.EventModel
 }
 
 // registerCallbacks registers all the reception callbacks to manage messages
 // from the main thread for the channels.EventModel.
 func (m *manager) registerCallbacks() {
-	m.mh.RegisterCallback(wDm.NewWASMEventModelTag, m.newWASMEventModelCB)
-	m.mh.RegisterCallback(wDm.ReceiveTag, m.receiveCB)
-	m.mh.RegisterCallback(wDm.ReceiveTextTag, m.receiveTextCB)
-	m.mh.RegisterCallback(wDm.ReceiveReplyTag, m.receiveReplyCB)
-	m.mh.RegisterCallback(wDm.ReceiveReactionTag, m.receiveReactionCB)
-	m.mh.RegisterCallback(wDm.UpdateSentStatusTag, m.updateSentStatusCB)
-
-	m.mh.RegisterCallback(wDm.BlockSenderTag, m.blockSenderCB)
-	m.mh.RegisterCallback(wDm.UnblockSenderTag, m.unblockSenderCB)
-	m.mh.RegisterCallback(wDm.GetConversationTag, m.getConversationCB)
-	m.mh.RegisterCallback(wDm.GetConversationsTag, m.getConversationsCB)
+	m.wtm.RegisterCallback(wDm.NewWASMEventModelTag, m.newWASMEventModelCB)
+	m.wtm.RegisterCallback(wDm.ReceiveTag, m.receiveCB)
+	m.wtm.RegisterCallback(wDm.ReceiveTextTag, m.receiveTextCB)
+	m.wtm.RegisterCallback(wDm.ReceiveReplyTag, m.receiveReplyCB)
+	m.wtm.RegisterCallback(wDm.ReceiveReactionTag, m.receiveReactionCB)
+	m.wtm.RegisterCallback(wDm.UpdateSentStatusTag, m.updateSentStatusCB)
+
+	m.wtm.RegisterCallback(wDm.BlockSenderTag, m.blockSenderCB)
+	m.wtm.RegisterCallback(wDm.UnblockSenderTag, m.unblockSenderCB)
+	m.wtm.RegisterCallback(wDm.GetConversationTag, m.getConversationCB)
+	m.wtm.RegisterCallback(wDm.GetConversationsTag, m.getConversationsCB)
 }
 
 // newWASMEventModelCB is the callback for NewWASMEventModel. Returns an empty
@@ -98,7 +98,7 @@ func (m *manager) messageReceivedCallback(uuid uint64, pubKey ed25519.PublicKey,
 	}
 
 	// Send it to the main thread
-	m.mh.SendMessage(wDm.MessageReceivedCallbackTag, data)
+	m.wtm.SendMessage(wDm.MessageReceivedCallbackTag, data)
 }
 
 // receiveCB is the callback for wasmModel.Receive. Returns a UUID of 0 on error
diff --git a/indexedDb/impl/dm/dmIndexedDbWorker.js b/indexedDb/impl/dm/dmIndexedDbWorker.js
index e199a7bb812b9ff119b7f130f41d3bb555247302..8a5fdbf8ad9a02967b408985a0219647003eaf7e 100644
--- a/indexedDb/impl/dm/dmIndexedDbWorker.js
+++ b/indexedDb/impl/dm/dmIndexedDbWorker.js
@@ -7,11 +7,15 @@
 
 importScripts('wasm_exec.js');
 
+const isReady = new Promise((resolve) => {
+    self.onWasmInitialized = resolve;
+});
+
 const go = new Go();
 const binPath = 'xxdk-dmIndexedDkWorker.wasm'
-WebAssembly.instantiateStreaming(fetch(binPath), go.importObject).then((result) => {
+WebAssembly.instantiateStreaming(fetch(binPath), go.importObject).then(async (result) => {
     go.run(result.instance);
-    LogLevel(1);
+    await isReady;
 }).catch((err) => {
     console.error(err);
 });
\ No newline at end of file
diff --git a/indexedDb/impl/dm/implementation.go b/indexedDb/impl/dm/implementation.go
index 068b8d41450b3484b3fa22e05a4f8378b48c778a..a92691878ebc4b6c21cd1d38297aca23e4109920 100644
--- a/indexedDb/impl/dm/implementation.go
+++ b/indexedDb/impl/dm/implementation.go
@@ -13,6 +13,7 @@ import (
 	"bytes"
 	"crypto/ed25519"
 	"encoding/json"
+	"gitlab.com/xx_network/primitives/netTime"
 	"strings"
 	"syscall/js"
 	"time"
@@ -25,8 +26,8 @@ import (
 	"gitlab.com/elixxir/client/v4/dm"
 	cryptoChannel "gitlab.com/elixxir/crypto/channel"
 	"gitlab.com/elixxir/crypto/message"
+	"gitlab.com/elixxir/wasm-utils/utils"
 	"gitlab.com/elixxir/xxdk-wasm/indexedDb/impl"
-	"gitlab.com/elixxir/xxdk-wasm/utils"
 	"gitlab.com/xx_network/primitives/id"
 )
 
@@ -42,16 +43,16 @@ type wasmModel struct {
 // upsertConversation is used for joining or updating a Conversation.
 func (w *wasmModel) upsertConversation(nickname string,
 	pubKey ed25519.PublicKey, partnerToken uint32, codeset uint8,
-	blocked bool) error {
+	blockedTimestamp *time.Time) error {
 	parentErr := errors.New("[DM indexedDB] failed to upsertConversation")
 
 	// Build object
 	newConvo := Conversation{
-		Pubkey:         pubKey,
-		Nickname:       nickname,
-		Token:          partnerToken,
-		CodesetVersion: codeset,
-		Blocked:        blocked,
+		Pubkey:           pubKey,
+		Nickname:         nickname,
+		Token:            partnerToken,
+		CodesetVersion:   codeset,
+		BlockedTimestamp: blockedTimestamp,
 	}
 
 	// Convert to jsObject
@@ -231,11 +232,11 @@ func (w *wasmModel) receiveWrapper(messageID message.ID, parentID *message.ID, n
 				"[DM indexedDB] Joining conversation with %s", nickname)
 
 			convoToUpdate = &Conversation{
-				Pubkey:         partnerKey,
-				Nickname:       nickname,
-				Token:          partnerToken,
-				CodesetVersion: codeset,
-				Blocked:        false,
+				Pubkey:           partnerKey,
+				Nickname:         nickname,
+				Token:            partnerToken,
+				CodesetVersion:   codeset,
+				BlockedTimestamp: nil,
 			}
 		}
 	} else {
@@ -268,7 +269,7 @@ func (w *wasmModel) receiveWrapper(messageID message.ID, parentID *message.ID, n
 	conversationUpdated := convoToUpdate != nil
 	if conversationUpdated {
 		err = w.upsertConversation(convoToUpdate.Nickname, convoToUpdate.Pubkey,
-			convoToUpdate.Token, convoToUpdate.CodesetVersion, convoToUpdate.Blocked)
+			convoToUpdate.Token, convoToUpdate.CodesetVersion, convoToUpdate.BlockedTimestamp)
 		if err != nil {
 			return 0, err
 		}
@@ -318,7 +319,8 @@ func (w *wasmModel) upsertMessage(msg *Message) (uint64, error) {
 	// Store message to database
 	msgIdObj, err := impl.Put(w.db, messageStoreName, messageObj)
 	if err != nil {
-		return 0, errors.Errorf("Unable to put Message: %+v", err)
+		return 0, errors.Errorf("Unable to put Message: %+v\n%s",
+			err, newMessageJson)
 	}
 
 	uuid := msgIdObj.Int()
@@ -348,14 +350,20 @@ func (w *wasmModel) UnblockSender(senderPubKey ed25519.PublicKey) {
 
 // setBlocked is a helper for blocking/unblocking a given Conversation.
 func (w *wasmModel) setBlocked(senderPubKey ed25519.PublicKey, isBlocked bool) error {
-	// Get current Conversation and set blocked
+	// Get current Conversation and set blocked accordingly
 	resultConvo, err := w.getConversation(senderPubKey)
 	if err != nil {
 		return err
 	}
 
+	var timeBlocked *time.Time
+	if isBlocked {
+		blockUser := netTime.Now()
+		timeBlocked = &blockUser
+	}
+
 	return w.upsertConversation(resultConvo.Nickname, resultConvo.Pubkey,
-		resultConvo.Token, resultConvo.CodesetVersion, isBlocked)
+		resultConvo.Token, resultConvo.CodesetVersion, timeBlocked)
 }
 
 // GetConversation returns the conversation held by the model (receiver).
@@ -368,11 +376,11 @@ func (w *wasmModel) GetConversation(senderPubKey ed25519.PublicKey) *dm.ModelCon
 	}
 
 	return &dm.ModelConversation{
-		Pubkey:         resultConvo.Pubkey,
-		Nickname:       resultConvo.Nickname,
-		Token:          resultConvo.Token,
-		CodesetVersion: resultConvo.CodesetVersion,
-		Blocked:        resultConvo.Blocked,
+		Pubkey:           resultConvo.Pubkey,
+		Nickname:         resultConvo.Nickname,
+		Token:            resultConvo.Token,
+		CodesetVersion:   resultConvo.CodesetVersion,
+		BlockedTimestamp: resultConvo.BlockedTimestamp,
 	}
 }
 
@@ -410,11 +418,11 @@ func (w *wasmModel) GetConversations() []dm.ModelConversation {
 			return nil
 		}
 		conversations[i] = dm.ModelConversation{
-			Pubkey:         resultConvo.Pubkey,
-			Nickname:       resultConvo.Nickname,
-			Token:          resultConvo.Token,
-			CodesetVersion: resultConvo.CodesetVersion,
-			Blocked:        resultConvo.Blocked,
+			Pubkey:           resultConvo.Pubkey,
+			Nickname:         resultConvo.Nickname,
+			Token:            resultConvo.Token,
+			CodesetVersion:   resultConvo.CodesetVersion,
+			BlockedTimestamp: resultConvo.BlockedTimestamp,
 		}
 	}
 	return conversations
diff --git a/indexedDb/impl/dm/implementation_test.go b/indexedDb/impl/dm/implementation_test.go
index 8e05e6af5fff85af79ea0f15be9dc1e2f5a77c12..c8f42c3871bcfc841683e93b57e2ed4105ab000b 100644
--- a/indexedDb/impl/dm/implementation_test.go
+++ b/indexedDb/impl/dm/implementation_test.go
@@ -17,8 +17,8 @@ import (
 	"gitlab.com/elixxir/client/v4/cmix/rounds"
 	"gitlab.com/elixxir/client/v4/dm"
 	"gitlab.com/elixxir/crypto/message"
+	"gitlab.com/elixxir/wasm-utils/utils"
 	"gitlab.com/elixxir/xxdk-wasm/indexedDb/impl"
-	"gitlab.com/elixxir/xxdk-wasm/utils"
 	"gitlab.com/xx_network/primitives/id"
 	"os"
 	"syscall/js"
@@ -102,7 +102,7 @@ func TestImpl_GetConversations(t *testing.T) {
 		testBytes := []byte(fmt.Sprintf("%d", i))
 		testPubKey := ed25519.PublicKey(testBytes)
 		err = m.upsertConversation("test", testPubKey,
-			uint32(i), uint8(i), false)
+			uint32(i), uint8(i), nil)
 		if err != nil {
 			t.Fatal(err.Error())
 		}
@@ -133,28 +133,28 @@ func TestWasmModel_BlockSender(t *testing.T) {
 
 	// Insert a test convo
 	testPubKey := ed25519.PublicKey{}
-	err = m.upsertConversation("test", testPubKey, 0, 0, false)
+	err = m.upsertConversation("test", testPubKey, 0, 0, nil)
 	if err != nil {
 		t.Fatal(err.Error())
 	}
 
 	// Default to unblocked
 	result := m.GetConversation(testPubKey)
-	if result.Blocked {
+	if result.BlockedTimestamp != nil {
 		t.Fatal("Expected blocked to be false")
 	}
 
 	// Now toggle blocked
 	m.BlockSender(testPubKey)
 	result = m.GetConversation(testPubKey)
-	if !result.Blocked {
+	if result.BlockedTimestamp == nil {
 		t.Fatal("Expected blocked to be true")
 	}
 
 	// Now toggle blocked again
 	m.UnblockSender(testPubKey)
 	result = m.GetConversation(testPubKey)
-	if result.Blocked {
+	if result.BlockedTimestamp != nil {
 		t.Fatal("Expected blocked to be false")
 	}
 }
diff --git a/indexedDb/impl/dm/init.go b/indexedDb/impl/dm/init.go
index b9bad462303b8000c33713f08170f69ac64f39c2..8332866b95055e7d322308cdf8669d1be78a95ac 100644
--- a/indexedDb/impl/dm/init.go
+++ b/indexedDb/impl/dm/init.go
@@ -49,12 +49,13 @@ func newWASMModel(databaseName string, encryption cryptoChannel.Cipher,
 	openRequest, err := idb.Global().Open(ctx, databaseName, currentVersion,
 		func(db *idb.Database, oldVersion, newVersion uint) error {
 			if oldVersion == newVersion {
-				jww.INFO.Printf("IndexDb version is current: v%d", newVersion)
+				jww.INFO.Printf("IndexDb version for %s is current: v%d",
+					databaseName, newVersion)
 				return nil
 			}
 
-			jww.INFO.Printf("IndexDb upgrade required: v%d -> v%d",
-				oldVersion, newVersion)
+			jww.INFO.Printf("IndexDb upgrade required for %s: v%d -> v%d",
+				databaseName, oldVersion, newVersion)
 
 			if oldVersion == 0 && newVersion >= 1 {
 				err := v1Upgrade(db)
@@ -75,10 +76,11 @@ func newWASMModel(databaseName string, encryption cryptoChannel.Cipher,
 	db, err := openRequest.Await(ctx)
 	if err != nil {
 		return nil, err
+	} else if ctx.Err() != nil {
+		return nil, ctx.Err()
 	}
 
 	wrapper := &wasmModel{db: db, receivedMessageCB: cb, cipher: encryption}
-
 	return wrapper, nil
 }
 
diff --git a/indexedDb/impl/dm/main.go b/indexedDb/impl/dm/main.go
index 20b20a0856c78ac753798c9fd4a692e4a88e4852..96fae8e6fbdbce3767739891d4d7ea466e08149c 100644
--- a/indexedDb/impl/dm/main.go
+++ b/indexedDb/impl/dm/main.go
@@ -11,32 +11,71 @@ package main
 
 import (
 	"fmt"
+	"os"
+	"syscall/js"
+
+	"github.com/spf13/cobra"
 	jww "github.com/spf13/jwalterweatherman"
+
 	"gitlab.com/elixxir/xxdk-wasm/logging"
-	"gitlab.com/elixxir/xxdk-wasm/wasm"
 	"gitlab.com/elixxir/xxdk-wasm/worker"
-	"syscall/js"
 )
 
 // SEMVER is the current semantic version of the xxDK DM web worker.
 const SEMVER = "0.1.0"
 
-func init() {
-	// Set up Javascript console listener set at level INFO
-	ll := logging.NewJsConsoleLogListener(jww.LevelInfo)
-	logging.AddLogListener(ll.Listen)
-	jww.SetStdoutThreshold(jww.LevelFatal + 1)
-	jww.INFO.Printf("xxDK DM web worker version: v%s", SEMVER)
+func main() {
+	// Set to os.Args because the default is os.Args[1:] and in WASM, args start
+	// at 0, not 1.
+	dmCmd.SetArgs(os.Args)
+
+	err := dmCmd.Execute()
+	if err != nil {
+		fmt.Println(err)
+		os.Exit(1)
+	}
 }
 
-func main() {
-	jww.INFO.Print("[WW] Starting xxDK WebAssembly DM Database Worker.")
+var dmCmd = &cobra.Command{
+	Use:     "dmIndexedDbWorker",
+	Short:   "IndexedDb database for DMs.",
+	Example: "const go = new Go();\ngo.argv = [\"--logLevel=1\"]",
+	Run: func(cmd *cobra.Command, args []string) {
+		// Start logger first to capture all logging events
+		err := logging.EnableLogging(logLevel, -1, 0, "", "")
+		if err != nil {
+			fmt.Printf(
+				"Failed to intialize logging in DM indexedDb worker: %+v", err)
+			os.Exit(1)
+		}
 
-	js.Global().Set("LogLevel", js.FuncOf(wasm.LogLevel))
+		jww.INFO.Printf("xxDK DM web worker version: v%s", SEMVER)
 
-	m := &manager{mh: worker.NewThreadManager("DmIndexedDbWorker", true)}
-	m.registerCallbacks()
-	m.mh.SignalReady()
-	<-make(chan bool)
-	fmt.Println("[WW] Closing xxDK WebAssembly Channels Database Worker.")
+		jww.INFO.Print("[WW] Starting xxDK WebAssembly DM Database Worker.")
+		m := &manager{
+			wtm: worker.NewThreadManager("DmIndexedDbWorker", true),
+		}
+		m.registerCallbacks()
+		m.wtm.SignalReady()
+
+		// Indicate to the Javascript caller that the WASM is ready by resolving
+		// a promise created by the caller.
+		js.Global().Get("onWasmInitialized").Invoke()
+
+		<-make(chan bool)
+		fmt.Println("[WW] Closing xxDK WebAssembly Channels Database Worker.")
+		os.Exit(0)
+	},
+}
+
+var (
+	logLevel jww.Threshold
+)
+
+func init() {
+	// Initialize all startup flags
+	dmCmd.Flags().IntVarP((*int)(&logLevel), "logLevel", "l", 2,
+		"Sets the log level output when outputting to the Javascript console. "+
+			"0 = TRACE, 1 = DEBUG, 2 = INFO, 3 = WARN, 4 = ERROR, "+
+			"5 = CRITICAL, 6 = FATAL, -1 = disabled.")
 }
diff --git a/indexedDb/impl/dm/model.go b/indexedDb/impl/dm/model.go
index dd4fee16205c9736936830bbb598252dc0774e05..774d011fe4987078808febd74a1657839e06dc8a 100644
--- a/indexedDb/impl/dm/model.go
+++ b/indexedDb/impl/dm/model.go
@@ -55,9 +55,9 @@ type Message struct {
 // message exchange between two recipients.
 // A Conversation has many Message.
 type Conversation struct {
-	Pubkey         []byte `json:"pub_key"` // Matches convoPkeyName
-	Nickname       string `json:"nickname"`
-	Token          uint32 `json:"token"`
-	CodesetVersion uint8  `json:"codeset_version"`
-	Blocked        bool   `json:"blocked"`
+	Pubkey           []byte     `json:"pub_key"` // Matches convoPkeyName
+	Nickname         string     `json:"nickname"`
+	Token            uint32     `json:"token"`
+	CodesetVersion   uint8      `json:"codeset_version"`
+	BlockedTimestamp *time.Time `json:"blocked_timestamp"`
 }
diff --git a/indexedDb/impl/state/callbacks.go b/indexedDb/impl/state/callbacks.go
new file mode 100644
index 0000000000000000000000000000000000000000..fbeb674ad08e8ab9c6f0ef95fff05e96d178a7d5
--- /dev/null
+++ b/indexedDb/impl/state/callbacks.go
@@ -0,0 +1,79 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+//go:build js && wasm
+
+package main
+
+import (
+	"encoding/json"
+	"github.com/pkg/errors"
+
+	"gitlab.com/elixxir/client/v4/storage/utility"
+	stateWorker "gitlab.com/elixxir/xxdk-wasm/indexedDb/worker/state"
+	"gitlab.com/elixxir/xxdk-wasm/worker"
+)
+
+// manager handles the message callbacks, which is used to
+// send information between the model and the main thread.
+type manager struct {
+	wtm   *worker.ThreadManager
+	model utility.WebState
+}
+
+// registerCallbacks registers all the reception callbacks to manage messages
+// from the main thread.
+func (m *manager) registerCallbacks() {
+	m.wtm.RegisterCallback(stateWorker.NewStateTag, m.newStateCB)
+	m.wtm.RegisterCallback(stateWorker.SetTag, m.setCB)
+	m.wtm.RegisterCallback(stateWorker.GetTag, m.getCB)
+}
+
+// newStateCB is the callback for NewState. Returns an empty
+// slice on success or an error message on failure.
+func (m *manager) newStateCB(data []byte) ([]byte, error) {
+	var msg stateWorker.NewStateMessage
+	err := json.Unmarshal(data, &msg)
+	if err != nil {
+		return []byte{}, errors.Errorf(
+			"failed to JSON unmarshal %T from main thread: %+v", msg, err)
+	}
+
+	m.model, err = NewState(msg.DatabaseName)
+	if err != nil {
+		return []byte(err.Error()), nil
+	}
+
+	return []byte{}, nil
+}
+
+// setCB is the callback for stateModel.Set.
+// Returns nil on error or the resulting byte data on success.
+func (m *manager) setCB(data []byte) ([]byte, error) {
+	var msg stateWorker.TransferMessage
+	err := json.Unmarshal(data, &msg)
+	if err != nil {
+		return nil, errors.Errorf(
+			"failed to JSON unmarshal %T from main thread: %+v", msg, err)
+	}
+
+	return nil, m.model.Set(msg.Key, msg.Value)
+}
+
+// getCB is the callback for stateModel.Get.
+// Returns nil on error or the resulting byte data on success.
+func (m *manager) getCB(data []byte) ([]byte, error) {
+	key := string(data)
+	result, err := m.model.Get(key)
+	msg := stateWorker.TransferMessage{
+		Key:   key,
+		Value: result,
+		Error: err.Error(),
+	}
+
+	return json.Marshal(msg)
+}
diff --git a/indexedDb/impl/state/implementation.go b/indexedDb/impl/state/implementation.go
new file mode 100644
index 0000000000000000000000000000000000000000..f580eff52c86978782226d9a5adb305d33d0013c
--- /dev/null
+++ b/indexedDb/impl/state/implementation.go
@@ -0,0 +1,66 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+//go:build js && wasm
+
+package main
+
+import (
+	"encoding/json"
+	"github.com/hack-pad/go-indexeddb/idb"
+	"github.com/pkg/errors"
+	"gitlab.com/elixxir/wasm-utils/utils"
+	"gitlab.com/elixxir/xxdk-wasm/indexedDb/impl"
+	"syscall/js"
+)
+
+// stateModel implements [ClientState] interface backed by IndexedDb.
+// NOTE: This model is NOT thread safe - it is the responsibility of the
+// caller to ensure that its methods are called sequentially.
+type stateModel struct {
+	db *idb.Database
+}
+
+func (s *stateModel) Get(key string) ([]byte, error) {
+	result, err := impl.Get(s.db, stateStoreName, js.ValueOf(key))
+	if err != nil {
+		return nil, err
+	}
+
+	stateObj := &State{}
+	err = json.Unmarshal([]byte(utils.JsToJson(result)), stateObj)
+	if err != nil {
+		return nil, err
+	}
+
+	return stateObj.Value, err
+}
+
+func (s *stateModel) Set(key string, value []byte) error {
+	state := &State{
+		Id:    key,
+		Value: value,
+	}
+
+	// Convert to jsObject
+	newStateJSON, err := json.Marshal(state)
+	if err != nil {
+		return errors.Errorf("Unable to marshal State: %+v", err)
+	}
+	stateObj, err := utils.JsonToJS(newStateJSON)
+	if err != nil {
+		return errors.Errorf("Unable to marshal State: %+v", err)
+	}
+
+	// Store State to database
+	_, err = impl.Put(s.db, stateStoreName, stateObj)
+	if err != nil {
+		return errors.Errorf("Unable to put State: %+v\n%s",
+			err, newStateJSON)
+	}
+	return nil
+}
diff --git a/indexedDb/impl/state/init.go b/indexedDb/impl/state/init.go
new file mode 100644
index 0000000000000000000000000000000000000000..e5ba9f9add4bd0f55c092a8ddd40ce97d936ebf0
--- /dev/null
+++ b/indexedDb/impl/state/init.go
@@ -0,0 +1,84 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+//go:build js && wasm
+
+package main
+
+import (
+	"github.com/hack-pad/go-indexeddb/idb"
+	jww "github.com/spf13/jwalterweatherman"
+	"gitlab.com/elixxir/client/v4/storage/utility"
+	"gitlab.com/elixxir/xxdk-wasm/indexedDb/impl"
+	"syscall/js"
+)
+
+// currentVersion is the current version of the IndexedDb runtime. Used for
+// migration purposes.
+const currentVersion uint = 1
+
+// NewState returns a [utility.WebState] backed by IndexedDb.
+// The name should be a base64 encoding of the users public key.
+func NewState(databaseName string) (utility.WebState, error) {
+	return newState(databaseName)
+}
+
+// newState creates the given [idb.Database] and returns a stateModel.
+func newState(databaseName string) (*stateModel, error) {
+	// Attempt to open database object
+	ctx, cancel := impl.NewContext()
+	defer cancel()
+	openRequest, err := idb.Global().Open(ctx, databaseName, currentVersion,
+		func(db *idb.Database, oldVersion, newVersion uint) error {
+			if oldVersion == newVersion {
+				jww.INFO.Printf("IndexDb version for %s is current: v%d",
+					databaseName, newVersion)
+				return nil
+			}
+
+			jww.INFO.Printf("IndexDb upgrade required for %s: v%d -> v%d",
+				databaseName, oldVersion, newVersion)
+
+			if oldVersion == 0 && newVersion >= 1 {
+				err := v1Upgrade(db)
+				if err != nil {
+					return err
+				}
+				oldVersion = 1
+			}
+
+			// if oldVersion == 1 && newVersion >= 2 { v2Upgrade(), oldVersion = 2 }
+			return nil
+		})
+	if err != nil {
+		return nil, err
+	}
+
+	// Wait for database open to finish
+	db, err := openRequest.Await(ctx)
+	if err != nil {
+		return nil, err
+	} else if ctx.Err() != nil {
+		return nil, ctx.Err()
+	}
+
+	wrapper := &stateModel{db: db}
+	return wrapper, nil
+}
+
+// v1Upgrade performs the v0 -> v1 database upgrade.
+//
+// This can never be changed without permanently breaking backwards
+// compatibility.
+func v1Upgrade(db *idb.Database) error {
+	storeOpts := idb.ObjectStoreOptions{
+		KeyPath:       js.ValueOf(pkeyName),
+		AutoIncrement: false,
+	}
+	_, err := db.CreateObjectStore(stateStoreName, storeOpts)
+	return err
+}
diff --git a/indexedDb/impl/state/main.go b/indexedDb/impl/state/main.go
new file mode 100644
index 0000000000000000000000000000000000000000..719b9969852c2ba946d6365367050878b0dd7b81
--- /dev/null
+++ b/indexedDb/impl/state/main.go
@@ -0,0 +1,80 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+//go:build js && wasm
+
+package main
+
+import (
+	"fmt"
+	"os"
+	"syscall/js"
+
+	"github.com/spf13/cobra"
+	jww "github.com/spf13/jwalterweatherman"
+
+	"gitlab.com/elixxir/xxdk-wasm/logging"
+	"gitlab.com/elixxir/xxdk-wasm/worker"
+)
+
+// SEMVER is the current semantic version of the xxDK web worker.
+const SEMVER = "0.1.0"
+
+func main() {
+	// Set to os.Args because the default is os.Args[1:] and in WASM, args start
+	// at 0, not 1.
+	channelsCmd.SetArgs(os.Args)
+
+	err := channelsCmd.Execute()
+	if err != nil {
+		fmt.Println(err)
+		os.Exit(1)
+	}
+}
+
+var channelsCmd = &cobra.Command{
+	Use:     "stateIndexedDbWorker",
+	Short:   "IndexedDb database for state.",
+	Example: "const go = new Go();\ngo.argv = [\"--logLevel=1\"]",
+	Run: func(cmd *cobra.Command, args []string) {
+		// Start logger first to capture all logging events
+		err := logging.EnableLogging(logLevel, -1, 0, "", "")
+		if err != nil {
+			fmt.Printf("Failed to intialize logging: %+v", err)
+			os.Exit(1)
+		}
+
+		jww.INFO.Printf("xxDK state web worker version: v%s", SEMVER)
+
+		jww.INFO.Print("[WW] Starting xxDK WebAssembly State Database Worker.")
+		m := &manager{
+			wtm: worker.NewThreadManager("StateIndexedDbWorker", true),
+		}
+		m.registerCallbacks()
+		m.wtm.SignalReady()
+
+		// Indicate to the Javascript caller that the WASM is ready by resolving
+		// a promise created by the caller.
+		js.Global().Get("onWasmInitialized").Invoke()
+
+		<-make(chan bool)
+		fmt.Println("[WW] Closing xxDK WebAssembly State Database Worker.")
+		os.Exit(0)
+	},
+}
+
+var (
+	logLevel jww.Threshold
+)
+
+func init() {
+	// Initialize all startup flags
+	channelsCmd.Flags().IntVarP((*int)(&logLevel), "logLevel", "l", 2,
+		"Sets the log level output when outputting to the Javascript console. "+
+			"0 = TRACE, 1 = DEBUG, 2 = INFO, 3 = WARN, 4 = ERROR, "+
+			"5 = CRITICAL, 6 = FATAL, -1 = disabled.")
+}
diff --git a/indexedDb/impl/state/model.go b/indexedDb/impl/state/model.go
new file mode 100644
index 0000000000000000000000000000000000000000..5b8638a5ff832cdc028d5dfb7a103faeb9118749
--- /dev/null
+++ b/indexedDb/impl/state/model.go
@@ -0,0 +1,27 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+//go:build js && wasm
+
+package main
+
+const (
+	// Text representation of primary key value (keyPath).
+	pkeyName = "id"
+
+	// Text representation of the names of the various [idb.ObjectStore].
+	stateStoreName = "states"
+)
+
+// State defines the IndexedDb representation of a single KV data store.
+type State struct {
+	// Id is a unique identifier for a given State.
+	Id string `json:"id"` // Matches pkeyName
+
+	// Value stores the data contents of the State.
+	Value []byte `json:"value"`
+}
diff --git a/indexedDb/impl/state/stateIndexedDbWorker.js b/indexedDb/impl/state/stateIndexedDbWorker.js
new file mode 100644
index 0000000000000000000000000000000000000000..a7c440d2a862a81737d75418ef39e5464c99ad90
--- /dev/null
+++ b/indexedDb/impl/state/stateIndexedDbWorker.js
@@ -0,0 +1,21 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+importScripts('wasm_exec.js');
+
+const isReady = new Promise((resolve) => {
+    self.onWasmInitialized = resolve;
+});
+
+const go = new Go();
+const binPath = 'xxdk-stateIndexedDkWorker.wasm'
+WebAssembly.instantiateStreaming(fetch(binPath), go.importObject).then(async (result) => {
+    go.run(result.instance);
+    await isReady;
+}).catch((err) => {
+    console.error(err);
+});
\ No newline at end of file
diff --git a/indexedDb/impl/utils.go b/indexedDb/impl/utils.go
index 3765cfce5e01698ca625844b5fe495906985366a..7dbf631c9deee97f3ef79b731832aab142820fbc 100644
--- a/indexedDb/impl/utils.go
+++ b/indexedDb/impl/utils.go
@@ -18,7 +18,7 @@ import (
 	"github.com/hack-pad/go-indexeddb/idb"
 	"github.com/pkg/errors"
 	jww "github.com/spf13/jwalterweatherman"
-	"gitlab.com/elixxir/xxdk-wasm/utils"
+	"gitlab.com/elixxir/wasm-utils/utils"
 	"syscall/js"
 	"time"
 )
@@ -30,9 +30,6 @@ const (
 
 	// ErrDoesNotExist is an error string for got undefined on Get operations.
 	ErrDoesNotExist = "result is undefined"
-
-	// ErrUniqueConstraint is an error string for failed uniqueness inserts.
-	ErrUniqueConstraint = "at least one key does not satisfy the uniqueness requirements"
 )
 
 // NewContext builds a context for indexedDb operations.
@@ -45,6 +42,31 @@ func EncodeBytes(input []byte) js.Value {
 	return js.ValueOf(base64.StdEncoding.EncodeToString(input))
 }
 
+// SendRequest is a wrapper for the request.Await() method providing a timeout.
+func SendRequest(request *idb.Request) (js.Value, error) {
+	ctx, cancel := NewContext()
+	defer cancel()
+	result, err := request.Await(ctx)
+	if err != nil {
+		return js.Undefined(), err
+	} else if ctx.Err() != nil {
+		return js.Undefined(), ctx.Err()
+	}
+	return result, nil
+}
+
+// SendCursorRequest is a wrapper for the cursorRequest.Await() method providing a timeout.
+func SendCursorRequest(cur *idb.CursorWithValueRequest,
+	iterFunc func(cursor *idb.CursorWithValue) error) error {
+	ctx, cancel := NewContext()
+	defer cancel()
+	err := cur.Iter(ctx, iterFunc)
+	if ctx.Err() != nil {
+		return ctx.Err()
+	}
+	return err
+}
+
 // Get is a generic helper for getting values from the given [idb.ObjectStore].
 // Only usable by primary key.
 func Get(db *idb.Database, objectStoreName string, key js.Value) (js.Value, error) {
@@ -62,17 +84,15 @@ func Get(db *idb.Database, objectStoreName string, key js.Value) (js.Value, erro
 			"Unable to get ObjectStore: %+v", err)
 	}
 
-	// Perform the operation
+	// Set up the operation
 	getRequest, err := store.Get(key)
 	if err != nil {
 		return js.Undefined(), errors.WithMessagef(parentErr,
 			"Unable to Get from ObjectStore: %+v", err)
 	}
 
-	// Wait for the operation to return
-	ctx, cancel := NewContext()
-	resultObj, err := getRequest.Await(ctx)
-	cancel()
+	// Perform the operation
+	resultObj, err := SendRequest(getRequest)
 	if err != nil {
 		return js.Undefined(), errors.WithMessagef(parentErr,
 			"Unable to get from ObjectStore: %+v", err)
@@ -103,14 +123,15 @@ func GetAll(db *idb.Database, objectStoreName string) ([]js.Value, error) {
 			"Unable to get ObjectStore: %+v", err)
 	}
 
-	// Perform the operation
-	result := make([]js.Value, 0)
+	// Set up the operation
 	cursorRequest, err := store.OpenCursor(idb.CursorNext)
 	if err != nil {
 		return nil, errors.WithMessagef(parentErr, "Unable to open Cursor: %+v", err)
 	}
-	ctx, cancel := NewContext()
-	err = cursorRequest.Iter(ctx,
+	result := make([]js.Value, 0)
+
+	// Perform the operation
+	err = SendCursorRequest(cursorRequest,
 		func(cursor *idb.CursorWithValue) error {
 			row, err := cursor.Value()
 			if err != nil {
@@ -119,7 +140,6 @@ func GetAll(db *idb.Database, objectStoreName string) ([]js.Value, error) {
 			result = append(result, row)
 			return nil
 		})
-	cancel()
 	if err != nil {
 		return nil, errors.WithMessagef(parentErr, err.Error())
 	}
@@ -150,17 +170,15 @@ func GetIndex(db *idb.Database, objectStoreName,
 			"Unable to get Index: %+v", err)
 	}
 
-	// Perform the operation
+	// Set up the operation
 	getRequest, err := idx.Get(key)
 	if err != nil {
 		return js.Undefined(), errors.WithMessagef(parentErr,
 			"Unable to Get from ObjectStore: %+v", err)
 	}
 
-	// Wait for the operation to return
-	ctx, cancel := NewContext()
-	resultObj, err := getRequest.Await(ctx)
-	cancel()
+	// Perform the operation
+	resultObj, err := SendRequest(getRequest)
 	if err != nil {
 		return js.Undefined(), errors.WithMessagef(parentErr,
 			"Unable to get from ObjectStore: %+v", err)
@@ -189,23 +207,21 @@ func Put(db *idb.Database, objectStoreName string, value js.Value) (js.Value, er
 		return js.Undefined(), errors.Errorf("Unable to get ObjectStore: %+v", err)
 	}
 
-	// Perform the operation
+	// Set up the operation
 	request, err := store.Put(value)
 	if err != nil {
 		return js.Undefined(), errors.Errorf("Unable to Put: %+v", err)
 	}
 
-	// Wait for the operation to return
-	ctx, cancel := NewContext()
-	result, err := request.Await(ctx)
-	cancel()
+	// Perform the operation
+	resultObj, err := SendRequest(request)
 	if err != nil {
 		return js.Undefined(), errors.Errorf("Putting value failed: %+v\n%s",
 			err, utils.JsToJson(value))
 	}
 	jww.DEBUG.Printf("Successfully put value in %s: %s",
 		objectStoreName, utils.JsToJson(value))
-	return result, nil
+	return resultObj, nil
 }
 
 // Delete is a generic helper for removing values from the given
@@ -226,16 +242,14 @@ func Delete(db *idb.Database, objectStoreName string, key js.Value) error {
 	}
 
 	// Perform the operation
-	_, err = store.Delete(key)
+	deleteRequest, err := store.Delete(key)
 	if err != nil {
 		return errors.WithMessagef(parentErr,
 			"Unable to Delete from ObjectStore: %+v", err)
 	}
 
-	// Wait for the operation to return
-	ctx, cancel := NewContext()
-	err = txn.Await(ctx)
-	cancel()
+	// Perform the operation
+	_, err = SendRequest(deleteRequest.Request)
 	if err != nil {
 		return errors.WithMessagef(parentErr,
 			"Unable to Delete from ObjectStore: %+v", err)
@@ -282,17 +296,18 @@ func Dump(db *idb.Database, objectStoreName string) ([]string, error) {
 		return nil, errors.WithMessagef(parentErr,
 			"Unable to get ObjectStore: %+v", err)
 	}
+
+	// Set up the operation
 	cursorRequest, err := store.OpenCursor(idb.CursorNext)
 	if err != nil {
 		return nil, errors.WithMessagef(parentErr,
 			"Unable to open Cursor: %+v", err)
 	}
-
-	// Run the query
 	jww.DEBUG.Printf("%s values:", objectStoreName)
 	results := make([]string, 0)
-	ctx, cancel := NewContext()
-	err = cursorRequest.Iter(ctx,
+
+	// Perform the operation
+	err = SendCursorRequest(cursorRequest,
 		func(cursor *idb.CursorWithValue) error {
 			value, err := cursor.Value()
 			if err != nil {
@@ -303,7 +318,6 @@ func Dump(db *idb.Database, objectStoreName string) ([]string, error) {
 			jww.DEBUG.Printf("- %v", valueStr)
 			return nil
 		})
-	cancel()
 	if err != nil {
 		return nil, errors.WithMessagef(parentErr,
 			"Unable to dump ObjectStore: %+v", err)
diff --git a/indexedDb/impl/utils_test.go b/indexedDb/impl/utils_test.go
index 00e235834c44788905af73cafd7448961708a4dc..ba6700356fb5e434ab5e8850373c9bb6a2775116 100644
--- a/indexedDb/impl/utils_test.go
+++ b/indexedDb/impl/utils_test.go
@@ -11,9 +11,11 @@ package impl
 
 import (
 	"github.com/hack-pad/go-indexeddb/idb"
+	jww "github.com/spf13/jwalterweatherman"
 	"strings"
 	"syscall/js"
 	"testing"
+	"time"
 )
 
 // Error path: Tests that Get returns an error when trying to get a message that
@@ -92,3 +94,52 @@ func newTestDB(name, index string, t *testing.T) *idb.Database {
 
 	return db
 }
+
+// TestBenchmark ensures IndexedDb can take at least n operations per second.
+func TestBenchmark(t *testing.T) {
+	jww.SetStdoutThreshold(jww.LevelInfo)
+	benchmarkDb(50, t)
+}
+
+// benchmarkDb sends n operations to IndexedDb and prints errors.
+func benchmarkDb(n int, t *testing.T) {
+	jww.INFO.Printf("Benchmarking IndexedDb: %d total.", n)
+
+	objectStoreName := "test"
+	testValue := js.ValueOf(make(map[string]interface{}))
+	db := newTestDB(objectStoreName, "index", t)
+
+	type metric struct {
+		didSucceed bool
+		duration   time.Duration
+	}
+	done := make(chan metric)
+
+	// Spawn n operations at the same time
+	startTime := time.Now()
+	for i := 0; i < n; i++ {
+		go func() {
+			opStart := time.Now()
+			_, err := Put(db, objectStoreName, testValue)
+			done <- metric{
+				didSucceed: err == nil,
+				duration:   time.Since(opStart),
+			}
+		}()
+	}
+
+	// Wait for all to complete
+	didSucceed := true
+	for i := 0; i < n; i++ {
+		result := <-done
+		if !result.didSucceed {
+			didSucceed = false
+		}
+		jww.DEBUG.Printf("Operation time: %s", result.duration)
+	}
+
+	timeElapsed := time.Since(startTime)
+	jww.INFO.Printf("Benchmarking complete. Succeeded: %t\n"+
+		"Took %s, Average of %s.",
+		didSucceed, timeElapsed, timeElapsed/time.Duration(n))
+}
diff --git a/indexedDb/worker/channels/implementation.go b/indexedDb/worker/channels/implementation.go
index 9639bc185095e74cbf4b3e63256fead39bbcef82..2e11702432dd5d4b1dae668715ccb6a7bdc50950 100644
--- a/indexedDb/worker/channels/implementation.go
+++ b/indexedDb/worker/channels/implementation.go
@@ -452,16 +452,16 @@ func (w *wasmModel) DeleteMessage(messageID message.ID) error {
 // MuteUserMessage is JSON marshalled and sent to the worker for
 // [wasmModel.MuteUser].
 type MuteUserMessage struct {
-	ChannelID *id.ID            `json:"channelID"`
-	PubKey    ed25519.PublicKey `json:"pubKey"`
-	Unmute    bool              `json:"unmute"`
+	ChannelID []byte `json:"channelID"`
+	PubKey    []byte `json:"pubKey"`
+	Unmute    bool   `json:"unmute"`
 }
 
 // MuteUser is called whenever a user is muted or unmuted.
 func (w *wasmModel) MuteUser(
 	channelID *id.ID, pubKey ed25519.PublicKey, unmute bool) {
 	msg := MuteUserMessage{
-		ChannelID: channelID,
+		ChannelID: channelID.Marshal(),
 		PubKey:    pubKey,
 		Unmute:    unmute,
 	}
diff --git a/indexedDb/worker/channels/init.go b/indexedDb/worker/channels/init.go
index 2ee630caf43177460a19f14d2b953d3a03b9c687..d24120aa445d6c161bf148a242439f4a6bf05eb1 100644
--- a/indexedDb/worker/channels/init.go
+++ b/indexedDb/worker/channels/init.go
@@ -10,47 +10,37 @@
 package channels
 
 import (
-	"crypto/ed25519"
 	"encoding/json"
-	"github.com/pkg/errors"
 	"time"
 
+	"github.com/pkg/errors"
+
 	jww "github.com/spf13/jwalterweatherman"
+	"gitlab.com/elixxir/client/v4/bindings"
 	"gitlab.com/elixxir/client/v4/channels"
 
 	cryptoChannel "gitlab.com/elixxir/crypto/channel"
-	"gitlab.com/elixxir/crypto/message"
 	"gitlab.com/elixxir/xxdk-wasm/storage"
 	"gitlab.com/elixxir/xxdk-wasm/worker"
-	"gitlab.com/xx_network/primitives/id"
 )
 
 // databaseSuffix is the suffix to be appended to the name of the database.
 const databaseSuffix = "_speakeasy"
 
-// MessageReceivedCallback is called any time a message is received or updated.
-//
-// update is true if the row is old and was edited.
-type MessageReceivedCallback func(uuid uint64, channelID *id.ID, update bool)
-
-// DeletedMessageCallback is called any time a message is deleted.
-type DeletedMessageCallback func(messageID message.ID)
-
-// MutedUserCallback is called any time a user is muted or unmuted. unmute is
-// true if the user has been unmuted and false if they have been muted.
-type MutedUserCallback func(
-	channelID *id.ID, pubKey ed25519.PublicKey, unmute bool)
+// eventUpdateCallback is the [bindings.ChannelUICallback] callback function
+// it has a type ([bindings.NickNameUpdate] to [bindings.MessageDeleted]
+// and json data that is the callback information.
+type eventUpdateCallback func(eventType int64, jsonData []byte)
 
 // NewWASMEventModelBuilder returns an EventModelBuilder which allows
 // the channel manager to define the path but the callback is the same
 // across the board.
 func NewWASMEventModelBuilder(wasmJsPath string,
-	encryption cryptoChannel.Cipher, messageReceivedCB MessageReceivedCallback,
-	deletedMessageCB DeletedMessageCallback,
-	mutedUserCB MutedUserCallback) channels.EventModelBuilder {
+	encryption cryptoChannel.Cipher,
+	channelCbs bindings.ChannelUICallbacks) channels.EventModelBuilder {
 	fn := func(path string) (channels.EventModel, error) {
 		return NewWASMEventModel(path, wasmJsPath, encryption,
-			messageReceivedCB, deletedMessageCB, mutedUserCB)
+			channelCbs)
 	}
 	return fn
 }
@@ -65,8 +55,7 @@ type NewWASMEventModelMessage struct {
 // NewWASMEventModel returns a [channels.EventModel] backed by a wasmModel.
 // The name should be a base64 encoding of the users public key.
 func NewWASMEventModel(path, wasmJsPath string, encryption cryptoChannel.Cipher,
-	messageReceivedCB MessageReceivedCallback,
-	deletedMessageCB DeletedMessageCallback, mutedUserCB MutedUserCallback) (
+	channelCbs bindings.ChannelUICallbacks) (
 	channels.EventModel, error) {
 	databaseName := path + databaseSuffix
 
@@ -75,17 +64,9 @@ func NewWASMEventModel(path, wasmJsPath string, encryption cryptoChannel.Cipher,
 		return nil, err
 	}
 
-	// Register handler to manage messages for the MessageReceivedCallback
-	wm.RegisterCallback(MessageReceivedCallbackTag,
-		messageReceivedCallbackHandler(messageReceivedCB))
-
-	// Register handler to manage messages for the DeletedMessageCallback
-	wm.RegisterCallback(DeletedMessageCallbackTag,
-		deletedMessageCallbackHandler(deletedMessageCB))
-
-	// Register handler to manage messages for the MutedUserCallback
-	wm.RegisterCallback(MutedUserCallbackTag,
-		mutedUserCallbackHandler(mutedUserCB))
+	// Register handler to manage messages for the EventUpdate
+	wm.RegisterCallback(EventUpdateCallbackTag,
+		messageReceivedCallbackHandler(channelCbs.EventUpdate))
 
 	// Store the database name
 	err = storage.StoreIndexedDb(databaseName)
@@ -132,49 +113,18 @@ func NewWASMEventModel(path, wasmJsPath string, encryption cryptoChannel.Cipher,
 	return &wasmModel{wm}, nil
 }
 
-// MessageReceivedCallbackMessage is JSON marshalled and received from the
-// worker for the [MessageReceivedCallback] callback.
-type MessageReceivedCallbackMessage struct {
-	UUID      uint64 `json:"uuid"`
-	ChannelID *id.ID `json:"channelID"`
-	Update    bool   `json:"update"`
+// EventUpdateCallbackMessage is JSON marshalled and received from the worker
+// for the [EventUpdate] callback.
+type EventUpdateCallbackMessage struct {
+	EventType int64  `json:"eventType"`
+	JsonData  []byte `json:"jsonData"`
 }
 
 // messageReceivedCallbackHandler returns a handler to manage messages for the
 // MessageReceivedCallback.
-func messageReceivedCallbackHandler(cb MessageReceivedCallback) func(data []byte) {
-	return func(data []byte) {
-		var msg MessageReceivedCallbackMessage
-		err := json.Unmarshal(data, &msg)
-		if err != nil {
-			jww.ERROR.Printf(
-				"Failed to JSON unmarshal %T from worker: %+v", msg, err)
-			return
-		}
-
-		cb(msg.UUID, msg.ChannelID, msg.Update)
-	}
-}
-
-// deletedMessageCallbackHandler returns a handler to manage messages for the
-// DeletedMessageCallback.
-func deletedMessageCallbackHandler(cb DeletedMessageCallback) func(data []byte) {
-	return func(data []byte) {
-		messageID, err := message.UnmarshalID(data)
-		if err != nil {
-			jww.ERROR.Printf(
-				"Failed to JSON unmarshal message ID from worker: %+v", err)
-		}
-
-		cb(messageID)
-	}
-}
-
-// mutedUserCallbackHandler returns a handler to manage messages for the
-// MutedUserCallback.
-func mutedUserCallbackHandler(cb MutedUserCallback) func(data []byte) {
+func messageReceivedCallbackHandler(cb eventUpdateCallback) func(data []byte) {
 	return func(data []byte) {
-		var msg MuteUserMessage
+		var msg EventUpdateCallbackMessage
 		err := json.Unmarshal(data, &msg)
 		if err != nil {
 			jww.ERROR.Printf(
@@ -182,7 +132,7 @@ func mutedUserCallbackHandler(cb MutedUserCallback) func(data []byte) {
 			return
 		}
 
-		cb(msg.ChannelID, msg.PubKey, msg.Unmute)
+		cb(msg.EventType, msg.JsonData)
 	}
 }
 
diff --git a/indexedDb/worker/channels/tags.go b/indexedDb/worker/channels/tags.go
index d3555e549163c18b772cc86f965a2f7aeca7a827..f21c91653547cfe37158d514ef482c468084d800 100644
--- a/indexedDb/worker/channels/tags.go
+++ b/indexedDb/worker/channels/tags.go
@@ -14,10 +14,8 @@ import "gitlab.com/elixxir/xxdk-wasm/worker"
 // List of tags that can be used when sending a message or registering a handler
 // to receive a message.
 const (
-	NewWASMEventModelTag       worker.Tag = "NewWASMEventModel"
-	MessageReceivedCallbackTag worker.Tag = "MessageReceivedCallback"
-	DeletedMessageCallbackTag  worker.Tag = "DeletedMessageCallback"
-	MutedUserCallbackTag       worker.Tag = "MutedUserCallback"
+	NewWASMEventModelTag   worker.Tag = "NewWASMEventModel"
+	EventUpdateCallbackTag worker.Tag = "EventUpdateCallback"
 
 	JoinChannelTag         worker.Tag = "JoinChannel"
 	LeaveChannelTag        worker.Tag = "LeaveChannel"
diff --git a/indexedDb/worker/state/implementation.go b/indexedDb/worker/state/implementation.go
new file mode 100644
index 0000000000000000000000000000000000000000..92a5752ade4aef34f002a3904cc586f047e4c6bb
--- /dev/null
+++ b/indexedDb/worker/state/implementation.go
@@ -0,0 +1,82 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+//go:build js && wasm
+
+package dm
+
+import (
+	"encoding/json"
+	"github.com/pkg/errors"
+	"time"
+
+	"gitlab.com/elixxir/xxdk-wasm/worker"
+)
+
+type wasmModel struct {
+	wh *worker.Manager
+}
+
+// TransferMessage is JSON marshalled and sent to the worker.
+type TransferMessage struct {
+	Key   string `json:"key"`
+	Value []byte `json:"value"`
+	Error string `json:"error"`
+}
+
+func (w *wasmModel) Set(key string, value []byte) error {
+	msg := TransferMessage{
+		Key:   key,
+		Value: value,
+	}
+
+	data, err := json.Marshal(msg)
+	if err != nil {
+		return errors.Errorf(
+			"Could not JSON marshal payload for TransferMessage: %+v", err)
+	}
+
+	resultChan := make(chan []byte)
+	w.wh.SendMessage(SetTag, data,
+		func(data []byte) {
+			resultChan <- data
+		})
+
+	select {
+	case result := <-resultChan:
+		return errors.New(string(result))
+	case <-time.After(worker.ResponseTimeout):
+		return errors.Errorf("Timed out after %s waiting for response from the "+
+			"worker about Get", worker.ResponseTimeout)
+	}
+}
+
+func (w *wasmModel) Get(key string) ([]byte, error) {
+	resultChan := make(chan []byte)
+	w.wh.SendMessage(GetTag, []byte(key),
+		func(data []byte) {
+			resultChan <- data
+		})
+
+	select {
+	case result := <-resultChan:
+		var msg TransferMessage
+		err := json.Unmarshal(result, &msg)
+		if err != nil {
+			return nil, errors.Errorf(
+				"failed to JSON unmarshal %T from main thread: %+v", msg, err)
+		}
+
+		if len(msg.Error) > 0 {
+			return nil, errors.New(msg.Error)
+		}
+		return msg.Value, nil
+	case <-time.After(worker.ResponseTimeout):
+		return nil, errors.Errorf("Timed out after %s waiting for response from the "+
+			"worker about Get", worker.ResponseTimeout)
+	}
+}
diff --git a/indexedDb/worker/state/init.go b/indexedDb/worker/state/init.go
new file mode 100644
index 0000000000000000000000000000000000000000..b4842ae5ca954e1bfead70a483690b88fbf1974d
--- /dev/null
+++ b/indexedDb/worker/state/init.go
@@ -0,0 +1,72 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+//go:build js && wasm
+
+package dm
+
+import (
+	"encoding/json"
+	"time"
+
+	"github.com/pkg/errors"
+
+	"gitlab.com/elixxir/client/v4/storage/utility"
+	"gitlab.com/elixxir/xxdk-wasm/storage"
+	"gitlab.com/elixxir/xxdk-wasm/worker"
+)
+
+// databaseSuffix is the suffix to be appended to the name of the database.
+const databaseSuffix = "_speakeasy_state"
+
+// NewStateMessage is JSON marshalled and sent to the worker for
+// [NewState].
+type NewStateMessage struct {
+	DatabaseName string `json:"databaseName"`
+}
+
+// NewState returns a [utility.WebState] backed by indexeddb.
+// The name should be a base64 encoding of the users public key.
+func NewState(path, wasmJsPath string) (utility.WebState, error) {
+	databaseName := path + databaseSuffix
+
+	wh, err := worker.NewManager(wasmJsPath, "stateIndexedDb", true)
+	if err != nil {
+		return nil, err
+	}
+
+	// Store the database name
+	err = storage.StoreIndexedDb(databaseName)
+	if err != nil {
+		return nil, err
+	}
+
+	msg := NewStateMessage{
+		DatabaseName: databaseName,
+	}
+
+	payload, err := json.Marshal(msg)
+	if err != nil {
+		return nil, err
+	}
+
+	dataChan := make(chan []byte)
+	wh.SendMessage(NewStateTag, payload,
+		func(data []byte) { dataChan <- data })
+
+	select {
+	case data := <-dataChan:
+		if len(data) > 0 {
+			return nil, errors.New(string(data))
+		}
+	case <-time.After(worker.ResponseTimeout):
+		return nil, errors.Errorf("timed out after %s waiting for indexedDB "+
+			"database in worker to initialize", worker.ResponseTimeout)
+	}
+
+	return &wasmModel{wh}, nil
+}
diff --git a/indexedDb/worker/state/tags.go b/indexedDb/worker/state/tags.go
new file mode 100644
index 0000000000000000000000000000000000000000..0f6327d9f54509735fd6e6a1d7de57e3b8015a04
--- /dev/null
+++ b/indexedDb/worker/state/tags.go
@@ -0,0 +1,20 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+//go:build js && wasm
+
+package dm
+
+import "gitlab.com/elixxir/xxdk-wasm/worker"
+
+// List of tags that can be used when sending a message or registering a handler
+// to receive a message.
+const (
+	NewStateTag worker.Tag = "NewState"
+	SetTag      worker.Tag = "Set"
+	GetTag      worker.Tag = "Get"
+)
diff --git a/logging/fileLogger.go b/logging/fileLogger.go
new file mode 100644
index 0000000000000000000000000000000000000000..831511a81e069b1899b081d40268cf38042cf0b7
--- /dev/null
+++ b/logging/fileLogger.go
@@ -0,0 +1,95 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+//go:build js && wasm
+
+package logging
+
+import (
+	"io"
+	"math"
+
+	"github.com/armon/circbuf"
+	"github.com/pkg/errors"
+	jww "github.com/spf13/jwalterweatherman"
+
+	"gitlab.com/elixxir/xxdk-wasm/worker"
+)
+
+// fileLogger manages the recording of jwalterweatherman logs to the local
+// in-memory file buffer.
+type fileLogger struct {
+	threshold jww.Threshold
+	cb        *circbuf.Buffer
+}
+
+// newFileLogger starts logging to a local, in-memory log file at the specified
+// threshold. Returns a [fileLogger] that can be used to get the log file.
+func newFileLogger(threshold jww.Threshold, maxLogFileSize int) (*fileLogger, error) {
+	b, err := circbuf.NewBuffer(int64(maxLogFileSize))
+	if err != nil {
+		return nil, errors.Wrap(err, "could not create new circular buffer")
+	}
+
+	fl := &fileLogger{
+		threshold: threshold,
+		cb:        b,
+	}
+
+	jww.FEEDBACK.Printf("[LOG] Outputting log to file of max size %d at level %s",
+		b.Size(), fl.threshold)
+
+	logger = fl
+	return fl, nil
+}
+
+// Write adheres to the io.Writer interface and writes log entries to the
+// buffer.
+func (fl *fileLogger) Write(p []byte) (n int, err error) {
+	return fl.cb.Write(p)
+}
+
+// Listen adheres to the [jwalterweatherman.LogListener] type and returns the
+// log writer when the threshold is within the set threshold limit.
+func (fl *fileLogger) Listen(threshold jww.Threshold) io.Writer {
+	if threshold < fl.threshold {
+		return nil
+	}
+	return fl
+}
+
+// StopLogging stops log message writes. Once logging is stopped, it cannot be
+// resumed and the log file cannot be recovered.
+func (fl *fileLogger) StopLogging() {
+	fl.threshold = math.MaxInt
+	fl.cb.Reset()
+}
+
+// GetFile returns the entire log file.
+func (fl *fileLogger) GetFile() []byte {
+	return fl.cb.Bytes()
+}
+
+// Threshold returns the log level threshold used in the file.
+func (fl *fileLogger) Threshold() jww.Threshold {
+	return fl.threshold
+}
+
+// MaxSize returns the max size, in bytes, that the log file is allowed to be.
+func (fl *fileLogger) MaxSize() int {
+	return int(fl.cb.Size())
+}
+
+// Size returns the current size, in bytes, written to the log file.
+func (fl *fileLogger) Size() int {
+	return int(fl.cb.TotalWritten())
+}
+
+// Worker returns nil.
+func (fl *fileLogger) Worker() *worker.Manager {
+	return nil
+}
diff --git a/logging/fileLogger_test.go b/logging/fileLogger_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..317f69544a927280827aa4f3739c7f1e39d30ac4
--- /dev/null
+++ b/logging/fileLogger_test.go
@@ -0,0 +1,228 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+//go:build js && wasm
+
+package logging
+
+import (
+	"bytes"
+	"github.com/armon/circbuf"
+	jww "github.com/spf13/jwalterweatherman"
+	"math/rand"
+	"reflect"
+	"testing"
+)
+
+func Test_newFileLogger(t *testing.T) {
+	expected := &fileLogger{
+		threshold: jww.LevelError,
+	}
+	expected.cb, _ = circbuf.NewBuffer(512)
+	fl, err := newFileLogger(expected.threshold, int(expected.cb.Size()))
+	if err != nil {
+		t.Fatalf("Failed to make new fileLogger: %+v", err)
+	}
+
+	if !reflect.DeepEqual(expected, fl) {
+		t.Errorf("Unexpected new fileLogger.\nexpected: %+v\nreceived: %+v",
+			expected, fl)
+	}
+	if !reflect.DeepEqual(logger, fl) {
+		t.Errorf("Failed to set logger.\nexpected: %+v\nreceived: %+v",
+			logger, fl)
+	}
+
+}
+
+// Tests that fileLogger.Write writes the expected data to the buffer and that
+// when the max file size is reached, old data is replaced.
+func Test_fileLogger_Write(t *testing.T) {
+	rng := rand.New(rand.NewSource(3424))
+	fl, err := newFileLogger(jww.LevelError, 512)
+	if err != nil {
+		t.Fatalf("Failed to make new fileLogger: %+v", err)
+	}
+
+	expected := make([]byte, fl.MaxSize())
+	rng.Read(expected)
+	n, err := fl.Write(expected)
+	if err != nil {
+		t.Fatalf("Failed to write: %+v", err)
+	} else if n != len(expected) {
+		t.Fatalf("Did not write expected length.\nexpected: %d\nreceived: %d",
+			len(expected), n)
+	}
+
+	if !bytes.Equal(fl.cb.Bytes(), expected) {
+		t.Fatalf("Incorrect bytes in buffer.\nexpected: %v\nreceived: %v",
+			expected, fl.cb.Bytes())
+	}
+
+	// Check that the data is overwritten
+	rng.Read(expected)
+	n, err = fl.Write(expected)
+	if err != nil {
+		t.Fatalf("Failed to write: %+v", err)
+	} else if n != len(expected) {
+		t.Fatalf("Did not write expected length.\nexpected: %d\nreceived: %d",
+			len(expected), n)
+	}
+
+	if !bytes.Equal(fl.cb.Bytes(), expected) {
+		t.Fatalf("Incorrect bytes in buffer.\nexpected: %v\nreceived: %v",
+			expected, fl.cb.Bytes())
+	}
+}
+
+// Tests that fileLogger.Listen only returns an io.Writer for valid thresholds.
+func Test_fileLogger_Listen(t *testing.T) {
+	th := jww.LevelError
+	fl, err := newFileLogger(th, 512)
+	if err != nil {
+		t.Fatalf("Failed to make new fileLogger: %+v", err)
+	}
+
+	thresholds := []jww.Threshold{-1, jww.LevelTrace, jww.LevelDebug,
+		jww.LevelFatal, jww.LevelWarn, jww.LevelError, jww.LevelCritical,
+		jww.LevelFatal}
+
+	for _, threshold := range thresholds {
+		w := fl.Listen(threshold)
+		if threshold < th {
+			if w != nil {
+				t.Errorf("Did not receive nil io.Writer for level %s: %+v",
+					threshold, w)
+			}
+		} else if w == nil {
+			t.Errorf("Received nil io.Writer for level %s", threshold)
+		}
+	}
+}
+
+// Tests that fileLogger.Listen always returns nil after fileLogger.StopLogging
+// is called.
+func Test_fileLogger_StopLogging(t *testing.T) {
+	fl, err := newFileLogger(jww.LevelError, 512)
+	if err != nil {
+		t.Fatalf("Failed to make new fileLogger: %+v", err)
+	}
+
+	fl.StopLogging()
+
+	if w := fl.Listen(jww.LevelFatal); w != nil {
+		t.Errorf("Listen returned non-nil io.Writer when logging should have "+
+			"been stopped: %+v", w)
+	}
+
+	file := fl.GetFile()
+	if !bytes.Equal([]byte{}, file) {
+		t.Errorf("Did not receice empty file: %+v", file)
+	}
+}
+
+// Tests that fileLogger.GetFile returns the expected file.
+func Test_fileLogger_GetFile(t *testing.T) {
+	rng := rand.New(rand.NewSource(9863))
+	fl, err := newFileLogger(jww.LevelError, 512)
+	if err != nil {
+		t.Fatalf("Failed to make new fileLogger: %+v", err)
+	}
+
+	var expected []byte
+	for i := 0; i < 5; i++ {
+		p := make([]byte, rng.Intn(64))
+		rng.Read(p)
+		expected = append(expected, p...)
+
+		if _, err = fl.Write(p); err != nil {
+			t.Errorf("Write %d failed: %+v", i, err)
+		}
+	}
+
+	file := fl.GetFile()
+	if !bytes.Equal(expected, file) {
+		t.Errorf("Unexpected file.\nexpected: %v\nreceived: %v", expected, file)
+	}
+}
+
+// Tests that fileLogger.Threshold returns the expected threshold.
+func Test_fileLogger_Threshold(t *testing.T) {
+	thresholds := []jww.Threshold{-1, jww.LevelTrace, jww.LevelDebug,
+		jww.LevelFatal, jww.LevelWarn, jww.LevelError, jww.LevelCritical,
+		jww.LevelFatal}
+
+	for _, threshold := range thresholds {
+		fl, err := newFileLogger(threshold, 512)
+		if err != nil {
+			t.Fatalf("Failed to make new fileLogger: %+v", err)
+		}
+
+		if fl.Threshold() != threshold {
+			t.Errorf("Incorrect threshold.\nexpected: %s (%d)\nreceived: %s (%d)",
+				threshold, threshold, fl.Threshold(), fl.Threshold())
+		}
+	}
+}
+
+// Unit test of fileLogger.MaxSize.
+func Test_fileLogger_MaxSize(t *testing.T) {
+	maxSize := 512
+	fl, err := newFileLogger(jww.LevelError, maxSize)
+	if err != nil {
+		t.Fatalf("Failed to make new fileLogger: %+v", err)
+	}
+
+	if fl.MaxSize() != maxSize {
+		t.Errorf("Incorrect max size.\nexpected: %d\nreceived: %d",
+			maxSize, fl.MaxSize())
+	}
+}
+
+// Unit test of fileLogger.Size.
+func Test_fileLogger_Size(t *testing.T) {
+	rng := rand.New(rand.NewSource(9863))
+	fl, err := newFileLogger(jww.LevelError, 512)
+	if err != nil {
+		t.Fatalf("Failed to make new fileLogger: %+v", err)
+	}
+
+	var expected []byte
+	for i := 0; i < 5; i++ {
+		p := make([]byte, rng.Intn(64))
+		rng.Read(p)
+		expected = append(expected, p...)
+
+		if _, err = fl.Write(p); err != nil {
+			t.Errorf("Write %d failed: %+v", i, err)
+		}
+
+		size := fl.Size()
+		if size != len(expected) {
+			t.Errorf("Incorrect size (%d).\nexpected: %d\nreceived: %d",
+				i, len(expected), size)
+		}
+	}
+
+	file := fl.GetFile()
+	if !bytes.Equal(expected, file) {
+		t.Errorf("Unexpected file.\nexpected: %v\nreceived: %v", expected, file)
+	}
+}
+
+// Tests that fileLogger.Worker always returns nil.
+func Test_fileLogger_Worker(t *testing.T) {
+	fl, err := newFileLogger(jww.LevelError, 512)
+	if err != nil {
+		t.Fatalf("Failed to make new fileLogger: %+v", err)
+	}
+
+	w := fl.Worker()
+	if w != nil {
+		t.Errorf("Did not get nil worker: %+v", w)
+	}
+}
diff --git a/logging/logLevel.go b/logging/logLevel.go
deleted file mode 100644
index 895857475c4c647d7625b43644e6c4f32be98155..0000000000000000000000000000000000000000
--- a/logging/logLevel.go
+++ /dev/null
@@ -1,89 +0,0 @@
-////////////////////////////////////////////////////////////////////////////////
-// Copyright © 2022 xx foundation                                             //
-//                                                                            //
-// Use of this source code is governed by a license that can be found in the  //
-// LICENSE file.                                                              //
-////////////////////////////////////////////////////////////////////////////////
-
-//go:build js && wasm
-
-package logging
-
-import (
-	"fmt"
-	"github.com/pkg/errors"
-	jww "github.com/spf13/jwalterweatherman"
-	"gitlab.com/elixxir/xxdk-wasm/utils"
-	"log"
-	"syscall/js"
-)
-
-// LogLevel sets level of logging. All logs at the set level and below will be
-// displayed (e.g., when log level is ERROR, only ERROR, CRITICAL, and FATAL
-// messages will be printed).
-//
-// The default log level without updates is INFO.
-func LogLevel(threshold jww.Threshold) error {
-	if threshold < jww.LevelTrace || threshold > jww.LevelFatal {
-		return errors.Errorf("log level is not valid: log level: %d", threshold)
-	}
-
-	jww.SetLogThreshold(threshold)
-	jww.SetFlags(log.LstdFlags | log.Lmicroseconds)
-
-	ll := NewJsConsoleLogListener(threshold)
-	AddLogListener(ll.Listen)
-	jww.SetStdoutThreshold(jww.LevelFatal + 1)
-
-	msg := fmt.Sprintf("Log level set to: %s", threshold)
-	switch threshold {
-	case jww.LevelTrace:
-		fallthrough
-	case jww.LevelDebug:
-		fallthrough
-	case jww.LevelInfo:
-		jww.INFO.Print(msg)
-	case jww.LevelWarn:
-		jww.WARN.Print(msg)
-	case jww.LevelError:
-		jww.ERROR.Print(msg)
-	case jww.LevelCritical:
-		jww.CRITICAL.Print(msg)
-	case jww.LevelFatal:
-		jww.FATAL.Print(msg)
-	}
-
-	return nil
-}
-
-// LogLevelJS sets level of logging. All logs at the set level and below will be
-// displayed (e.g., when log level is ERROR, only ERROR, CRITICAL, and FATAL
-// messages will be printed).
-//
-// Log level options:
-//
-//	TRACE    - 0
-//	DEBUG    - 1
-//	INFO     - 2
-//	WARN     - 3
-//	ERROR    - 4
-//	CRITICAL - 5
-//	FATAL    - 6
-//
-// The default log level without updates is INFO.
-//
-// Parameters:
-//   - args[0] - Log level (int).
-//
-// Returns:
-//   - Throws TypeError if the log level is invalid.
-func LogLevelJS(_ js.Value, args []js.Value) any {
-	threshold := jww.Threshold(args[0].Int())
-	err := LogLevel(threshold)
-	if err != nil {
-		utils.Throw(utils.TypeError, err)
-		return nil
-	}
-
-	return nil
-}
diff --git a/logging/logger.go b/logging/logger.go
index 03bd1cf4abebde1e4645bc87d31089ed9bf1c5d3..8155210daf6b0b81667334d8675f63bd6b2aaca5 100644
--- a/logging/logger.go
+++ b/logging/logger.go
@@ -10,29 +10,13 @@
 package logging
 
 import (
-	"encoding/binary"
-	"encoding/json"
-	"fmt"
-	"github.com/armon/circbuf"
 	"github.com/pkg/errors"
-	jww "github.com/spf13/jwalterweatherman"
-	"gitlab.com/elixxir/xxdk-wasm/utils"
-	"gitlab.com/elixxir/xxdk-wasm/worker"
-	"io"
-	"strconv"
-	"sync/atomic"
 	"syscall/js"
-	"time"
-)
 
-const (
-	// DefaultInitThreshold is the log threshold used for the initial log before
-	// any logging options is set.
-	DefaultInitThreshold = jww.LevelTrace
+	jww "github.com/spf13/jwalterweatherman"
 
-	// logListenerChanSize is the size of the listener channel that stores log
-	// messages before they are written.
-	logListenerChanSize = 3000
+	"gitlab.com/elixxir/wasm-utils/utils"
+	"gitlab.com/elixxir/xxdk-wasm/worker"
 )
 
 // List of tags that can be used when sending a message or registering a handler
@@ -46,346 +30,79 @@ const (
 )
 
 // logger is the global that all jwalterweatherman logging is sent to.
-var logger *Logger
-
-// Logger manages the recording of jwalterweatherman logs. It can write logs to
-// a local, in-memory buffer or to an external worker.
-type Logger struct {
-	threshold      jww.Threshold
-	maxLogFileSize int
-	logListenerID  uint64
-
-	listenChan  chan []byte
-	mode        atomic.Uint32
-	processQuit chan struct{}
-
-	cb *circbuf.Buffer
-	wm *worker.Manager
-}
-
-// InitLogger initializes the logger. Include this in the init function in main.
-func InitLogger() *Logger {
-	logger = NewLogger()
-	return logger
-}
+var logger Logger
 
 // GetLogger returns the Logger object, used to manager where logging is
 // recorded.
-func GetLogger() *Logger {
+func GetLogger() Logger {
 	return logger
 }
 
-// NewLogger creates a new Logger that begins storing the first
-// DefaultInitThreshold log entries. If either the log file or log worker is
-// enabled, then these logs are redirected to the set destination. If the
-// channel fills up with no log recorder enabled, then the listener is disabled.
-func NewLogger() *Logger {
-	lf := newLogger()
-
-	// Add the log listener
-	lf.logListenerID = AddLogListener(lf.Listen)
+type Logger interface {
+	// StopLogging stops log message writes. Once logging is stopped, it cannot
+	// be resumed and the log file cannot be recovered.
+	StopLogging()
 
-	jww.INFO.Printf("[LOG] Enabled initial log file listener in %s with ID %d "+
-		"at threshold %s that can store %d entries",
-		lf.getMode(), lf.logListenerID, lf.Threshold(), cap(lf.listenChan))
+	// GetFile returns the entire log file.
+	GetFile() []byte
 
-	return lf
-}
-
-// newLogger initialises a Logger without adding it as a log listener.
-func newLogger() *Logger {
-	lf := &Logger{
-		threshold:   DefaultInitThreshold,
-		listenChan:  make(chan []byte, logListenerChanSize),
-		mode:        atomic.Uint32{},
-		processQuit: make(chan struct{}),
-	}
-	lf.setMode(initMode)
+	// Threshold returns the log level threshold used in the file.
+	Threshold() jww.Threshold
 
-	return lf
-}
+	// MaxSize returns the maximum size, in bytes, of the log file before it
+	// rolls over and starts overwriting the oldest entries
+	MaxSize() int
 
-// LogToFile starts logging to a local, in-memory log file.
-func (l *Logger) LogToFile(threshold jww.Threshold, maxLogFileSize int) error {
-	err := l.prepare(threshold, maxLogFileSize, fileMode)
-	if err != nil {
-		return err
-	}
-
-	b, err := circbuf.NewBuffer(int64(maxLogFileSize))
-	if err != nil {
-		return err
-	}
-	l.cb = b
-
-	sendLog := func(p []byte) {
-		if n, err2 := l.cb.Write(p); err2 != nil {
-			jww.ERROR.Printf(
-				"[LOG] Error writing log to circular buffer: %+v", err2)
-		} else if n != len(p) {
-			jww.ERROR.Printf(
-				"[LOG] Wrote %d bytes when %d bytes expected", n, len(p))
-		}
-	}
-	go l.processLog(workerMode, sendLog, l.processQuit)
+	// Size returns the number of bytes written to the log file.
+	Size() int
 
-	return nil
+	// Worker returns the manager for the Javascript Worker object. If the
+	// worker has not been initialized, it returns nil.
+	Worker() *worker.Manager
 }
 
-// LogToFileWorker starts a new worker that begins listening for logs and
-// writing them to file. This function blocks until the worker has started.
-func (l *Logger) LogToFileWorker(threshold jww.Threshold, maxLogFileSize int,
-	wasmJsPath, workerName string) error {
-	err := l.prepare(threshold, maxLogFileSize, workerMode)
-	if err != nil {
-		return err
-	}
+// EnableLogging enables logging to the Javascript console and to a local or
+// worker file buffer. This must be called only once at initialisation.
+func EnableLogging(logLevel, fileLogLevel jww.Threshold, maxLogFileSizeMB int,
+	workerScriptURL, workerName string) error {
 
-	// Create new worker manager, which will start the worker and wait until
-	// communication has been established
-	wm, err := worker.NewManager(wasmJsPath, workerName, false)
-	if err != nil {
-		return err
-	}
-	l.wm = wm
-
-	// Register the callback used by the Javascript to request the log file.
-	// This prevents an error print when GetFileExtTag is not registered.
-	l.wm.RegisterCallback(GetFileExtTag, func([]byte) {
-		jww.DEBUG.Print("[LOG] Received file requested from external " +
-			"Javascript. Ignoring file.")
-	})
-
-	data, err := json.Marshal(l.maxLogFileSize)
-	if err != nil {
-		return err
+	var listeners []jww.LogListener
+	if logLevel > -1 {
+		// Overwrites setting the log level to INFO done in bindings so that the
+		// Javascript console can be used
+		ll := NewJsConsoleLogListener(logLevel)
+		listeners = append(listeners, ll.Listen)
+		jww.SetStdoutThreshold(jww.LevelFatal + 1)
+		jww.FEEDBACK.Printf("[LOG] Log level for console set to %s", logLevel)
+	} else {
+		jww.FEEDBACK.Print("[LOG] Disabling logging to console.")
 	}
 
-	// Send message to initialize the log file listener
-	errChan := make(chan error)
-	l.wm.SendMessage(NewLogFileTag, data, func(data []byte) {
-		if len(data) > 0 {
-			errChan <- errors.New(string(data))
+	if fileLogLevel > -1 {
+		maxLogFileSize := maxLogFileSizeMB * 1_000_000
+		if workerScriptURL == "" {
+			fl, err := newFileLogger(fileLogLevel, maxLogFileSize)
+			if err != nil {
+				return errors.Wrap(err, "could not initialize logging to file")
+			}
+			listeners = append(listeners, fl.Listen)
 		} else {
-			errChan <- nil
-		}
-	})
+			wl, err := newWorkerLogger(
+				fileLogLevel, maxLogFileSize, workerScriptURL, workerName)
+			if err != nil {
+				return errors.Wrap(err, "could not initialize logging to worker file")
+			}
 
-	// Wait for worker to respond
-	select {
-	case err = <-errChan:
-		if err != nil {
-			return err
+			listeners = append(listeners, wl.Listen)
 		}
-	case <-time.After(worker.ResponseTimeout):
-		return errors.Errorf("timed out after %s waiting for new log "+
-			"file in worker to initialize", worker.ResponseTimeout)
-	}
 
-	jww.INFO.Printf("[LOG] Initialized log to file web worker %s.", workerName)
-
-	sendLog := func(p []byte) { l.wm.SendMessage(WriteLogTag, p, nil) }
-	go l.processLog(workerMode, sendLog, l.processQuit)
-
-	return nil
-}
-
-// processLog processes the log messages sent to the listener channel and sends
-// them to the appropriate recorder.
-func (l *Logger) processLog(m mode, sendLog func(p []byte), quit chan struct{}) {
-	jww.INFO.Printf("[LOG] Starting log file processing thread in %s.", m)
-
-	for {
-		select {
-		case <-quit:
-			jww.INFO.Printf("[LOG] Stopping log file processing thread.")
-			return
-		case p := <-l.listenChan:
-			go sendLog(p)
-		}
-	}
-}
-
-// prepare sets the threshold, maxLogFileSize, and mode of the logger and
-// prints a log message indicating this information.
-func (l *Logger) prepare(
-	threshold jww.Threshold, maxLogFileSize int, m mode) error {
-	if m := l.getMode(); m != initMode {
-		return errors.Errorf("log already set to %s", m)
-	} else if threshold < jww.LevelTrace || threshold > jww.LevelFatal {
-		return errors.Errorf("log level of %d is invalid", threshold)
-	}
-
-	l.threshold = threshold
-	l.maxLogFileSize = maxLogFileSize
-	l.setMode(m)
-
-	msg := fmt.Sprintf("[LOG] Outputting log to file in %s of max size %d "+
-		"with level %s", m, l.MaxSize(), l.Threshold())
-	switch l.Threshold() {
-	case jww.LevelTrace:
-		fallthrough
-	case jww.LevelDebug:
-		fallthrough
-	case jww.LevelInfo:
-		jww.INFO.Print(msg)
-	case jww.LevelWarn:
-		jww.WARN.Print(msg)
-	case jww.LevelError:
-		jww.ERROR.Print(msg)
-	case jww.LevelCritical:
-		jww.CRITICAL.Print(msg)
-	case jww.LevelFatal:
-		jww.FATAL.Print(msg)
+		js.Global().Set("GetLogger", js.FuncOf(GetLoggerJS))
 	}
+	jww.SetLogListeners(listeners...)
 
 	return nil
 }
 
-// StopLogging stops the logging of log messages and disables the log listener.
-// If the log worker is running, it is terminated. Once logging is stopped, it
-// cannot be resumed the log file cannot be recovered.
-func (l *Logger) StopLogging() {
-	jww.DEBUG.Printf("[LOG] Removing log listener with ID %d", l.logListenerID)
-	RemoveLogListener(l.logListenerID)
-
-	switch l.getMode() {
-	case workerMode:
-		l.wm.Stop()
-		jww.DEBUG.Printf("[LOG] Terminated log worker.")
-	case fileMode:
-		jww.DEBUG.Printf("[LOG] Reset circular buffer.")
-		l.cb.Reset()
-	}
-
-	select {
-	case l.processQuit <- struct{}{}:
-		jww.DEBUG.Printf("[LOG] Sent quit channel to log process.")
-	default:
-		jww.DEBUG.Printf("[LOG] Failed to stop log processes.")
-	}
-}
-
-// GetFile returns the entire log file.
-//
-// If the log file is listening locally, it returns it from the local buffer. If
-// it is listening from the worker, it blocks until the file is returned.
-func (l *Logger) GetFile() []byte {
-	switch l.getMode() {
-	case fileMode:
-		return l.cb.Bytes()
-	case workerMode:
-		fileChan := make(chan []byte)
-		l.wm.SendMessage(GetFileTag, nil, func(data []byte) { fileChan <- data })
-
-		select {
-		case file := <-fileChan:
-			return file
-		case <-time.After(worker.ResponseTimeout):
-			jww.FATAL.Panicf("[LOG] Timed out after %s waiting for log "+
-				"file from worker", worker.ResponseTimeout)
-			return nil
-		}
-	default:
-		return nil
-	}
-}
-
-// Threshold returns the log level threshold used in the file.
-func (l *Logger) Threshold() jww.Threshold {
-	return l.threshold
-}
-
-// MaxSize returns the max size, in bytes, that the log file is allowed to be.
-func (l *Logger) MaxSize() int {
-	return l.maxLogFileSize
-}
-
-// Size returns the current size, in bytes, written to the log file.
-//
-// If the log file is listening locally, it returns it from the local buffer. If
-// it is listening from the worker, it blocks until the size is returned.
-func (l *Logger) Size() int {
-	switch l.getMode() {
-	case fileMode:
-		return int(l.cb.Size())
-	case workerMode:
-		sizeChan := make(chan []byte)
-		l.wm.SendMessage(SizeTag, nil, func(data []byte) { sizeChan <- data })
-
-		select {
-		case data := <-sizeChan:
-			return int(jww.Threshold(binary.LittleEndian.Uint64(data)))
-		case <-time.After(worker.ResponseTimeout):
-			jww.FATAL.Panicf("[LOG] Timed out after %s waiting for log "+
-				"file size from worker", worker.ResponseTimeout)
-			return 0
-		}
-	default:
-		return 0
-	}
-}
-
-////////////////////////////////////////////////////////////////////////////////
-// JWW Listener                                                               //
-////////////////////////////////////////////////////////////////////////////////
-
-// Listen is called for every logging event. This function adheres to the
-// [jwalterweatherman.LogListener] type.
-func (l *Logger) Listen(t jww.Threshold) io.Writer {
-	if t < l.threshold {
-		return nil
-	}
-
-	return l
-}
-
-// Write sends the bytes to the listener channel. It always returns the length
-// of p and a nil error. This function adheres to the io.Writer interface.
-func (l *Logger) Write(p []byte) (n int, err error) {
-	select {
-	case l.listenChan <- append([]byte{}, p...):
-	default:
-		jww.ERROR.Printf(
-			"[LOG] Logger channel filled. Log file recording stopping.")
-		l.StopLogging()
-		return 0, errors.Errorf(
-			"Logger channel filled. Log file recording stopping.")
-	}
-	return len(p), nil
-}
-
-////////////////////////////////////////////////////////////////////////////////
-// Log File Mode                                                              //
-////////////////////////////////////////////////////////////////////////////////
-
-// mode represents the state of the Logger.
-type mode uint32
-
-const (
-	initMode mode = iota
-	fileMode
-	workerMode
-)
-
-func (l *Logger) setMode(m mode) { l.mode.Store(uint32(m)) }
-func (l *Logger) getMode() mode  { return mode(l.mode.Load()) }
-
-// String returns a human-readable representation of the mode for logging and
-// debugging. This function adheres to the fmt.Stringer interface.
-func (m mode) String() string {
-	switch m {
-	case initMode:
-		return "uninitialized mode"
-	case fileMode:
-		return "file mode"
-	case workerMode:
-		return "worker mode"
-	default:
-		return "invalid mode: " + strconv.Itoa(int(m))
-	}
-}
-
 ////////////////////////////////////////////////////////////////////////////////
 // Javascript Bindings                                                        //
 ////////////////////////////////////////////////////////////////////////////////
@@ -396,142 +113,98 @@ func (m mode) String() string {
 // Returns:
 //   - A Javascript representation of the [Logger] object.
 func GetLoggerJS(js.Value, []js.Value) any {
-	return newLoggerJS(GetLogger())
+	// l := GetLogger()
+	// if l != nil {
+	// 	return newLoggerJS(LoggerJS{GetLogger()})
+	// }
+	// return js.Null()
+	return newLoggerJS(LoggerJS{GetLogger()})
+}
+
+type LoggerJS struct {
+	api Logger
 }
 
 // newLoggerJS creates a new Javascript compatible object (map[string]any) that
 // matches the [Logger] structure.
-func newLoggerJS(lfw *Logger) map[string]any {
+func newLoggerJS(l LoggerJS) map[string]any {
 	logFileWorker := map[string]any{
-		"LogToFile":       js.FuncOf(lfw.LogToFileJS),
-		"LogToFileWorker": js.FuncOf(lfw.LogToFileWorkerJS),
-		"StopLogging":     js.FuncOf(lfw.StopLoggingJS),
-		"GetFile":         js.FuncOf(lfw.GetFileJS),
-		"Threshold":       js.FuncOf(lfw.ThresholdJS),
-		"MaxSize":         js.FuncOf(lfw.MaxSizeJS),
-		"Size":            js.FuncOf(lfw.SizeJS),
-		"Worker":          js.FuncOf(lfw.WorkerJS),
+		"StopLogging": js.FuncOf(l.StopLogging),
+		"GetFile":     js.FuncOf(l.GetFile),
+		"Threshold":   js.FuncOf(l.Threshold),
+		"MaxSize":     js.FuncOf(l.MaxSize),
+		"Size":        js.FuncOf(l.Size),
+		"Worker":      js.FuncOf(l.Worker),
 	}
 
 	return logFileWorker
 }
 
-// LogToFileJS starts logging to a local, in-memory log file.
-//
-// Parameters:
-//   - args[0] - Log level (int).
-//   - args[1] - Max log file size, in bytes (int).
-//
-// Returns:
-//   - Throws a TypeError if starting the log file fails.
-func (l *Logger) LogToFileJS(_ js.Value, args []js.Value) any {
-	threshold := jww.Threshold(args[0].Int())
-	maxLogFileSize := args[1].Int()
-
-	err := l.LogToFile(threshold, maxLogFileSize)
-	if err != nil {
-		utils.Throw(utils.TypeError, err)
-		return nil
-	}
-
-	return nil
-}
-
-// LogToFileWorkerJS starts a new worker that begins listening for logs and
-// writing them to file. This function blocks until the worker has started.
-//
-// Parameters:
-//   - args[0] - Log level (int).
-//   - args[1] - Max log file size, in bytes (int).
-//   - args[2] - Path to Javascript start file for the worker WASM (string).
-//   - args[3] - Name of the worker (used in logs) (string).
-//
-// Returns a promise:
-//   - Resolves to nothing on success (void).
-//   - Rejected with an error if starting the worker fails.
-func (l *Logger) LogToFileWorkerJS(_ js.Value, args []js.Value) any {
-	threshold := jww.Threshold(args[0].Int())
-	maxLogFileSize := args[1].Int()
-	wasmJsPath := args[2].String()
-	workerName := args[3].String()
-
-	promiseFn := func(resolve, reject func(args ...any) js.Value) {
-		err := l.LogToFileWorker(
-			threshold, maxLogFileSize, wasmJsPath, workerName)
-		if err != nil {
-			reject(utils.JsTrace(err))
-		} else {
-			resolve()
-		}
-	}
-
-	return utils.CreatePromise(promiseFn)
-}
-
-// StopLoggingJS stops the logging of log messages and disables the log
+// StopLogging stops the logging of log messages and disables the log
 // listener. If the log worker is running, it is terminated. Once logging is
 // stopped, it cannot be resumed the log file cannot be recovered.
-func (l *Logger) StopLoggingJS(js.Value, []js.Value) any {
-	l.StopLogging()
+func (l *LoggerJS) StopLogging(js.Value, []js.Value) any {
+	l.api.StopLogging()
 
 	return nil
 }
 
-// GetFileJS returns the entire log file.
+// GetFile returns the entire log file.
 //
 // If the log file is listening locally, it returns it from the local buffer. If
 // it is listening from the worker, it blocks until the file is returned.
 //
 // Returns a promise:
 //   - Resolves to the log file contents (string).
-func (l *Logger) GetFileJS(js.Value, []js.Value) any {
+func (l *LoggerJS) GetFile(js.Value, []js.Value) any {
 	promiseFn := func(resolve, _ func(args ...any) js.Value) {
-		resolve(string(l.GetFile()))
+		resolve(string(l.api.GetFile()))
 	}
 
 	return utils.CreatePromise(promiseFn)
 }
 
-// ThresholdJS returns the log level threshold used in the file.
+// Threshold returns the log level threshold used in the file.
 //
 // Returns:
 //   - Log level (int).
-func (l *Logger) ThresholdJS(js.Value, []js.Value) any {
-	return int(l.Threshold())
+func (l *LoggerJS) Threshold(js.Value, []js.Value) any {
+	return int(l.api.Threshold())
 }
 
-// MaxSizeJS returns the max size, in bytes, that the log file is allowed to be.
+// MaxSize returns the max size, in bytes, that the log file is allowed to be.
 //
 // Returns:
 //   - Max file size (int).
-func (l *Logger) MaxSizeJS(js.Value, []js.Value) any {
-	return l.MaxSize()
+func (l *LoggerJS) MaxSize(js.Value, []js.Value) any {
+	return l.api.MaxSize()
 }
 
-// SizeJS returns the current size, in bytes, written to the log file.
+// Size returns the current size, in bytes, written to the log file.
 //
 // If the log file is listening locally, it returns it from the local buffer. If
 // it is listening from the worker, it blocks until the size is returned.
 //
 // Returns a promise:
 //   - Resolves to the current file size (int).
-func (l *Logger) SizeJS(js.Value, []js.Value) any {
+func (l *LoggerJS) Size(js.Value, []js.Value) any {
 	promiseFn := func(resolve, _ func(args ...any) js.Value) {
-		resolve(l.Size())
+		resolve(l.api.Size())
 	}
 
 	return utils.CreatePromise(promiseFn)
 }
 
-// WorkerJS returns the web worker object.
+// Worker returns the web worker object.
 //
 // Returns:
 //   - Javascript worker object. If the worker has not been initialized, it
 //     returns null.
-func (l *Logger) WorkerJS(js.Value, []js.Value) any {
-	if l.getMode() == workerMode {
-		return l.wm.GetWorker()
+func (l *LoggerJS) Worker(js.Value, []js.Value) any {
+	wm := l.api.Worker()
+	if wm == nil {
+		return js.Null()
 	}
 
-	return js.Null()
+	return wm.GetWorker()
 }
diff --git a/logging/logger_test.go b/logging/logger_test.go
deleted file mode 100644
index 0b5267be9cabaf995ddc8fcdba4b5d3ec8eea133..0000000000000000000000000000000000000000
--- a/logging/logger_test.go
+++ /dev/null
@@ -1,172 +0,0 @@
-////////////////////////////////////////////////////////////////////////////////
-// Copyright © 2022 xx foundation                                             //
-//                                                                            //
-// Use of this source code is governed by a license that can be found in the  //
-// LICENSE file.                                                              //
-////////////////////////////////////////////////////////////////////////////////
-
-//go:build js && wasm
-
-package logging
-
-import (
-	"bytes"
-	"fmt"
-	jww "github.com/spf13/jwalterweatherman"
-	"testing"
-)
-
-// Tests InitLogger
-func TestInitLogger(t *testing.T) {
-}
-
-// Tests GetLogger
-func TestGetLogger(t *testing.T) {
-}
-
-// Tests NewLogger
-func TestNewLogger(t *testing.T) {
-}
-
-// Tests Logger.LogToFile
-func TestLogger_LogToFile(t *testing.T) {
-	jww.SetStdoutThreshold(jww.LevelTrace)
-	l := NewLogger()
-
-	err := l.LogToFile(jww.LevelTrace, 50000000)
-	if err != nil {
-		t.Fatalf("Failed to LogToFile: %+v", err)
-	}
-
-	jww.INFO.Printf("test")
-
-	file := l.cb.Bytes()
-	fmt.Printf("file:----------------------------\n%s\n---------------------------------\n", file)
-}
-
-// Tests Logger.LogToFileWorker
-func TestLogger_LogToFileWorker(t *testing.T) {
-}
-
-// Tests Logger.processLog
-func TestLogger_processLog(t *testing.T) {
-}
-
-// Tests Logger.prepare
-func TestLogger_prepare(t *testing.T) {
-}
-
-// Tests Logger.StopLogging
-func TestLogger_StopLogging(t *testing.T) {
-}
-
-// Tests Logger.GetFile
-func TestLogger_GetFile(t *testing.T) {
-}
-
-// Tests Logger.Threshold
-func TestLogger_Threshold(t *testing.T) {
-}
-
-// Tests Logger.MaxSize
-func TestLogger_MaxSize(t *testing.T) {
-}
-
-// Tests Logger.Size
-func TestLogger_Size(t *testing.T) {
-}
-
-// Tests Logger.Listen
-func TestLogger_Listen(t *testing.T) {
-
-	// l := newLogger()
-
-}
-
-// Tests that Logger.Write can fill the listenChan channel completely and that
-// all messages are received in the order they were added.
-func TestLogger_Write(t *testing.T) {
-	l := newLogger()
-	expectedLogs := make([][]byte, logListenerChanSize)
-
-	for i := range expectedLogs {
-		p := []byte(
-			fmt.Sprintf("Log message %d of %d.", i+1, logListenerChanSize))
-		expectedLogs[i] = p
-		n, err := l.Listen(jww.LevelError).Write(p)
-		if err != nil {
-			t.Errorf("Received impossible error (%d): %+v", i, err)
-		} else if n != len(p) {
-			t.Errorf("Received incorrect bytes written (%d)."+
-				"\nexpected: %d\nreceived: %d", i, len(p), n)
-		}
-	}
-
-	for i, expected := range expectedLogs {
-		select {
-		case received := <-l.listenChan:
-			if !bytes.Equal(expected, received) {
-				t.Errorf("Received unexpected meessage (%d)."+
-					"\nexpected: %q\nreceived: %q", i, expected, received)
-			}
-		default:
-			t.Errorf("Failed to read from channel.")
-		}
-	}
-}
-
-// Error path: Tests that Logger.Write returns an error when the listener
-// channel is full.
-func TestLogger_Write_ChannelFilledError(t *testing.T) {
-	l := newLogger()
-	expectedLogs := make([][]byte, logListenerChanSize)
-
-	for i := range expectedLogs {
-		p := []byte(
-			fmt.Sprintf("Log message %d of %d.", i+1, logListenerChanSize))
-		expectedLogs[i] = p
-		n, err := l.Listen(jww.LevelError).Write(p)
-		if err != nil {
-			t.Errorf("Received impossible error (%d): %+v", i, err)
-		} else if n != len(p) {
-			t.Errorf("Received incorrect bytes written (%d)."+
-				"\nexpected: %d\nreceived: %d", i, len(p), n)
-		}
-	}
-
-	_, err := l.Write([]byte("test"))
-	if err == nil {
-		t.Error("Failed to receive error when the chanel should be full.")
-	}
-}
-
-// Tests that Logger.getMode gets the same value set with Logger.setMode.
-func TestLogger_setMode_getMode(t *testing.T) {
-	l := newLogger()
-
-	for i, m := range []mode{initMode, fileMode, workerMode, 12} {
-		l.setMode(m)
-		received := l.getMode()
-		if m != received {
-			t.Errorf("Received wrong mode (%d).\nexpected: %s\nreceived: %s",
-				i, m, received)
-		}
-	}
-
-}
-
-// Unit test of mode.String.
-func Test_mode_String(t *testing.T) {
-	for m, expected := range map[mode]string{
-		initMode:   "uninitialized mode",
-		fileMode:   "file mode",
-		workerMode: "worker mode",
-		12:         "invalid mode: 12",
-	} {
-		s := m.String()
-		if s != expected {
-			t.Errorf("Wrong string for mode %d.\nexpected: %s\nreceived: %s",
-				m, expected, s)
-		}
-	}
-}
diff --git a/logging/workerLogger.go b/logging/workerLogger.go
new file mode 100644
index 0000000000000000000000000000000000000000..bfac4703943c94956a8dc6501b722e7d0e5cad62
--- /dev/null
+++ b/logging/workerLogger.go
@@ -0,0 +1,162 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+//go:build js && wasm
+
+package logging
+
+import (
+	"encoding/binary"
+	"encoding/json"
+	"io"
+	"math"
+	"time"
+
+	"github.com/pkg/errors"
+	jww "github.com/spf13/jwalterweatherman"
+
+	"gitlab.com/elixxir/xxdk-wasm/worker"
+)
+
+// TODO: add ability to import worker so that multiple threads can send logs: https://stackoverflow.com/questions/8343781/how-to-do-worker-to-worker-communication
+
+// workerLogger manages the recording of jwalterweatherman logs to the in-memory
+// file buffer in a remote Worker thread.
+type workerLogger struct {
+	threshold      jww.Threshold
+	maxLogFileSize int
+	wm             *worker.Manager
+}
+
+// newWorkerLogger starts logging to an in-memory log file in a remote Worker
+// at the specified threshold. Returns a [workerLogger] that can be used to get
+// the log file.
+func newWorkerLogger(threshold jww.Threshold, maxLogFileSize int,
+	wasmJsPath, workerName string) (*workerLogger, error) {
+	// Create new worker manager, which will start the worker and wait until
+	// communication has been established
+	wm, err := worker.NewManager(wasmJsPath, workerName, false)
+	if err != nil {
+		return nil, err
+	}
+
+	wl := &workerLogger{
+		threshold:      threshold,
+		maxLogFileSize: maxLogFileSize,
+		wm:             wm,
+	}
+
+	// Register the callback used by the Javascript to request the log file.
+	// This prevents an error print when GetFileExtTag is not registered.
+	wl.wm.RegisterCallback(GetFileExtTag, func([]byte) {
+		jww.DEBUG.Print("[LOG] Received file requested from external " +
+			"Javascript. Ignoring file.")
+	})
+
+	data, err := json.Marshal(wl.maxLogFileSize)
+	if err != nil {
+		return nil, err
+	}
+
+	// Send message to initialize the log file listener
+	errChan := make(chan error)
+	wl.wm.SendMessage(NewLogFileTag, data, func(data []byte) {
+		if len(data) > 0 {
+			errChan <- errors.New(string(data))
+		} else {
+			errChan <- nil
+		}
+	})
+
+	// Wait for worker to respond
+	select {
+	case err = <-errChan:
+		if err != nil {
+			return nil, err
+		}
+	case <-time.After(worker.ResponseTimeout):
+		return nil, errors.Errorf("timed out after %s waiting for new log "+
+			"file in worker to initialize", worker.ResponseTimeout)
+	}
+
+	jww.FEEDBACK.Printf("[LOG] Outputting log to file of max size %d at level "+
+		"%s using web worker %s", wl.maxLogFileSize, wl.threshold, workerName)
+
+	logger = wl
+	return wl, nil
+}
+
+// Write adheres to the io.Writer interface and sends the log entries to the
+// worker to be added to the file buffer. Always returns the length of p and
+// nil. All errors are printed to the log.
+func (wl *workerLogger) Write(p []byte) (n int, err error) {
+	wl.wm.SendMessage(WriteLogTag, p, nil)
+	return len(p), nil
+}
+
+// Listen adheres to the [jwalterweatherman.LogListener] type and returns the
+// log writer when the threshold is within the set threshold limit.
+func (wl *workerLogger) Listen(threshold jww.Threshold) io.Writer {
+	if threshold < wl.threshold {
+		return nil
+	}
+	return wl
+}
+
+// StopLogging stops log message writes and terminates the worker. Once logging
+// is stopped, it cannot be resumed and the log file cannot be recovered.
+func (wl *workerLogger) StopLogging() {
+	wl.threshold = math.MaxInt
+
+	wl.wm.Stop()
+	jww.DEBUG.Printf("[LOG] Terminated log worker.")
+}
+
+// GetFile returns the entire log file.
+func (wl *workerLogger) GetFile() []byte {
+	fileChan := make(chan []byte)
+	wl.wm.SendMessage(GetFileTag, nil, func(data []byte) { fileChan <- data })
+
+	select {
+	case file := <-fileChan:
+		return file
+	case <-time.After(worker.ResponseTimeout):
+		jww.FATAL.Panicf("[LOG] Timed out after %s waiting for log "+
+			"file from worker", worker.ResponseTimeout)
+		return nil
+	}
+}
+
+// Threshold returns the log level threshold used in the file.
+func (wl *workerLogger) Threshold() jww.Threshold {
+	return wl.threshold
+}
+
+// MaxSize returns the max size, in bytes, that the log file is allowed to be.
+func (wl *workerLogger) MaxSize() int {
+	return wl.maxLogFileSize
+}
+
+// Size returns the number of bytes written to the log file.
+func (wl *workerLogger) Size() int {
+	sizeChan := make(chan []byte)
+	wl.wm.SendMessage(SizeTag, nil, func(data []byte) { sizeChan <- data })
+
+	select {
+	case data := <-sizeChan:
+		return int(binary.LittleEndian.Uint64(data))
+	case <-time.After(worker.ResponseTimeout):
+		jww.FATAL.Panicf("[LOG] Timed out after %s waiting for log "+
+			"file size from worker", worker.ResponseTimeout)
+		return 0
+	}
+}
+
+// Worker returns the manager for the Javascript Worker object.
+func (wl *workerLogger) Worker() *worker.Manager {
+	return wl.wm
+}
diff --git a/logging/workerThread/logFileWorker.js b/logging/workerThread/logFileWorker.js
index 159bfaa0d919a4f0cb2758af48d80c65891e7820..ed246f62563f89645f95fe13c715a81b60aa756b 100644
--- a/logging/workerThread/logFileWorker.js
+++ b/logging/workerThread/logFileWorker.js
@@ -7,11 +7,15 @@
 
 importScripts('wasm_exec.js');
 
+const isReady = new Promise((resolve) => {
+    self.onWasmInitialized = resolve;
+});
+
 const go = new Go();
 const binPath = 'xxdk-logFileWorker.wasm'
-WebAssembly.instantiateStreaming(fetch(binPath), go.importObject).then((result) => {
+WebAssembly.instantiateStreaming(fetch(binPath), go.importObject).then(async (result) => {
     go.run(result.instance);
-    LogLevel(1);
+    await isReady;
 }).catch((err) => {
     console.error(err);
 });
\ No newline at end of file
diff --git a/logging/workerThread/main.go b/logging/workerThread/main.go
index 91b059f3da1195b0ff244027608a0f8a98482fed..1a9a31a0ca39ee684b4427eef03fc89f4a407b8c 100644
--- a/logging/workerThread/main.go
+++ b/logging/workerThread/main.go
@@ -13,25 +13,21 @@ import (
 	"encoding/binary"
 	"encoding/json"
 	"fmt"
+	"os"
+	"syscall/js"
+
 	"github.com/armon/circbuf"
 	"github.com/pkg/errors"
+	"github.com/spf13/cobra"
 	jww "github.com/spf13/jwalterweatherman"
+
 	"gitlab.com/elixxir/xxdk-wasm/logging"
 	"gitlab.com/elixxir/xxdk-wasm/worker"
-	"syscall/js"
 )
 
 // SEMVER is the current semantic version of the xxDK Logger web worker.
 const SEMVER = "0.1.0"
 
-func init() {
-	// Set up Javascript console listener set at level INFO
-	ll := logging.NewJsConsoleLogListener(jww.LevelDebug)
-	logging.AddLogListener(ll.Listen)
-	jww.SetStdoutThreshold(jww.LevelFatal + 1)
-	jww.INFO.Printf("xxDK Logger web worker version: v%s", SEMVER)
-}
-
 // workerLogFile manages communication with the main thread and writing incoming
 // logging messages to the log file.
 type workerLogFile struct {
@@ -40,17 +36,60 @@ type workerLogFile struct {
 }
 
 func main() {
-	jww.INFO.Print("[LOG] Starting xxDK WebAssembly Logger Worker.")
+	// Set to os.Args because the default is os.Args[1:] and in WASM, args start
+	// at 0, not 1.
+	LoggerCmd.SetArgs(os.Args)
+
+	err := LoggerCmd.Execute()
+	if err != nil {
+		fmt.Println(err)
+		os.Exit(1)
+	}
+}
+
+var LoggerCmd = &cobra.Command{
+	Use:     "Logger",
+	Short:   "Web worker buffer file logger",
+	Example: "const go = new Go();\ngo.argv = [\"--logLevel=1\"]",
+	Run: func(cmd *cobra.Command, args []string) {
+		// Start logger first to capture all logging events
+		err := logging.EnableLogging(logLevel, -1, 0, "", "")
+		if err != nil {
+			fmt.Printf(
+				"Failed to intialize logging in logging worker: %+v", err)
+			os.Exit(1)
+		}
 
-	js.Global().Set("LogLevel", js.FuncOf(logging.LogLevelJS))
+		jww.INFO.Printf("xxDK Logger web worker version: v%s", SEMVER)
 
-	wlf := workerLogFile{wtm: worker.NewThreadManager("Logger", false)}
+		jww.INFO.Print("[LOG] Starting xxDK WebAssembly Logger Worker.")
 
-	wlf.registerCallbacks()
+		wlf := workerLogFile{wtm: worker.NewThreadManager("Logger", false)}
 
-	wlf.wtm.SignalReady()
-	<-make(chan bool)
-	fmt.Println("[WW] Closing xxDK WebAssembly Log Worker.")
+		wlf.registerCallbacks()
+
+		wlf.wtm.SignalReady()
+
+		// Indicate to the Javascript caller that the WASM is ready by resolving
+		// a promise created by the caller.
+		js.Global().Get("onWasmInitialized").Invoke()
+
+		<-make(chan bool)
+		fmt.Println("[WW] Closing xxDK WebAssembly Log Worker.")
+		os.Exit(0)
+	},
+}
+
+var (
+	logLevel jww.Threshold
+)
+
+func init() {
+	// Initialize all startup flags
+	LoggerCmd.Flags().IntVarP((*int)(&logLevel), "logLevel", "l", 2,
+		"Sets the log level output when outputting to the Javascript console. "+
+			"0 = TRACE, 1 = DEBUG, 2 = INFO, 3 = WARN, 4 = ERROR, "+
+			"5 = CRITICAL, 6 = FATAL, -1 = disabled.")
 }
 
 // registerCallbacks registers all the necessary callbacks for the main thread
diff --git a/main.go b/main.go
index c109ff54f41fc0c3acb34bd7d3c848013237360c..267bb55bb812d1950c412a51337464cd35f39b36 100644
--- a/main.go
+++ b/main.go
@@ -10,38 +10,76 @@
 package main
 
 import (
-	"gitlab.com/elixxir/xxdk-wasm/logging"
+	"fmt"
 	"os"
 	"syscall/js"
 
+	"github.com/spf13/cobra"
+
 	jww "github.com/spf13/jwalterweatherman"
+
+	"gitlab.com/elixxir/wasm-utils/utils"
+	"gitlab.com/elixxir/xxdk-wasm/logging"
 	"gitlab.com/elixxir/xxdk-wasm/storage"
-	"gitlab.com/elixxir/xxdk-wasm/utils"
 	"gitlab.com/elixxir/xxdk-wasm/wasm"
 )
 
-func init() {
-	// Start logger first to capture all logging events
-	logging.InitLogger()
-
-	// Overwrites setting the log level to INFO done in bindings so that the
-	// Javascript console can be used
-	ll := logging.NewJsConsoleLogListener(jww.LevelInfo)
-	logging.AddLogListener(ll.Listen)
-	jww.SetStdoutThreshold(jww.LevelFatal + 1)
+func main() {
+	// Set to os.Args because the default is os.Args[1:] and in WASM, args start
+	// at 0, not 1.
+	wasmCmd.SetArgs(os.Args)
 
-	// Check that the WASM binary version is correct
-	err := storage.CheckAndStoreVersions()
+	err := wasmCmd.Execute()
 	if err != nil {
-		jww.FATAL.Panicf("WASM binary version error: %+v", err)
+		fmt.Println(err)
+		os.Exit(1)
 	}
 }
 
-func main() {
-	jww.INFO.Printf("Starting xxDK WebAssembly bindings.")
+var wasmCmd = &cobra.Command{
+	Use:     "xxdk-wasm",
+	Short:   "WebAssembly bindings for xxDK.",
+	Example: "const go = new Go();\ngo.argv = [\"--logLevel=1\"]",
+	Run: func(cmd *cobra.Command, args []string) {
+		// Start logger first to capture all logging events
+		err := logging.EnableLogging(logLevel, fileLogLevel, maxLogFileSizeMB,
+			workerScriptURL, workerName)
+		if err != nil {
+			fmt.Printf("Failed to intialize logging: %+v", err)
+			os.Exit(1)
+		}
+
+		// Check that the WASM binary version is correct
+		err = storage.CheckAndStoreVersions()
+		if err != nil {
+			jww.FATAL.Panicf("WASM binary version error: %+v", err)
+		}
+
+		// Enable all top level bindings functions
+		setGlobals()
+
+		// Indicate to the Javascript caller that the WASM is ready by resolving
+		// a promise created by the caller, as shown below:
+		//
+		//  let isReady = new Promise((resolve) => {
+		//    window.onWasmInitialized = resolve;
+		//  });
+		//
+		//  const go = new Go();
+		//  go.run(result.instance);
+		//  await isReady;
+		//
+		// Source: https://github.com/golang/go/issues/49710#issuecomment-986484758
+		js.Global().Get("onWasmInitialized").Invoke()
+
+		<-make(chan bool)
+		os.Exit(0)
+	},
+}
 
-	// logging/worker.go
-	js.Global().Set("GetLogger", js.FuncOf(logging.GetLoggerJS))
+// setGlobals enables all global functions to be accessible to Javascript.
+func setGlobals() {
+	jww.INFO.Printf("Starting xxDK WebAssembly bindings.")
 
 	// storage/password.go
 	js.Global().Set("GetOrInitPassword", js.FuncOf(storage.GetOrInitPassword))
@@ -62,6 +100,11 @@ func main() {
 	js.Global().Set("InitializeBackup", js.FuncOf(wasm.InitializeBackup))
 	js.Global().Set("ResumeBackup", js.FuncOf(wasm.ResumeBackup))
 
+	// wasm/notifications.go
+	js.Global().Set("LoadNotifications", js.FuncOf(wasm.LoadNotifications))
+	js.Global().Set("LoadNotificationsDummy",
+		js.FuncOf(wasm.LoadNotificationsDummy))
+
 	// wasm/channels.go
 	js.Global().Set("GenerateChannelIdentity",
 		js.FuncOf(wasm.GenerateChannelIdentity))
@@ -89,10 +132,14 @@ func main() {
 	js.Global().Set("GetShareUrlType", js.FuncOf(wasm.GetShareUrlType))
 	js.Global().Set("ValidForever", js.FuncOf(wasm.ValidForever))
 	js.Global().Set("IsNicknameValid", js.FuncOf(wasm.IsNicknameValid))
+	js.Global().Set("GetNotificationReportsForMe",
+		js.FuncOf(wasm.GetNotificationReportsForMe))
 	js.Global().Set("GetNoMessageErr", js.FuncOf(wasm.GetNoMessageErr))
 	js.Global().Set("CheckNoMessageErr", js.FuncOf(wasm.CheckNoMessageErr))
 	js.Global().Set("NewChannelsDatabaseCipher",
 		js.FuncOf(wasm.NewChannelsDatabaseCipher))
+	js.Global().Set("GetNotificationReportsForMe",
+		js.FuncOf(wasm.GetNotificationReportsForMe))
 
 	// wasm/dm.go
 	js.Global().Set("InitChannelsFileTransfer",
@@ -110,6 +157,8 @@ func main() {
 	// wasm/cmix.go
 	js.Global().Set("NewCmix", js.FuncOf(wasm.NewCmix))
 	js.Global().Set("LoadCmix", js.FuncOf(wasm.LoadCmix))
+	js.Global().Set("LoadSynchronizedCmix",
+		js.FuncOf(wasm.LoadSynchronizedCmix))
 
 	// wasm/delivery.go
 	js.Global().Set("SetDashboardURL", js.FuncOf(wasm.SetDashboardURL))
@@ -157,7 +206,6 @@ func main() {
 		js.FuncOf(wasm.GetFactsFromContact))
 
 	// wasm/logging.go
-	js.Global().Set("LogLevel", js.FuncOf(wasm.LogLevel))
 	js.Global().Set("RegisterLogWriter", js.FuncOf(wasm.RegisterLogWriter))
 	js.Global().Set("EnableGrpcLogs", js.FuncOf(wasm.EnableGrpcLogs))
 
@@ -194,6 +242,8 @@ func main() {
 	js.Global().Set("TransmitSingleUse", js.FuncOf(wasm.TransmitSingleUse))
 	js.Global().Set("Listen", js.FuncOf(wasm.Listen))
 
+	// wasm/sync.go
+
 	// wasm/timeNow.go
 	js.Global().Set("SetTimeSource", js.FuncOf(wasm.SetTimeSource))
 	js.Global().Set("SetOffset", js.FuncOf(wasm.SetOffset))
@@ -212,7 +262,32 @@ func main() {
 	js.Global().Set("GetClientDependencies", js.FuncOf(wasm.GetClientDependencies))
 	js.Global().Set("GetWasmSemanticVersion", js.FuncOf(wasm.GetWasmSemanticVersion))
 	js.Global().Set("GetXXDKSemanticVersion", js.FuncOf(wasm.GetXXDKSemanticVersion))
+}
+
+var (
+	logLevel, fileLogLevel      jww.Threshold
+	maxLogFileSizeMB            int
+	workerScriptURL, workerName string
+)
 
-	<-make(chan bool)
-	os.Exit(0)
+func init() {
+	// Initialize all startup flags
+	wasmCmd.Flags().IntVarP((*int)(&logLevel), "logLevel", "l", 2,
+		"Sets the log level output when outputting to the Javascript console. "+
+			"0 = TRACE, 1 = DEBUG, 2 = INFO, 3 = WARN, 4 = ERROR, "+
+			"5 = CRITICAL, 6 = FATAL, -1 = disabled.")
+	wasmCmd.Flags().IntVarP((*int)(&fileLogLevel), "fileLogLevel", "m", -1,
+		"The log level when outputting to the file buffer. "+
+			"0 = TRACE, 1 = DEBUG, 2 = INFO, 3 = WARN, 4 = ERROR, "+
+			"5 = CRITICAL, 6 = FATAL, -1 = disabled.")
+	wasmCmd.Flags().IntVarP(&maxLogFileSizeMB, "maxLogFileSize", "s", 5,
+		"Max file size, in MB, for the file buffer before it rolls over "+
+			"and starts overwriting the oldest entries.")
+	wasmCmd.Flags().StringVarP(&workerScriptURL, "workerScriptURL", "w", "",
+		"URL to the script that executes the worker. If set, it enables the "+
+			"saving of log file to buffer in Worker instead of in the local "+
+			"thread. This allows logging to be available after the main WASM "+
+			"thread crashes.")
+	wasmCmd.Flags().StringVar(&workerName, "workerName", "xxdkLogFileWorker",
+		"Name of the logger worker.")
 }
diff --git a/storage/indexedDbEncryptionTrack.go b/storage/indexedDbEncryptionTrack.go
index d568a63787490ea0979a189191272da70d957e05..bb3f049b8adeef1e2f69b535e79285338f5107bc 100644
--- a/storage/indexedDbEncryptionTrack.go
+++ b/storage/indexedDbEncryptionTrack.go
@@ -12,6 +12,8 @@ package storage
 import (
 	"github.com/pkg/errors"
 	"os"
+
+	"gitlab.com/elixxir/wasm-utils/storage"
 )
 
 // Key to store if the database is encrypted or not
@@ -22,12 +24,15 @@ const databaseEncryptionToggleKey = "xxdkWasmDatabaseEncryptionToggle/"
 func StoreIndexedDbEncryptionStatus(
 	databaseName string, encryptionStatus bool) (
 	loadedEncryptionStatus bool, err error) {
-	data, err := GetLocalStorage().GetItem(
-		databaseEncryptionToggleKey + databaseName)
+	ls := storage.GetLocalStorage()
+	data, err := ls.Get(databaseEncryptionToggleKey + databaseName)
 	if err != nil {
 		if errors.Is(err, os.ErrNotExist) {
-			GetLocalStorage().SetItem(
-				databaseEncryptionToggleKey+databaseName, []byte{1})
+			keyName := databaseEncryptionToggleKey + databaseName
+			if err = ls.Set(keyName, []byte{1}); err != nil {
+				return false,
+					errors.Wrapf(err, "localStorage: failed to set %q", keyName)
+			}
 			return encryptionStatus, nil
 		} else {
 			return false, err
diff --git a/storage/indexedDbList.go b/storage/indexedDbList.go
index a736984f0ef742d96e7a63238ea6ebfbdc25d9f9..8917fcdbf3c413d306bd6cc0687d2af80b441992 100644
--- a/storage/indexedDbList.go
+++ b/storage/indexedDbList.go
@@ -11,8 +11,11 @@ package storage
 
 import (
 	"encoding/json"
-	"github.com/pkg/errors"
 	"os"
+
+	"github.com/pkg/errors"
+
+	"gitlab.com/elixxir/wasm-utils/storage"
 )
 
 const indexedDbListKey = "xxDkWasmIndexedDbList"
@@ -20,7 +23,7 @@ const indexedDbListKey = "xxDkWasmIndexedDbList"
 // GetIndexedDbList returns the list of stored indexedDb databases.
 func GetIndexedDbList() (map[string]struct{}, error) {
 	list := make(map[string]struct{})
-	listBytes, err := GetLocalStorage().GetItem(indexedDbListKey)
+	listBytes, err := storage.GetLocalStorage().Get(indexedDbListKey)
 	if err != nil && !errors.Is(err, os.ErrNotExist) {
 		return nil, err
 	} else if err == nil {
@@ -47,7 +50,11 @@ func StoreIndexedDb(databaseName string) error {
 		return err
 	}
 
-	GetLocalStorage().SetItem(indexedDbListKey, listBytes)
+	err = storage.GetLocalStorage().Set(indexedDbListKey, listBytes)
+	if err != nil {
+		return errors.Wrapf(err,
+			"localStorage: failed to set %q", indexedDbListKey)
+	}
 
 	return nil
 }
diff --git a/storage/localStorage.go b/storage/localStorage.go
deleted file mode 100644
index d54039c99c427640b3298c7aeb87dfb819d046be..0000000000000000000000000000000000000000
--- a/storage/localStorage.go
+++ /dev/null
@@ -1,209 +0,0 @@
-////////////////////////////////////////////////////////////////////////////////
-// Copyright © 2022 xx foundation                                             //
-//                                                                            //
-// Use of this source code is governed by a license that can be found in the  //
-// LICENSE file.                                                              //
-////////////////////////////////////////////////////////////////////////////////
-
-//go:build js && wasm
-
-package storage
-
-import (
-	"encoding/base64"
-	"encoding/json"
-	jww "github.com/spf13/jwalterweatherman"
-	"gitlab.com/elixxir/xxdk-wasm/utils"
-	"os"
-	"strings"
-	"syscall/js"
-)
-
-// localStorageWasmPrefix is prefixed to every keyName saved to local storage by
-// LocalStorage. It allows the identifications and deletion of keys only created
-// by this WASM binary while ignoring keys made by other scripts on the same
-// page.
-const localStorageWasmPrefix = "xxdkWasmStorage/"
-
-// LocalStorage contains the js.Value representation of localStorage.
-type LocalStorage struct {
-	// The Javascript value containing the localStorage object
-	v js.Value
-
-	// The prefix appended to each key name. This is so that all keys created by
-	// this structure can be deleted without affecting other keys in local
-	// storage.
-	prefix string
-}
-
-// jsStorage is the global that stores Javascript as window.localStorage.
-//
-//   - Specification:
-//     https://html.spec.whatwg.org/multipage/webstorage.html#dom-localstorage-dev
-//   - Documentation:
-//     https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage
-var jsStorage = newLocalStorage(localStorageWasmPrefix)
-
-// newLocalStorage creates a new LocalStorage object with the specified prefix.
-func newLocalStorage(prefix string) *LocalStorage {
-	return &LocalStorage{
-		v:      js.Global().Get("localStorage"),
-		prefix: prefix,
-	}
-}
-
-// GetLocalStorage returns Javascript's local storage.
-func GetLocalStorage() *LocalStorage {
-	return jsStorage
-}
-
-// GetItem returns a key's value from the local storage given its name. Returns
-// os.ErrNotExist if the key does not exist. Underneath, it calls
-// localStorage.GetItem().
-//
-//   - Specification:
-//     https://html.spec.whatwg.org/multipage/webstorage.html#dom-storage-getitem-dev
-//   - Documentation:
-//     https://developer.mozilla.org/en-US/docs/Web/API/Storage/getItem
-func (ls *LocalStorage) GetItem(keyName string) ([]byte, error) {
-	keyValue := ls.getItem(ls.prefix + keyName)
-	if keyValue.IsNull() {
-		return nil, os.ErrNotExist
-	}
-
-	decodedKeyValue, err := base64.StdEncoding.DecodeString(keyValue.String())
-	if err != nil {
-		return nil, err
-	}
-
-	return decodedKeyValue, nil
-}
-
-// SetItem adds a key's value to local storage given its name. Underneath, it
-// calls localStorage.SetItem().
-//
-//   - Specification:
-//     https://html.spec.whatwg.org/multipage/webstorage.html#dom-storage-setitem-dev
-//   - Documentation:
-//     https://developer.mozilla.org/en-US/docs/Web/API/Storage/setItem
-func (ls *LocalStorage) SetItem(keyName string, keyValue []byte) {
-	encodedKeyValue := base64.StdEncoding.EncodeToString(keyValue)
-	ls.setItem(ls.prefix+keyName, encodedKeyValue)
-}
-
-// RemoveItem removes a key's value from local storage given its name. If there
-// is no item with the given key, this function does nothing. Underneath, it
-// calls localStorage.RemoveItem().
-//
-//   - Specification:
-//     https://html.spec.whatwg.org/multipage/webstorage.html#dom-storage-removeitem-dev
-//   - Documentation:
-//     https://developer.mozilla.org/en-US/docs/Web/API/Storage/removeItem
-func (ls *LocalStorage) RemoveItem(keyName string) {
-	ls.removeItem(ls.prefix + keyName)
-}
-
-// Clear clears all the keys in storage. Underneath, it calls
-// localStorage.clear().
-//
-//   - Specification:
-//     https://html.spec.whatwg.org/multipage/webstorage.html#dom-storage-clear-dev
-//   - Documentation:
-//     https://developer.mozilla.org/en-US/docs/Web/API/Storage/clear
-func (ls *LocalStorage) Clear() {
-	ls.clear()
-}
-
-// ClearPrefix clears all keys with the given prefix.  Returns the number of
-// keys cleared.
-func (ls *LocalStorage) ClearPrefix(prefix string) int {
-	// Get a copy of all key names at once
-	keys := ls.keys()
-
-	// Loop through each key
-	var n int
-	for i := 0; i < keys.Length(); i++ {
-		if v := keys.Index(i); !v.IsNull() {
-			keyName := strings.TrimPrefix(v.String(), ls.prefix)
-			if strings.HasPrefix(keyName, prefix) {
-				ls.removeItem(v.String())
-				n++
-			}
-		}
-	}
-
-	return n
-}
-
-// ClearWASM clears all the keys in storage created by WASM. Returns the number
-// of keys cleared.
-func (ls *LocalStorage) ClearWASM() int {
-	// Get a copy of all key names at once
-	keys := ls.keys()
-
-	// Loop through each key
-	var n int
-	for i := 0; i < keys.Length(); i++ {
-		if v := keys.Index(i); !v.IsNull() {
-			keyName := v.String()
-			if strings.HasPrefix(keyName, ls.prefix) {
-				ls.RemoveItem(strings.TrimPrefix(keyName, ls.prefix))
-				n++
-			}
-		}
-	}
-
-	return n
-}
-
-// Key returns the name of the nth key in localStorage. Return os.ErrNotExist if
-// the key does not exist. The order of keys is not defined. If there is no item
-// with the given key, this function does nothing. Underneath, it calls
-// localStorage.key().
-//
-//   - Specification:
-//     https://html.spec.whatwg.org/multipage/webstorage.html#dom-storage-key-dev
-//   - Documentation:
-//     https://developer.mozilla.org/en-US/docs/Web/API/Storage/key
-func (ls *LocalStorage) Key(n int) (string, error) {
-	keyName := ls.key(n)
-	if keyName.IsNull() {
-		return "", os.ErrNotExist
-	}
-
-	return strings.TrimPrefix(keyName.String(), ls.prefix), nil
-}
-
-// Keys returns a list of all key names in local storage.
-func (ls *LocalStorage) Keys() []string {
-	keyNamesJson := utils.JSON.Call("stringify", ls.keys())
-
-	var keyNames []string
-	err := json.Unmarshal([]byte(keyNamesJson.String()), &keyNames)
-	if err != nil {
-		jww.FATAL.Panicf(
-			"Failed to JSON unmarshal localStorage key name list: %+v", err)
-	}
-
-	return keyNames
-}
-
-// Length returns the number of keys in localStorage. Underneath, it accesses
-// the property localStorage.length.
-//
-//   - Specification:
-//     https://html.spec.whatwg.org/multipage/webstorage.html#dom-storage-key-dev
-//   - Documentation:
-//     https://developer.mozilla.org/en-US/docs/Web/API/Storage/length
-func (ls *LocalStorage) Length() int {
-	return ls.length().Int()
-}
-
-// Wrappers for Javascript Storage methods and properties.
-func (ls *LocalStorage) getItem(keyName string) js.Value  { return ls.v.Call("getItem", keyName) }
-func (ls *LocalStorage) setItem(keyName, keyValue string) { ls.v.Call("setItem", keyName, keyValue) }
-func (ls *LocalStorage) removeItem(keyName string)        { ls.v.Call("removeItem", keyName) }
-func (ls *LocalStorage) clear()                           { ls.v.Call("clear") }
-func (ls *LocalStorage) key(n int) js.Value               { return ls.v.Call("key", n) }
-func (ls *LocalStorage) length() js.Value                 { return ls.v.Get("length") }
-func (ls *LocalStorage) keys() js.Value                   { return utils.Object.Call("keys", ls.v) }
diff --git a/storage/localStorage_test.go b/storage/localStorage_test.go
deleted file mode 100644
index 20e424108af9b7ed64c9c4ba00e348b121beefc4..0000000000000000000000000000000000000000
--- a/storage/localStorage_test.go
+++ /dev/null
@@ -1,271 +0,0 @@
-////////////////////////////////////////////////////////////////////////////////
-// Copyright © 2022 xx foundation                                             //
-//                                                                            //
-// Use of this source code is governed by a license that can be found in the  //
-// LICENSE file.                                                              //
-////////////////////////////////////////////////////////////////////////////////
-
-//go:build js && wasm
-
-package storage
-
-import (
-	"bytes"
-	"github.com/pkg/errors"
-	"math/rand"
-	"os"
-	"strconv"
-	"testing"
-)
-
-// Tests that a value set with LocalStorage.SetItem and retrieved with
-// LocalStorage.GetItem matches the original.
-func TestLocalStorage_GetItem_SetItem(t *testing.T) {
-	values := map[string][]byte{
-		"key1": []byte("key value"),
-		"key2": {0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
-		"key3": {0, 49, 0, 0, 0, 38, 249, 93, 242, 189, 222, 32, 138, 248, 121,
-			151, 42, 108, 82, 199, 163, 61, 4, 200, 140, 231, 225, 20, 35, 243,
-			253, 161, 61, 2, 227, 208, 173, 183, 33, 66, 236, 107, 105, 119, 26,
-			42, 44, 60, 109, 172, 38, 47, 220, 17, 129, 4, 234, 241, 141, 81,
-			84, 185, 32, 120, 115, 151, 128, 196, 143, 117, 222, 78, 44, 115,
-			109, 20, 249, 46, 158, 139, 231, 157, 54, 219, 141, 252},
-	}
-
-	for keyName, keyValue := range values {
-		jsStorage.SetItem(keyName, keyValue)
-
-		loadedValue, err := jsStorage.GetItem(keyName)
-		if err != nil {
-			t.Errorf("Failed to load %q: %+v", keyName, err)
-		}
-
-		if !bytes.Equal(keyValue, loadedValue) {
-			t.Errorf("Loaded value does not match original for %q"+
-				"\nexpected: %q\nreceived: %q", keyName, keyValue, loadedValue)
-		}
-	}
-}
-
-// Tests that LocalStorage.GetItem returns the error os.ErrNotExist when the key
-// does not exist in storage.
-func TestLocalStorage_GetItem_NotExistError(t *testing.T) {
-	_, err := jsStorage.GetItem("someKey")
-	if err == nil || !errors.Is(err, os.ErrNotExist) {
-		t.Errorf("Incorrect error for non existant key."+
-			"\nexpected: %v\nreceived: %v", os.ErrNotExist, err)
-	}
-}
-
-// Tests that LocalStorage.RemoveItem deletes a key from store and that it
-// cannot be retrieved.
-func TestLocalStorage_RemoveItem(t *testing.T) {
-	keyName := "key"
-	jsStorage.SetItem(keyName, []byte("value"))
-	jsStorage.RemoveItem(keyName)
-
-	_, err := jsStorage.GetItem(keyName)
-	if err == nil || !errors.Is(err, os.ErrNotExist) {
-		t.Errorf("Failed to remove %q: %+v", keyName, err)
-	}
-}
-
-// Tests that LocalStorage.Clear deletes all keys from storage.
-func TestLocalStorage_Clear(t *testing.T) {
-	for i := 0; i < 10; i++ {
-		jsStorage.SetItem(strconv.Itoa(i), []byte(strconv.Itoa(i)))
-	}
-
-	jsStorage.Clear()
-
-	l := jsStorage.Length()
-
-	if l > 0 {
-		t.Errorf("Clear did not delete all keys. Found %d keys.", l)
-	}
-}
-
-// Tests that LocalStorage.ClearPrefix deletes only the keys with the given
-// prefix.
-func TestLocalStorage_ClearPrefix(t *testing.T) {
-	s := newLocalStorage("")
-	s.clear()
-	prng := rand.New(rand.NewSource(11))
-	const numKeys = 10
-	var yesPrefix, noPrefix []string
-	prefix := "keyNamePrefix/"
-
-	for i := 0; i < numKeys; i++ {
-		keyName := "keyNum" + strconv.Itoa(i)
-		if prng.Intn(2) == 0 {
-			keyName = prefix + keyName
-			yesPrefix = append(yesPrefix, keyName)
-		} else {
-			noPrefix = append(noPrefix, keyName)
-		}
-
-		s.SetItem(keyName, []byte(strconv.Itoa(i)))
-	}
-
-	n := s.ClearPrefix(prefix)
-	if n != numKeys/2 {
-		t.Errorf("Incorrect number of keys.\nexpected: %d\nreceived: %d",
-			numKeys/2, n)
-	}
-
-	for _, keyName := range noPrefix {
-		if _, err := s.GetItem(keyName); err != nil {
-			t.Errorf("Could not get keyName %q: %+v", keyName, err)
-		}
-	}
-	for _, keyName := range yesPrefix {
-		keyValue, err := s.GetItem(keyName)
-		if err == nil || !errors.Is(err, os.ErrNotExist) {
-			t.Errorf("Found keyName %q: %q", keyName, keyValue)
-		}
-	}
-}
-
-// Tests that LocalStorage.ClearWASM deletes all the WASM keys from storage and
-// does not remove any others
-func TestLocalStorage_ClearWASM(t *testing.T) {
-	jsStorage.clear()
-	prng := rand.New(rand.NewSource(11))
-	const numKeys = 10
-	var yesPrefix, noPrefix []string
-
-	for i := 0; i < numKeys; i++ {
-		keyName := "keyNum" + strconv.Itoa(i)
-		if prng.Intn(2) == 0 {
-			yesPrefix = append(yesPrefix, keyName)
-			jsStorage.SetItem(keyName, []byte(strconv.Itoa(i)))
-		} else {
-			noPrefix = append(noPrefix, keyName)
-			jsStorage.setItem(keyName, strconv.Itoa(i))
-		}
-	}
-
-	n := jsStorage.ClearWASM()
-	if n != numKeys/2 {
-		t.Errorf("Incorrect number of keys.\nexpected: %d\nreceived: %d",
-			numKeys/2, n)
-	}
-
-	for _, keyName := range noPrefix {
-		if v := jsStorage.getItem(keyName); v.IsNull() {
-			t.Errorf("Could not get keyName %q.", keyName)
-		}
-	}
-	for _, keyName := range yesPrefix {
-		keyValue, err := jsStorage.GetItem(keyName)
-		if err == nil || !errors.Is(err, os.ErrNotExist) {
-			t.Errorf("Found keyName %q: %q", keyName, keyValue)
-		}
-	}
-}
-
-// Tests that LocalStorage.Key return all added keys when looping through all
-// indexes.
-func TestLocalStorage_Key(t *testing.T) {
-	jsStorage.clear()
-	values := map[string][]byte{
-		"key1": []byte("key value"),
-		"key2": {0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
-		"key3": {0, 49, 0, 0, 0, 38, 249, 93},
-	}
-
-	for keyName, keyValue := range values {
-		jsStorage.SetItem(keyName, keyValue)
-	}
-
-	numKeys := len(values)
-	for i := 0; i < numKeys; i++ {
-		keyName, err := jsStorage.Key(i)
-		if err != nil {
-			t.Errorf("No key found for index %d: %+v", i, err)
-		}
-
-		if _, exists := values[keyName]; !exists {
-			t.Errorf("No key with name %q added to storage.", keyName)
-		}
-		delete(values, keyName)
-	}
-
-	if len(values) != 0 {
-		t.Errorf("%d keys not read from storage: %q", len(values), values)
-	}
-}
-
-// Tests that LocalStorage.Key returns the error os.ErrNotExist when the index
-// is greater than or equal to the number of keys.
-func TestLocalStorage_Key_NotExistError(t *testing.T) {
-	jsStorage.clear()
-	jsStorage.SetItem("key", []byte("value"))
-
-	_, err := jsStorage.Key(1)
-	if err == nil || !errors.Is(err, os.ErrNotExist) {
-		t.Errorf("Incorrect error for non existant key index."+
-			"\nexpected: %v\nreceived: %v", os.ErrNotExist, err)
-	}
-
-	_, err = jsStorage.Key(2)
-	if err == nil || !errors.Is(err, os.ErrNotExist) {
-		t.Errorf("Incorrect error for non existant key index."+
-			"\nexpected: %v\nreceived: %v", os.ErrNotExist, err)
-	}
-}
-
-// Tests that LocalStorage.Length returns the correct Length when adding and
-// removing various keys.
-func TestLocalStorage_Length(t *testing.T) {
-	jsStorage.clear()
-	values := map[string][]byte{
-		"key1": []byte("key value"),
-		"key2": {0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
-		"key3": {0, 49, 0, 0, 0, 38, 249, 93},
-	}
-
-	i := 0
-	for keyName, keyValue := range values {
-		jsStorage.SetItem(keyName, keyValue)
-		i++
-
-		if jsStorage.Length() != i {
-			t.Errorf("Incorrect length.\nexpected: %d\nreceived: %d",
-				i, jsStorage.Length())
-		}
-	}
-
-	i = len(values)
-	for keyName := range values {
-		jsStorage.RemoveItem(keyName)
-		i--
-
-		if jsStorage.Length() != i {
-			t.Errorf("Incorrect length.\nexpected: %d\nreceived: %d",
-				i, jsStorage.Length())
-		}
-	}
-}
-
-// Tests that LocalStorage.Keys return a list that contains all the added keys.
-func TestLocalStorage_Keys(t *testing.T) {
-	s := newLocalStorage("")
-	s.clear()
-	values := map[string][]byte{
-		"key1": []byte("key value"),
-		"key2": {0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
-		"key3": {0, 49, 0, 0, 0, 38, 249, 93},
-	}
-
-	for keyName, keyValue := range values {
-		s.SetItem(keyName, keyValue)
-	}
-
-	keys := s.Keys()
-	for i, keyName := range keys {
-		if _, exists := values[keyName]; !exists {
-			t.Errorf("Key %q does not exist (%d).", keyName, i)
-		}
-	}
-}
diff --git a/storage/password.go b/storage/password.go
index 36175e1002dbe3343a32fa41a9af239a9e13a7aa..7be49b310b54b7aa2764678f35d0b91550d2c491 100644
--- a/storage/password.go
+++ b/storage/password.go
@@ -12,16 +12,21 @@ package storage
 import (
 	"crypto/cipher"
 	"encoding/json"
-	"github.com/pkg/errors"
-	jww "github.com/spf13/jwalterweatherman"
-	"gitlab.com/elixxir/xxdk-wasm/utils"
-	"gitlab.com/xx_network/crypto/csprng"
-	"golang.org/x/crypto/argon2"
-	"golang.org/x/crypto/blake2b"
-	"golang.org/x/crypto/chacha20poly1305"
 	"io"
 	"os"
 	"syscall/js"
+
+	"golang.org/x/crypto/argon2"
+	"golang.org/x/crypto/blake2b"
+	"golang.org/x/crypto/chacha20poly1305"
+
+	"github.com/pkg/errors"
+	jww "github.com/spf13/jwalterweatherman"
+
+	"gitlab.com/elixxir/wasm-utils/exception"
+	"gitlab.com/elixxir/wasm-utils/storage"
+	"gitlab.com/elixxir/wasm-utils/utils"
+	"gitlab.com/xx_network/crypto/csprng"
 )
 
 // Data lengths.
@@ -91,7 +96,7 @@ const (
 func GetOrInitPassword(_ js.Value, args []js.Value) any {
 	internalPassword, err := getOrInit(args[0].String())
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -109,7 +114,7 @@ func GetOrInitPassword(_ js.Value, args []js.Value) any {
 func ChangeExternalPassword(_ js.Value, args []js.Value) any {
 	err := changeExternalPassword(args[0].String(), args[1].String())
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -130,7 +135,7 @@ func VerifyPassword(_ js.Value, args []js.Value) any {
 // getOrInit is the private function for GetOrInitPassword that is used for
 // testing.
 func getOrInit(externalPassword string) ([]byte, error) {
-	localStorage := GetLocalStorage()
+	localStorage := storage.GetLocalStorage()
 	internalPassword, err := getInternalPassword(externalPassword, localStorage)
 	if err != nil {
 		if errors.Is(err, os.ErrNotExist) {
@@ -148,7 +153,7 @@ func getOrInit(externalPassword string) ([]byte, error) {
 // changeExternalPassword is the private function for ChangeExternalPassword
 // that is used for testing.
 func changeExternalPassword(oldExternalPassword, newExternalPassword string) error {
-	localStorage := GetLocalStorage()
+	localStorage := storage.GetLocalStorage()
 	internalPassword, err := getInternalPassword(
 		oldExternalPassword, localStorage)
 	if err != nil {
@@ -159,13 +164,17 @@ func changeExternalPassword(oldExternalPassword, newExternalPassword string) err
 	if err != nil {
 		return err
 	}
-	localStorage.SetItem(saltKey, salt)
+	if err = localStorage.Set(saltKey, salt); err != nil {
+		return errors.Wrapf(err, "localStorage: failed to set %q", saltKey)
+	}
 
 	key := deriveKey(newExternalPassword, salt, defaultParams())
 
 	encryptedInternalPassword := encryptPassword(
 		internalPassword, key, csprng.NewSystemRNG())
-	localStorage.SetItem(passwordKey, encryptedInternalPassword)
+	if err = localStorage.Set(passwordKey, encryptedInternalPassword); err != nil {
+		return errors.Wrapf(err, "localStorage: failed to set %q", passwordKey)
+	}
 
 	return nil
 }
@@ -173,14 +182,14 @@ func changeExternalPassword(oldExternalPassword, newExternalPassword string) err
 // verifyPassword is the private function for VerifyPassword that is used for
 // testing.
 func verifyPassword(externalPassword string) bool {
-	_, err := getInternalPassword(externalPassword, GetLocalStorage())
+	_, err := getInternalPassword(externalPassword, storage.GetLocalStorage())
 	return err == nil
 }
 
 // initInternalPassword generates a new internal password, stores an encrypted
 // version in local storage, and returns it.
 func initInternalPassword(externalPassword string,
-	localStorage *LocalStorage, csprng io.Reader,
+	localStorage *storage.LocalStorage, csprng io.Reader,
 	params argonParams) ([]byte, error) {
 	internalPassword := make([]byte, internalPasswordLen)
 
@@ -198,19 +207,28 @@ func initInternalPassword(externalPassword string,
 	if err != nil {
 		return nil, err
 	}
-	localStorage.SetItem(saltKey, salt)
+	if err = localStorage.Set(saltKey, salt); err != nil {
+		return nil,
+			errors.Wrapf(err, "localStorage: failed to set %q", saltKey)
+	}
 
 	// Store argon2 parameters
 	paramsData, err := json.Marshal(params)
 	if err != nil {
 		return nil, err
 	}
-	localStorage.SetItem(argonParamsKey, paramsData)
+	if err = localStorage.Set(argonParamsKey, paramsData); err != nil {
+		return nil,
+			errors.Wrapf(err, "localStorage: failed to set %q", argonParamsKey)
+	}
 
 	key := deriveKey(externalPassword, salt, params)
 
 	encryptedInternalPassword := encryptPassword(internalPassword, key, csprng)
-	localStorage.SetItem(passwordKey, encryptedInternalPassword)
+	if err = localStorage.Set(passwordKey, encryptedInternalPassword); err != nil {
+		return nil,
+			errors.Wrapf(err, "localStorage: failed to set %q", passwordKey)
+	}
 
 	return internalPassword, nil
 }
@@ -218,18 +236,18 @@ func initInternalPassword(externalPassword string,
 // getInternalPassword retrieves the internal password from local storage,
 // decrypts it, and returns it.
 func getInternalPassword(
-	externalPassword string, localStorage *LocalStorage) ([]byte, error) {
-	encryptedInternalPassword, err := localStorage.GetItem(passwordKey)
+	externalPassword string, localStorage *storage.LocalStorage) ([]byte, error) {
+	encryptedInternalPassword, err := localStorage.Get(passwordKey)
 	if err != nil {
 		return nil, errors.WithMessage(err, getPasswordStorageErr)
 	}
 
-	salt, err := localStorage.GetItem(saltKey)
+	salt, err := localStorage.Get(saltKey)
 	if err != nil {
 		return nil, errors.WithMessage(err, getSaltStorageErr)
 	}
 
-	paramsData, err := localStorage.GetItem(argonParamsKey)
+	paramsData, err := localStorage.Get(argonParamsKey)
 	if err != nil {
 		return nil, errors.WithMessage(err, getParamsStorageErr)
 	}
diff --git a/storage/password_test.go b/storage/password_test.go
index 24f1ed035197a1856c9f1750d2072609d9feb9c1..0e4c4c64105e93247cf24d1665360fdbd834a687 100644
--- a/storage/password_test.go
+++ b/storage/password_test.go
@@ -14,9 +14,11 @@ import (
 	"crypto/rand"
 	"encoding/base64"
 	"fmt"
-	"gitlab.com/xx_network/crypto/csprng"
 	"strings"
 	"testing"
+
+	"gitlab.com/elixxir/wasm-utils/storage"
+	"gitlab.com/xx_network/crypto/csprng"
 )
 
 // Tests that running getOrInit twice returns the same internal password both
@@ -77,7 +79,7 @@ func Test_changeExternalPassword(t *testing.T) {
 // Tests that verifyPassword returns true for a valid password and false for an
 // invalid password
 func Test_verifyPassword(t *testing.T) {
-	GetLocalStorage().Clear()
+	storage.GetLocalStorage().Clear()
 	externalPassword := "myPassword"
 
 	if _, err := getOrInit(externalPassword); err != nil {
@@ -97,7 +99,7 @@ func Test_verifyPassword(t *testing.T) {
 // the encrypted one saved to local storage.
 func Test_initInternalPassword(t *testing.T) {
 	externalPassword := "myPassword"
-	ls := GetLocalStorage()
+	ls := storage.GetLocalStorage()
 	rng := csprng.NewSystemRNG()
 
 	internalPassword, err := initInternalPassword(
@@ -107,14 +109,14 @@ func Test_initInternalPassword(t *testing.T) {
 	}
 
 	// Attempt to retrieve encrypted internal password from storage
-	encryptedInternalPassword, err := ls.GetItem(passwordKey)
+	encryptedInternalPassword, err := ls.Get(passwordKey)
 	if err != nil {
 		t.Errorf(
 			"Failed to load encrypted internal password from storage: %+v", err)
 	}
 
 	// Attempt to retrieve salt from storage
-	salt, err := ls.GetItem(saltKey)
+	salt, err := ls.Get(saltKey)
 	if err != nil {
 		t.Errorf("Failed to load salt from storage: %+v", err)
 	}
@@ -138,7 +140,7 @@ func Test_initInternalPassword(t *testing.T) {
 // error when read.
 func Test_initInternalPassword_CsprngReadError(t *testing.T) {
 	externalPassword := "myPassword"
-	ls := GetLocalStorage()
+	ls := storage.GetLocalStorage()
 	b := bytes.NewBuffer([]byte{})
 
 	expectedErr := strings.Split(readInternalPasswordErr, "%")[0]
@@ -154,7 +156,7 @@ func Test_initInternalPassword_CsprngReadError(t *testing.T) {
 // return enough bytes.
 func Test_initInternalPassword_CsprngReadNumBytesError(t *testing.T) {
 	externalPassword := "myPassword"
-	ls := GetLocalStorage()
+	ls := storage.GetLocalStorage()
 	b := bytes.NewBuffer(make([]byte, internalPasswordLen/2))
 
 	expectedErr := fmt.Sprintf(
@@ -171,7 +173,7 @@ func Test_initInternalPassword_CsprngReadNumBytesError(t *testing.T) {
 // to local storage by initInternalPassword.
 func Test_getInternalPassword(t *testing.T) {
 	externalPassword := "myPassword"
-	ls := GetLocalStorage()
+	ls := storage.GetLocalStorage()
 	rng := csprng.NewSystemRNG()
 
 	internalPassword, err := initInternalPassword(
@@ -196,7 +198,7 @@ func Test_getInternalPassword(t *testing.T) {
 // loaded from local storage.
 func Test_getInternalPassword_LocalStorageGetPasswordError(t *testing.T) {
 	externalPassword := "myPassword"
-	ls := GetLocalStorage()
+	ls := storage.GetLocalStorage()
 	ls.Clear()
 
 	expectedErr := strings.Split(getPasswordStorageErr, "%")[0]
@@ -212,9 +214,11 @@ func Test_getInternalPassword_LocalStorageGetPasswordError(t *testing.T) {
 // loaded from local storage.
 func Test_getInternalPassword_LocalStorageGetError(t *testing.T) {
 	externalPassword := "myPassword"
-	ls := GetLocalStorage()
+	ls := storage.GetLocalStorage()
 	ls.Clear()
-	ls.SetItem(passwordKey, []byte("password"))
+	if err := ls.Set(passwordKey, []byte("password")); err != nil {
+		t.Fatalf("Failed to set %q: %+v", passwordKey, err)
+	}
 
 	expectedErr := strings.Split(getSaltStorageErr, "%")[0]
 
@@ -229,11 +233,17 @@ func Test_getInternalPassword_LocalStorageGetError(t *testing.T) {
 // decrypted.
 func Test_getInternalPassword_DecryptPasswordError(t *testing.T) {
 	externalPassword := "myPassword"
-	ls := GetLocalStorage()
+	ls := storage.GetLocalStorage()
 	ls.Clear()
-	ls.SetItem(saltKey, []byte("salt"))
-	ls.SetItem(passwordKey, []byte("password"))
-	ls.SetItem(argonParamsKey, []byte(`{"Time": 1, "Memory": 65536, "Threads": 4}`))
+	if err := ls.Set(saltKey, []byte("salt")); err != nil {
+		t.Errorf("failed to set %q: %+v", saltKey, err)
+	}
+	if err := ls.Set(passwordKey, []byte("password")); err != nil {
+		t.Errorf("failed to set %q: %+v", passwordKey, err)
+	}
+	if err := ls.Set(argonParamsKey, []byte(`{"Time": 1, "Memory": 65536, "Threads": 4}`)); err != nil {
+		t.Errorf("failed to set %q: %+v", argonParamsKey, err)
+	}
 
 	expectedErr := strings.Split(decryptPasswordErr, "%")[0]
 
diff --git a/storage/purge.go b/storage/purge.go
index b80481b470863c32dee6cad894858cb081a08abe..df75cebf9175251d53c722565e7c1111d6d9262f 100644
--- a/storage/purge.go
+++ b/storage/purge.go
@@ -10,13 +10,15 @@
 package storage
 
 import (
+	"sync/atomic"
+	"syscall/js"
+
 	"github.com/hack-pad/go-indexeddb/idb"
-	"github.com/pkg/errors"
 	jww "github.com/spf13/jwalterweatherman"
+
 	"gitlab.com/elixxir/client/v4/storage/utility"
-	"gitlab.com/elixxir/xxdk-wasm/utils"
-	"sync/atomic"
-	"syscall/js"
+	"gitlab.com/elixxir/wasm-utils/exception"
+	"gitlab.com/elixxir/wasm-utils/storage"
 )
 
 // numClientsRunning is an atomic that tracks the current number of Cmix
@@ -49,7 +51,7 @@ func DecrementNumClientsRunning() {
 //     passed into [wasm.NewCmix].
 //
 // Returns:
-//   - Throws a TypeError if the password is incorrect or if not all cMix
+//   - Throws an error if the password is incorrect or if not all cMix
 //     followers have been stopped.
 func Purge(_ js.Value, args []js.Value) any {
 	storageDirectory := args[0].String()
@@ -57,22 +59,21 @@ func Purge(_ js.Value, args []js.Value) any {
 
 	// Check the password
 	if !verifyPassword(userPassword) {
-		utils.Throw(utils.TypeError, errors.New("invalid password"))
+		exception.Throwf("invalid password")
 		return nil
 	}
 
 	// Verify all Cmix followers are stopped
 	if n := atomic.LoadUint64(&numClientsRunning); n != 0 {
-		utils.Throw(utils.TypeError, errors.Errorf(
-			"%d cMix followers running; all need to be stopped", n))
+		exception.Throwf("%d cMix followers running; all need to be stopped", n)
 		return nil
 	}
 
 	// Get all indexedDb database names
 	databaseList, err := GetIndexedDbList()
 	if err != nil {
-		utils.Throw(utils.TypeError, errors.Errorf(
-			"failed to get list of indexedDb database names: %+v", err))
+		exception.Throwf(
+			"failed to get list of indexedDb database names: %+v", err)
 		return nil
 	}
 	jww.DEBUG.Printf("[PURGE] Found %d databases to delete: %s",
@@ -82,26 +83,34 @@ func Purge(_ js.Value, args []js.Value) any {
 	for dbName := range databaseList {
 		_, err = idb.Global().DeleteDatabase(dbName)
 		if err != nil {
-			utils.Throw(utils.TypeError, errors.Errorf(
-				"failed to delete indexedDb database %q: %+v", dbName, err))
+			exception.Throwf(
+				"failed to delete indexedDb database %q: %+v", dbName, err)
 			return nil
 		}
 	}
 
 	// Get local storage
-	ls := GetLocalStorage()
+	ls := storage.GetLocalStorage()
 
 	// Clear all local storage saved by this WASM project
-	n := ls.ClearWASM()
+	n := ls.Clear()
 	jww.DEBUG.Printf("[PURGE] Cleared %d WASM keys in local storage", n)
 
 	// Clear all EKV from local storage
-	n = ls.ClearPrefix(storageDirectory)
+	keys := ls.LocalStorageUNSAFE().KeysPrefix(storageDirectory)
+	n = len(keys)
+	for _, keyName := range keys {
+		ls.LocalStorageUNSAFE().RemoveItem(keyName)
+	}
 	jww.DEBUG.Printf("[PURGE] Cleared %d keys with the prefix %q (for EKV)",
 		n, storageDirectory)
 
 	// Clear all NDFs saved to local storage
-	n = ls.ClearPrefix(utility.NdfStorageKeyNamePrefix)
+	keys = ls.LocalStorageUNSAFE().KeysPrefix(utility.NdfStorageKeyNamePrefix)
+	n = len(keys)
+	for _, keyName := range keys {
+		ls.LocalStorageUNSAFE().RemoveItem(keyName)
+	}
 	jww.DEBUG.Printf("[PURGE] Cleared %d keys with the prefix %q (for NDF)",
 		n, utility.NdfStorageKeyNamePrefix)
 
diff --git a/storage/version.go b/storage/version.go
index b0f192ab24f8ad80d1fb9a9a5c96177f67545440..5fa7e439af29a96c66b22c92b86db42c384c121f 100644
--- a/storage/version.go
+++ b/storage/version.go
@@ -17,6 +17,7 @@ import (
 	jww "github.com/spf13/jwalterweatherman"
 
 	"gitlab.com/elixxir/client/v4/bindings"
+	"gitlab.com/elixxir/wasm-utils/storage"
 )
 
 // SEMVER is the current semantic version of xxDK WASM.
@@ -35,11 +36,11 @@ const (
 // On first load, only the xxDK WASM and xxDK client versions are stored.
 func CheckAndStoreVersions() error {
 	return checkAndStoreVersions(
-		SEMVER, bindings.GetVersion(), GetLocalStorage())
+		SEMVER, bindings.GetVersion(), storage.GetLocalStorage())
 }
 
 func checkAndStoreVersions(
-	currentWasmVer, currentClientVer string, ls *LocalStorage) error {
+	currentWasmVer, currentClientVer string, ls *storage.LocalStorage) error {
 	// Get the stored client version, if it exists
 	storedClientVer, err :=
 		initOrLoadStoredSemver(clientVerKey, currentClientVer, ls)
@@ -76,8 +77,12 @@ func checkAndStoreVersions(
 	// Upgrade path code goes here
 
 	// Save current versions
-	ls.SetItem(clientVerKey, []byte(currentClientVer))
-	ls.SetItem(semverKey, []byte(currentWasmVer))
+	if err = ls.Set(clientVerKey, []byte(currentClientVer)); err != nil {
+		return errors.Wrapf(err, "localStorage: failed to set %q", clientVerKey)
+	}
+	if err = ls.Set(semverKey, []byte(currentWasmVer)); err != nil {
+		return errors.Wrapf(err, "localStorage: failed to set %q", semverKey)
+	}
 
 	return nil
 }
@@ -86,13 +91,16 @@ func checkAndStoreVersions(
 // local storage. If no version is stored, then the current version is stored
 // and returned.
 func initOrLoadStoredSemver(
-	key, currentVersion string, ls *LocalStorage) (string, error) {
-	storedVersion, err := ls.GetItem(key)
+	key, currentVersion string, ls *storage.LocalStorage) (string, error) {
+	storedVersion, err := ls.Get(key)
 	if err != nil {
 		if errors.Is(err, os.ErrNotExist) {
 			// Save the current version if this is the first run
 			jww.INFO.Printf("Initialising %s to v%s", key, currentVersion)
-			ls.SetItem(key, []byte(currentVersion))
+			if err = ls.Set(key, []byte(currentVersion)); err != nil {
+				return "",
+					errors.Wrapf(err, "localStorage: failed to set %q", key)
+			}
 			return currentVersion, nil
 		} else {
 			// If the item exists, but cannot be loaded, return an error
diff --git a/storage/version_test.go b/storage/version_test.go
index a8ead72e52a53d9840936a7deb6865d9aa30f201..27043680fea08beb01c6584557335a308d4c457a 100644
--- a/storage/version_test.go
+++ b/storage/version_test.go
@@ -11,12 +11,14 @@ package storage
 
 import (
 	"testing"
+
+	"gitlab.com/elixxir/wasm-utils/storage"
 )
 
 // Tests that checkAndStoreVersions correct initialises the client and WASM
 // versions on first run and upgrades them correctly on subsequent runs.
 func Test_checkAndStoreVersions(t *testing.T) {
-	ls := GetLocalStorage()
+	ls := storage.GetLocalStorage()
 	ls.Clear()
 	oldWasmVer := "0.1"
 	newWasmVer := "1.0"
@@ -28,7 +30,7 @@ func Test_checkAndStoreVersions(t *testing.T) {
 	}
 
 	// Check client version
-	storedClientVer, err := ls.GetItem(clientVerKey)
+	storedClientVer, err := ls.Get(clientVerKey)
 	if err != nil {
 		t.Errorf("Failed to get client version from storage: %+v", err)
 	}
@@ -38,7 +40,7 @@ func Test_checkAndStoreVersions(t *testing.T) {
 	}
 
 	// Check WASM version
-	storedWasmVer, err := ls.GetItem(semverKey)
+	storedWasmVer, err := ls.Get(semverKey)
 	if err != nil {
 		t.Errorf("Failed to get WASM version from storage: %+v", err)
 	}
@@ -53,7 +55,7 @@ func Test_checkAndStoreVersions(t *testing.T) {
 	}
 
 	// Check client version
-	storedClientVer, err = ls.GetItem(clientVerKey)
+	storedClientVer, err = ls.Get(clientVerKey)
 	if err != nil {
 		t.Errorf("Failed to get client version from storage: %+v", err)
 	}
@@ -63,7 +65,7 @@ func Test_checkAndStoreVersions(t *testing.T) {
 	}
 
 	// Check WASM version
-	storedWasmVer, err = ls.GetItem(semverKey)
+	storedWasmVer, err = ls.Get(semverKey)
 	if err != nil {
 		t.Errorf("Failed to get WASM version from storage: %+v", err)
 	}
@@ -76,7 +78,7 @@ func Test_checkAndStoreVersions(t *testing.T) {
 // Tests that initOrLoadStoredSemver initialises the correct version on first
 // run and returns the same version on subsequent runs.
 func Test_initOrLoadStoredSemver(t *testing.T) {
-	ls := GetLocalStorage()
+	ls := storage.GetLocalStorage()
 	key := "testKey"
 	oldVersion := "0.1"
 
diff --git a/test/wasm_exec.js b/test/wasm_exec.js
index c613dfc656f2799b6b3c922f59003cd3347454a7..07649aee63e71d945a3b558a249faa7240be4416 100644
--- a/test/wasm_exec.js
+++ b/test/wasm_exec.js
@@ -503,7 +503,7 @@
 					},
 
 					// func throw(exception string, message string)
-					'gitlab.com/elixxir/xxdk-wasm/utils.throw': (sp) => {
+					'gitlab.com/elixxir/wasm-utils/utils.throw': (sp) => {
 						const exception = loadString(sp + 8)
 						const message = loadString(sp + 24)
 						throw globalThis[exception](message)
diff --git a/utils/array.go b/utils/array.go
deleted file mode 100644
index 3597d0ba9cca229e1141c164d9e1204cf56c117a..0000000000000000000000000000000000000000
--- a/utils/array.go
+++ /dev/null
@@ -1,71 +0,0 @@
-////////////////////////////////////////////////////////////////////////////////
-// Copyright © 2022 xx foundation                                             //
-//                                                                            //
-// Use of this source code is governed by a license that can be found in the  //
-// LICENSE file.                                                              //
-////////////////////////////////////////////////////////////////////////////////
-
-//go:build js && wasm
-
-package utils
-
-import (
-	"bytes"
-	"encoding/base64"
-	"syscall/js"
-)
-
-// Uint8ArrayToBase64 encodes an uint8 array to a base 64 string.
-//
-// Parameters:
-//   - args[0] - Javascript 8-bit unsigned integer array (Uint8Array).
-//
-// Returns:
-//   - Base 64 encoded string (string).
-func Uint8ArrayToBase64(_ js.Value, args []js.Value) any {
-	return base64.StdEncoding.EncodeToString(CopyBytesToGo(args[0]))
-}
-
-// Base64ToUint8Array decodes a base 64 encoded string to a Uint8Array.
-//
-// Parameters:
-//   - args[0] - Base 64 encoded string (string).
-//
-// Returns:
-//   - Javascript 8-bit unsigned integer array (Uint8Array).
-//   - Throws TypeError if decoding the string fails.
-func Base64ToUint8Array(_ js.Value, args []js.Value) any {
-	b, err := base64ToUint8Array(args[0])
-	if err != nil {
-		Throw(TypeError, err)
-	}
-
-	return b
-}
-
-// base64ToUint8Array is a helper function that returns an error instead of
-// throwing it.
-func base64ToUint8Array(base64String js.Value) (js.Value, error) {
-	b, err := base64.StdEncoding.DecodeString(base64String.String())
-	if err != nil {
-		return js.Value{}, err
-	}
-
-	return CopyBytesToJS(b), nil
-}
-
-// Uint8ArrayEquals returns true if the two Uint8Array are equal and false
-// otherwise.
-//
-// Parameters:
-//   - args[0] - Array A (Uint8Array).
-//   - args[1] - Array B (Uint8Array).
-//
-// Returns:
-//   - If the two arrays are equal (boolean).
-func Uint8ArrayEquals(_ js.Value, args []js.Value) any {
-	a := CopyBytesToGo(args[0])
-	b := CopyBytesToGo(args[1])
-
-	return bytes.Equal(a, b)
-}
diff --git a/utils/array_test.go b/utils/array_test.go
deleted file mode 100644
index 0353d1cce9155cf47e4c8e21132809b6d7d1915c..0000000000000000000000000000000000000000
--- a/utils/array_test.go
+++ /dev/null
@@ -1,114 +0,0 @@
-////////////////////////////////////////////////////////////////////////////////
-// Copyright © 2022 xx foundation                                             //
-//                                                                            //
-// Use of this source code is governed by a license that can be found in the  //
-// LICENSE file.                                                              //
-////////////////////////////////////////////////////////////////////////////////
-
-//go:build js && wasm
-
-package utils
-
-import (
-	"encoding/base64"
-	"fmt"
-	"strings"
-	"syscall/js"
-	"testing"
-)
-
-var testBytes = [][]byte{
-	nil,
-	{},
-	{0},
-	{0, 1, 2, 3},
-	{214, 108, 207, 78, 229, 11, 42, 219, 42, 87, 205, 104, 252, 73, 223,
-		229, 145, 209, 79, 111, 34, 96, 238, 127, 11, 105, 114, 62, 239,
-		130, 145, 82, 3},
-}
-
-// Tests that a series of Uint8Array Javascript objects are correctly converted
-// to base 64 strings with Uint8ArrayToBase64.
-func TestUint8ArrayToBase64(t *testing.T) {
-	for i, val := range testBytes {
-		// Create Uint8Array and set each element individually
-		jsBytes := Uint8Array.New(len(val))
-		for j, v := range val {
-			jsBytes.SetIndex(j, v)
-		}
-
-		jsB64 := Uint8ArrayToBase64(js.Value{}, []js.Value{jsBytes})
-
-		expected := base64.StdEncoding.EncodeToString(val)
-
-		if expected != jsB64 {
-			t.Errorf("Did not receive expected base64 encoded string (%d)."+
-				"\nexpected: %s\nreceived: %s", i, expected, jsB64)
-		}
-	}
-}
-
-// Tests that Base64ToUint8Array correctly decodes a series of base 64 encoded
-// strings into Uint8Array.
-func TestBase64ToUint8Array(t *testing.T) {
-	for i, val := range testBytes {
-		b64 := base64.StdEncoding.EncodeToString(val)
-		jsArr, err := base64ToUint8Array(js.ValueOf(b64))
-		if err != nil {
-			t.Errorf("Failed to convert js.Value to base 64: %+v", err)
-		}
-
-		// Generate the expected string to match the output of toString() on a
-		// Uint8Array
-		expected := strings.ReplaceAll(fmt.Sprintf("%d", val), " ", ",")[1:]
-		expected = expected[:len(expected)-1]
-
-		// Get the string value of the Uint8Array
-		jsString := jsArr.Call("toString").String()
-
-		if expected != jsString {
-			t.Errorf("Failed to recevie expected string representation of "+
-				"the Uint8Array (%d).\nexpected: %s\nreceived: %s",
-				i, expected, jsString)
-		}
-	}
-}
-
-// Tests that a base 64 encoded string decoded to Uint8Array via
-// Base64ToUint8Array and back to a base 64 encoded string via
-// Uint8ArrayToBase64 matches the original.
-func TestBase64ToUint8ArrayUint8ArrayToBase64(t *testing.T) {
-	for i, val := range testBytes {
-		b64 := base64.StdEncoding.EncodeToString(val)
-		jsArr, err := base64ToUint8Array(js.ValueOf(b64))
-		if err != nil {
-			t.Errorf("Failed to convert js.Value to base 64: %+v", err)
-		}
-
-		jsB64 := Uint8ArrayToBase64(js.Value{}, []js.Value{jsArr})
-
-		if b64 != jsB64 {
-			t.Errorf("JSON from Uint8Array does not match original (%d)."+
-				"\nexpected: %s\nreceived: %s", i, b64, jsB64)
-		}
-	}
-}
-
-func TestUint8ArrayEquals(t *testing.T) {
-	for i, val := range testBytes {
-		// Create Uint8Array and set each element individually
-		jsBytesA := Uint8Array.New(len(val))
-		for j, v := range val {
-			jsBytesA.SetIndex(j, v)
-		}
-
-		jsBytesB := CopyBytesToJS(val)
-
-		if !Uint8ArrayEquals(js.Value{}, []js.Value{jsBytesA, jsBytesB}).(bool) {
-			t.Errorf("Two equal byte slices were found to be different (%d)."+
-				"\nexpected: %s\nreceived: %s", i,
-				jsBytesA.Call("toString").String(),
-				jsBytesB.Call("toString").String())
-		}
-	}
-}
diff --git a/utils/convert.go b/utils/convert.go
deleted file mode 100644
index b1f2cd10172bca7e88ff6c77931c754ab1b7f1d8..0000000000000000000000000000000000000000
--- a/utils/convert.go
+++ /dev/null
@@ -1,62 +0,0 @@
-////////////////////////////////////////////////////////////////////////////////
-// Copyright © 2022 xx foundation                                             //
-//                                                                            //
-// Use of this source code is governed by a license that can be found in the  //
-// LICENSE file.                                                              //
-////////////////////////////////////////////////////////////////////////////////
-
-//go:build js && wasm
-
-package utils
-
-import (
-	"encoding/json"
-	"syscall/js"
-)
-
-// CopyBytesToGo copies the [Uint8Array] stored in the [js.Value] to []byte.
-// This is a wrapper for [js.CopyBytesToGo] to make it more convenient.
-func CopyBytesToGo(src js.Value) []byte {
-	b := make([]byte, src.Length())
-	js.CopyBytesToGo(b, src)
-	return b
-}
-
-// CopyBytesToJS copies the []byte to a [Uint8Array] stored in a [js.Value].
-// This is a wrapper for [js.CopyBytesToJS] to make it more convenient.
-func CopyBytesToJS(src []byte) js.Value {
-	dst := Uint8Array.New(len(src))
-	js.CopyBytesToJS(dst, src)
-	return dst
-}
-
-// JsToJson converts the Javascript value to JSON.
-func JsToJson(value js.Value) string {
-	if value.IsUndefined() {
-		return "null"
-	}
-
-	return JSON.Call("stringify", value).String()
-}
-
-// JsonToJS converts a JSON bytes input to a [js.Value] of the object subtype.
-func JsonToJS(inputJson []byte) (js.Value, error) {
-	var jsObj map[string]any
-	err := json.Unmarshal(inputJson, &jsObj)
-	if err != nil {
-		return js.ValueOf(nil), err
-	}
-
-	return js.ValueOf(jsObj), nil
-}
-
-// JsErrorToJson converts the Javascript error to JSON. This should be used for
-// all Javascript error objects instead of JsonToJS.
-func JsErrorToJson(value js.Value) string {
-	if value.IsUndefined() {
-		return "null"
-	}
-
-	properties := Object.Call("getOwnPropertyNames", value)
-	return JSON.Call("stringify", value, properties).String()
-}
diff --git a/utils/convert_test.go b/utils/convert_test.go
deleted file mode 100644
index 508c27f783f1365f28b4b7b78b7389fc4d3ff1a6..0000000000000000000000000000000000000000
--- a/utils/convert_test.go
+++ /dev/null
@@ -1,305 +0,0 @@
-////////////////////////////////////////////////////////////////////////////////
-// Copyright © 2022 xx foundation                                             //
-//                                                                            //
-// Use of this source code is governed by a license that can be found in the  //
-// LICENSE file.                                                              //
-////////////////////////////////////////////////////////////////////////////////
-
-//go:build js && wasm
-
-package utils
-
-import (
-	"encoding/base64"
-	"encoding/json"
-	"sort"
-	"syscall/js"
-	"testing"
-)
-
-import (
-	"bytes"
-	"fmt"
-	"strings"
-)
-
-// Tests that CopyBytesToGo returns a byte slice that matches the Uint8Array.
-func TestCopyBytesToGo(t *testing.T) {
-	for i, val := range testBytes {
-		// Create Uint8Array and set each element individually
-		jsBytes := Uint8Array.New(len(val))
-		for j, v := range val {
-			jsBytes.SetIndex(j, v)
-		}
-
-		goBytes := CopyBytesToGo(jsBytes)
-
-		if !bytes.Equal(val, goBytes) {
-			t.Errorf("Failed to recevie expected bytes from Uint8Array (%d)."+
-				"\nexpected: %d\nreceived: %d",
-				i, val, goBytes)
-		}
-	}
-}
-
-// Tests that CopyBytesToJS returns a Javascript Uint8Array with values matching
-// the original byte slice.
-func TestCopyBytesToJS(t *testing.T) {
-	for i, val := range testBytes {
-		jsBytes := CopyBytesToJS(val)
-
-		// Generate the expected string to match the output of toString() on a
-		// Uint8Array
-		expected := strings.ReplaceAll(fmt.Sprintf("%d", val), " ", ",")[1:]
-		expected = expected[:len(expected)-1]
-
-		// Get the string value of the Uint8Array
-		jsString := jsBytes.Call("toString").String()
-
-		if expected != jsString {
-			t.Errorf("Failed to recevie expected string representation of "+
-				"the Uint8Array (%d).\nexpected: %s\nreceived: %s",
-				i, expected, jsString)
-		}
-	}
-}
-
-// Tests that a byte slice converted to Javascript via CopyBytesToJS and
-// converted back to Go via CopyBytesToGo matches the original.
-func TestCopyBytesToJSCopyBytesToGo(t *testing.T) {
-	for i, val := range testBytes {
-		jsBytes := CopyBytesToJS(val)
-		goBytes := CopyBytesToGo(jsBytes)
-
-		if !bytes.Equal(val, goBytes) {
-			t.Errorf("Failed to recevie expected bytes from Uint8Array (%d)."+
-				"\nexpected: %d\nreceived: %d",
-				i, val, goBytes)
-		}
-	}
-
-}
-
-// Tests that JsToJson can convert a Javascript object to JSON that matches the
-// output of json.Marshal on the Go version of the same object.
-func TestJsToJson(t *testing.T) {
-	testObj := map[string]any{
-		"nil":    nil,
-		"bool":   true,
-		"int":    1,
-		"float":  1.5,
-		"string": "I am string",
-		"array":  []any{1, 2, 3},
-		"object": map[string]any{"int": 5},
-	}
-
-	expected, err := json.Marshal(testObj)
-	if err != nil {
-		t.Errorf("Failed to JSON marshal test object: %+v", err)
-	}
-
-	jsJson := JsToJson(js.ValueOf(testObj))
-
-	// Javascript does not return the JSON object fields sorted so the letters
-	// of each Javascript string are sorted and compared
-	er := []rune(string(expected))
-	sort.SliceStable(er, func(i, j int) bool { return er[i] < er[j] })
-	jj := []rune(jsJson)
-	sort.SliceStable(jj, func(i, j int) bool { return jj[i] < jj[j] })
-
-	if string(er) != string(jj) {
-		t.Errorf("Recieved incorrect JSON from Javascript object."+
-			"\nexpected: %s\nreceived: %s", expected, jsJson)
-	}
-}
-
-// Tests that JsToJson return a null object when the Javascript object is
-// undefined.
-func TestJsToJson_Undefined(t *testing.T) {
-	expected, err := json.Marshal(nil)
-	if err != nil {
-		t.Errorf("Failed to JSON marshal test object: %+v", err)
-	}
-
-	jsJson := JsToJson(js.Undefined())
-
-	if string(expected) != jsJson {
-		t.Errorf("Recieved incorrect JSON from Javascript object."+
-			"\nexpected: %s\nreceived: %s", expected, jsJson)
-	}
-}
-
-// Tests that JsonToJS can convert a JSON object with multiple types to a
-// Javascript object and that all values match.
-func TestJsonToJS(t *testing.T) {
-	testObj := map[string]any{
-		"nil":    nil,
-		"bool":   true,
-		"int":    1,
-		"float":  1.5,
-		"string": "I am string",
-		"bytes":  []byte{1, 2, 3},
-		"array":  []any{1, 2, 3},
-		"object": map[string]any{"int": 5},
-	}
-	jsonData, err := json.Marshal(testObj)
-	if err != nil {
-		t.Errorf("Failed to JSON marshal test object: %+v", err)
-	}
-
-	jsObj, err := JsonToJS(jsonData)
-	if err != nil {
-		t.Errorf("Failed to convert JSON to Javascript object: %+v", err)
-	}
-
-	for key, val := range testObj {
-		jsVal := jsObj.Get(key)
-		switch key {
-		case "nil":
-			if !jsVal.IsNull() {
-				t.Errorf("Key %s is not null.", key)
-			}
-		case "bool":
-			if jsVal.Bool() != val {
-				t.Errorf("Incorrect value for key %s."+
-					"\nexpected: %t\nreceived: %t", key, val, jsVal.Bool())
-			}
-		case "int":
-			if jsVal.Int() != val {
-				t.Errorf("Incorrect value for key %s."+
-					"\nexpected: %d\nreceived: %d", key, val, jsVal.Int())
-			}
-		case "float":
-			if jsVal.Float() != val {
-				t.Errorf("Incorrect value for key %s."+
-					"\nexpected: %f\nreceived: %f", key, val, jsVal.Float())
-			}
-		case "string":
-			if jsVal.String() != val {
-				t.Errorf("Incorrect value for key %s."+
-					"\nexpected: %s\nreceived: %s", key, val, jsVal.String())
-			}
-		case "bytes":
-			if jsVal.String() != base64.StdEncoding.EncodeToString(val.([]byte)) {
-				t.Errorf("Incorrect value for key %s."+
-					"\nexpected: %s\nreceived: %s", key,
-					base64.StdEncoding.EncodeToString(val.([]byte)),
-					jsVal.String())
-			}
-		case "array":
-			for i, v := range val.([]any) {
-				if jsVal.Index(i).Int() != v {
-					t.Errorf("Incorrect value for key %s index %d."+
-						"\nexpected: %d\nreceived: %d",
-						key, i, v, jsVal.Index(i).Int())
-				}
-			}
-		case "object":
-			if jsVal.Get("int").Int() != val.(map[string]any)["int"] {
-				t.Errorf("Incorrect value for key %s."+
-					"\nexpected: %d\nreceived: %d", key,
-					val.(map[string]any)["int"], jsVal.Get("int").Int())
-			}
-		}
-	}
-}
-
-// Tests that JSON can be converted to a Javascript object via JsonToJS and back
-// to JSON using JsToJson and matches the original.
-func TestJsonToJSJsToJson(t *testing.T) {
-	testObj := map[string]any{
-		"nil":    nil,
-		"bool":   true,
-		"int":    1,
-		"float":  1.5,
-		"string": "I am string",
-		"bytes":  []byte{1, 2, 3},
-		"array":  []any{1, 2, 3},
-		"object": map[string]any{"int": 5},
-	}
-	jsonData, err := json.Marshal(testObj)
-	if err != nil {
-		t.Errorf("Failed to JSON marshal test object: %+v", err)
-	}
-
-	jsObj, err := JsonToJS(jsonData)
-	if err != nil {
-		t.Errorf("Failed to convert the Javascript object to JSON: %+v", err)
-	}
-
-	jsJson := JsToJson(jsObj)
-
-	// Javascript does not return the JSON object fields sorted so the letters
-	// of each Javascript string are sorted and compared
-	er := []rune(string(jsonData))
-	sort.SliceStable(er, func(i, j int) bool { return er[i] < er[j] })
-	jj := []rune(jsJson)
-	sort.SliceStable(jj, func(i, j int) bool { return jj[i] < jj[j] })
-
-	if string(er) != string(jj) {
-		t.Errorf("JSON from Javascript does not match original."+
-			"\nexpected: %s\nreceived: %s", jsonData, jsJson)
-	}
-}
-
-// Tests that JsErrorToJson can convert a Javascript object to JSON that matches
-// the output of json.Marshal on the Go version of the same object.
-func TestJsErrorToJson(t *testing.T) {
-	testObj := map[string]any{
-		"nil":    nil,
-		"bool":   true,
-		"int":    1,
-		"float":  1.5,
-		"string": "I am string",
-		"array":  []any{1, 2, 3},
-		"object": map[string]any{"int": 5},
-	}
-
-	expected, err := json.Marshal(testObj)
-	if err != nil {
-		t.Errorf("Failed to JSON marshal test object: %+v", err)
-	}
-
-	jsJson := JsErrorToJson(js.ValueOf(testObj))
-
-	// Javascript does not return the JSON object fields sorted so the letters
-	// of each Javascript string are sorted and compared
-	er := []rune(string(expected))
-	sort.SliceStable(er, func(i, j int) bool { return er[i] < er[j] })
-	jj := []rune(jsJson)
-	sort.SliceStable(jj, func(i, j int) bool { return jj[i] < jj[j] })
-
-	if string(er) != string(jj) {
-		t.Errorf("Recieved incorrect JSON from Javascript object."+
-			"\nexpected: %s\nreceived: %s", expected, jsJson)
-	}
-}
-
-// Tests that JsErrorToJson return a null object when the Javascript object is
-// undefined.
-func TestJsErrorToJson_Undefined(t *testing.T) {
-	expected, err := json.Marshal(nil)
-	if err != nil {
-		t.Errorf("Failed to JSON marshal test object: %+v", err)
-	}
-
-	jsJson := JsErrorToJson(js.Undefined())
-
-	if string(expected) != jsJson {
-		t.Errorf("Recieved incorrect JSON from Javascript object."+
-			"\nexpected: %s\nreceived: %s", expected, jsJson)
-	}
-}
-
-// Tests that JsErrorToJson returns a JSON object containing the original error
-// string.
-func TestJsErrorToJson_ErrorObject(t *testing.T) {
-	expected := "An error"
-	jsErr := Error.New(expected)
-	jsJson := JsErrorToJson(jsErr)
-
-	if !strings.Contains(jsJson, expected) {
-		t.Errorf("Recieved incorrect JSON from Javascript error."+
-			"\nexpected: %s\nreceived: %s", expected, jsJson)
-	}
-}
diff --git a/utils/errors.go b/utils/errors.go
deleted file mode 100644
index 2e1cbacbce3b7c70a67f52e1217a76ef63f887b5..0000000000000000000000000000000000000000
--- a/utils/errors.go
+++ /dev/null
@@ -1,71 +0,0 @@
-////////////////////////////////////////////////////////////////////////////////
-// Copyright © 2022 xx foundation                                             //
-//                                                                            //
-// Use of this source code is governed by a license that can be found in the  //
-// LICENSE file.                                                              //
-////////////////////////////////////////////////////////////////////////////////
-
-//go:build js && wasm
-
-package utils
-
-import (
-	"fmt"
-	"syscall/js"
-)
-
-// JsError converts the error to a Javascript Error.
-func JsError(err error) js.Value {
-	return Error.New(err.Error())
-}
-
-// JsTrace converts the error to a Javascript Error that includes the error's
-// stack trace.
-func JsTrace(err error) js.Value {
-	return Error.New(fmt.Sprintf("%+v", err))
-}
-
-// Throw function stub to throws Javascript exceptions. The exception must be
-// one of the defined Exception below. Any other error types will result in an
-// error.
-func Throw(exception Exception, err error) {
-	throw(exception, fmt.Sprintf("%+v", err))
-}
-
-func throw(exception Exception, message string)
-
-// Exception are the possible Javascript error types that can be thrown.
-type Exception string
-
-const (
-	// EvalError occurs when error has occurred in the eval() function.
-	//
-	// Deprecated: This exception is not thrown by JavaScript anymore, however
-	// the EvalError object remains for compatibility.
-	EvalError Exception = "EvalError"
-
-	// RangeError occurs when a numeric variable or parameter is outside its
-	// valid range.
-	RangeError Exception = "RangeError"
-
-	// ReferenceError occurs when a variable that does not exist (or hasn't yet
-	// been initialized) in the current scope is referenced.
-	ReferenceError Exception = "ReferenceError"
-
-	// SyntaxError occurs when trying to interpret syntactically invalid code.
-	SyntaxError Exception = "SyntaxError"
-
-	// TypeError occurs when an operation could not be performed, typically (but
-	// not exclusively) when a value is not of the expected type.
-	//
-	// A TypeError may be thrown when:
-	//  - an operand or argument passed to a function is incompatible with the
-	//    type expected by that operator or function; or
-	//  - when attempting to modify a value that cannot be changed; or
-	//  - when attempting to use a value in an inappropriate way.
-	TypeError Exception = "TypeError"
-
-	// URIError occurs when a global URI handling function was used in a wrong
-	// way.
-	URIError Exception = "URIError"
-)
diff --git a/utils/errors_test.go b/utils/errors_test.go
deleted file mode 100644
index f9965e34878f0cd507e2839482e1f38201a99055..0000000000000000000000000000000000000000
--- a/utils/errors_test.go
+++ /dev/null
@@ -1,42 +0,0 @@
-////////////////////////////////////////////////////////////////////////////////
-// Copyright © 2022 xx foundation                                             //
-//                                                                            //
-// Use of this source code is governed by a license that can be found in the  //
-// LICENSE file.                                                              //
-////////////////////////////////////////////////////////////////////////////////
-
-//go:build js && wasm
-
-package utils
-
-import (
-	"fmt"
-	"github.com/pkg/errors"
-	"testing"
-)
-
-// Tests that TestJsError returns a Javascript Error object with the expected
-// message.
-func TestJsError(t *testing.T) {
-	err := errors.New("test error")
-	expectedErr := err.Error()
-	jsError := JsError(err).Get("message").String()
-
-	if jsError != expectedErr {
-		t.Errorf("Failed to get expected error message."+
-			"\nexpected: %s\nreceived: %s", expectedErr, jsError)
-	}
-}
-
-// Tests that TestJsTrace returns a Javascript Error object with the expected
-// message and stack trace.
-func TestJsTrace(t *testing.T) {
-	err := errors.New("test error")
-	expectedErr := fmt.Sprintf("%+v", err)
-	jsError := JsTrace(err).Get("message").String()
-
-	if jsError != expectedErr {
-		t.Errorf("Failed to get expected error message."+
-			"\nexpected: %s\nreceived: %s", expectedErr, jsError)
-	}
-}
diff --git a/utils/utils.go b/utils/utils.go
deleted file mode 100644
index cb1e46f3859695e7c77fbf0c9403eb24b7b10ff4..0000000000000000000000000000000000000000
--- a/utils/utils.go
+++ /dev/null
@@ -1,106 +0,0 @@
-////////////////////////////////////////////////////////////////////////////////
-// Copyright © 2022 xx foundation                                             //
-//                                                                            //
-// Use of this source code is governed by a license that can be found in the  //
-// LICENSE file.                                                              //
-////////////////////////////////////////////////////////////////////////////////
-
-//go:build js && wasm
-
-package utils
-
-import (
-	"github.com/pkg/errors"
-	jww "github.com/spf13/jwalterweatherman"
-	"syscall/js"
-)
-
-var (
-	// Error is the Javascript Error type. It used to create new Javascript
-	// errors.
-	Error = js.Global().Get("Error")
-
-	// JSON is the Javascript JSON type. It is used to perform JSON operations
-	// on the Javascript layer.
-	JSON = js.Global().Get("JSON")
-
-	// Object is the Javascript Object type. It is used to perform Object
-	// operations on the Javascript layer.
-	Object = js.Global().Get("Object")
-
-	// Promise is the Javascript Promise type. It is used to generate new
-	// promises.
-	Promise = js.Global().Get("Promise")
-
-	// Uint8Array is the Javascript Uint8Array type. It is used to create new
-	// Uint8Array.
-	Uint8Array = js.Global().Get("Uint8Array")
-)
-
-// WrapCB wraps a Javascript function in an object so that it can be called
-// later with only the arguments and without specifying the function name.
-//
-// Panics if m is not a function.
-func WrapCB(parent js.Value, m string) func(args ...any) js.Value {
-	if parent.Get(m).Type() != js.TypeFunction {
-		// Create the error separate from the print so stack trace is printed
-		err := errors.Errorf("Function %q is not of type %s", m, js.TypeFunction)
-		jww.FATAL.Panicf("%+v", err)
-	}
-
-	return func(args ...any) js.Value { return parent.Call(m, args...) }
-}
-
-// PromiseFn converts the Javascript Promise construct into Go.
-//
-// Call resolve with the return of the function on success. Call reject with an
-// error on failure.
-type PromiseFn func(resolve, reject func(args ...any) js.Value)
-
-// CreatePromise creates a Javascript promise to return the value of a blocking
-// Go function to Javascript.
-func CreatePromise(f PromiseFn) any {
-	// Create handler for promise (this will be a Javascript function)
-	handler := js.FuncOf(func(this js.Value, args []js.Value) any {
-		// Spawn a new go routine to perform the blocking function
-		go func(resolve, reject js.Value) {
-			f(resolve.Invoke, reject.Invoke)
-		}(args[0], args[1])
-
-		return nil
-	})
-
-	// Create and return the Promise object
-	return Promise.New(handler)
-}
-
-// Await waits on a Javascript value. It blocks until the awaitable successfully
-// resolves to the result or rejects to err.
-//
-// If there is a result, err will be nil and vice versa.
-func Await(awaitable js.Value) (result []js.Value, err []js.Value) {
-	then := make(chan []js.Value)
-	defer close(then)
-	thenFunc := js.FuncOf(func(this js.Value, args []js.Value) any {
-		then <- args
-		return nil
-	})
-	defer thenFunc.Release()
-
-	catch := make(chan []js.Value)
-	defer close(catch)
-	catchFunc := js.FuncOf(func(this js.Value, args []js.Value) any {
-		catch <- args
-		return nil
-	})
-	defer catchFunc.Release()
-
-	awaitable.Call("then", thenFunc).Call("catch", catchFunc)
-
-	select {
-	case result = <-then:
-		return result, nil
-	case err = <-catch:
-		return nil, err
-	}
-}
diff --git a/utils/utils_js.s b/utils/utils_js.s
deleted file mode 100644
index 45c1668a272247a134e6c85508bf5c09f0d7b3f0..0000000000000000000000000000000000000000
--- a/utils/utils_js.s
+++ /dev/null
@@ -1,6 +0,0 @@
-#include "textflag.h"
-
-// Throw enables throwing of Javascript exceptions.
-TEXT ·throw(SB), NOSPLIT, $0
-  CallImport
-  RET
diff --git a/wasm/authenticatedConnection.go b/wasm/authenticatedConnection.go
index 3308a9be32e8321c7d80c7fe5558c794407050b0..66162cfb68ec42906a3d596278cdf2b4801149b4 100644
--- a/wasm/authenticatedConnection.go
+++ b/wasm/authenticatedConnection.go
@@ -11,7 +11,8 @@ package wasm
 
 import (
 	"gitlab.com/elixxir/client/v4/bindings"
-	"gitlab.com/elixxir/xxdk-wasm/utils"
+	"gitlab.com/elixxir/wasm-utils/exception"
+	"gitlab.com/elixxir/wasm-utils/utils"
 	"syscall/js"
 )
 
@@ -73,7 +74,7 @@ func (ac *AuthenticatedConnection) SendE2E(_ js.Value, args []js.Value) any {
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
 		sendReport, err := ac.api.SendE2E(mt, payload)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(utils.CopyBytesToJS(sendReport))
 		}
@@ -86,7 +87,7 @@ func (ac *AuthenticatedConnection) SendE2E(_ js.Value, args []js.Value) any {
 // resources.
 //
 // Returns:
-//   - Throws a TypeError if closing fails.
+//   - Throws an error if closing fails.
 func (ac *AuthenticatedConnection) Close(js.Value, []js.Value) any {
 	return ac.api.Close()
 }
@@ -108,13 +109,13 @@ func (ac *AuthenticatedConnection) GetPartner(js.Value, []js.Value) any {
 //     [bindings.Listener] interface.
 //
 // Returns:
-//   - Throws a TypeError is registering the listener fails.
+//   - Throws an error is registering the listener fails.
 func (ac *AuthenticatedConnection) RegisterListener(
 	_ js.Value, args []js.Value) any {
 	err := ac.api.RegisterListener(args[0].Int(),
 		&listener{utils.WrapCB(args[1], "Hear"), utils.WrapCB(args[1], "Name")})
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -143,7 +144,7 @@ func (c *Cmix) ConnectWithAuthentication(_ js.Value, args []js.Value) any {
 		ac, err := c.api.ConnectWithAuthentication(
 			e2eID, recipientContact, e2eParamsJSON)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(newAuthenticatedConnectionJS(ac))
 		}
diff --git a/wasm/backup.go b/wasm/backup.go
index 9f5e7f6e7b302675ad8a385745dd645473961405..1a4a05de8d86410493544015501f852f72e7462d 100644
--- a/wasm/backup.go
+++ b/wasm/backup.go
@@ -11,7 +11,8 @@ package wasm
 
 import (
 	"gitlab.com/elixxir/client/v4/bindings"
-	"gitlab.com/elixxir/xxdk-wasm/utils"
+	"gitlab.com/elixxir/wasm-utils/exception"
+	"gitlab.com/elixxir/wasm-utils/utils"
 	"syscall/js"
 )
 
@@ -69,7 +70,7 @@ func (ubf *updateBackupFunc) UpdateBackup(encryptedBackup []byte) {
 //
 // Returns:
 //   - JSON of [bindings.BackupReport] (Uint8Array).
-//   - Throws a TypeError if creating [Cmix] from backup fails.
+//   - Throws an error if creating [Cmix] from backup fails.
 func NewCmixFromBackup(_ js.Value, args []js.Value) any {
 	ndfJSON := args[0].String()
 	storageDir := args[1].String()
@@ -80,7 +81,7 @@ func NewCmixFromBackup(_ js.Value, args []js.Value) any {
 	report, err := bindings.NewCmixFromBackup(ndfJSON, storageDir,
 		backupPassphrase, sessionPassword, backupFileContents)
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -104,13 +105,13 @@ func NewCmixFromBackup(_ js.Value, args []js.Value) any {
 //
 // Returns:
 //   - Javascript representation of the [Backup] object.
-//   - Throws a TypeError if initializing the [Backup] fails.
+//   - Throws an error if initializing the [Backup] fails.
 func InitializeBackup(_ js.Value, args []js.Value) any {
 	cb := &updateBackupFunc{utils.WrapCB(args[3], "UpdateBackup")}
 	api, err := bindings.InitializeBackup(
 		args[0].Int(), args[1].Int(), args[2].String(), cb)
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -133,12 +134,12 @@ func InitializeBackup(_ js.Value, args []js.Value) any {
 //
 // Returns:
 //   - Javascript representation of the [Backup] object.
-//   - Throws a TypeError if initializing the [Backup] fails.
+//   - Throws an error if initializing the [Backup] fails.
 func ResumeBackup(_ js.Value, args []js.Value) any {
 	cb := &updateBackupFunc{utils.WrapCB(args[2], "UpdateBackup")}
 	api, err := bindings.ResumeBackup(args[0].Int(), args[1].Int(), cb)
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -149,11 +150,11 @@ func ResumeBackup(_ js.Value, args []js.Value) any {
 // storage. To enable backups again, call [InitializeBackup].
 //
 // Returns:
-//   - Throws a TypeError if stopping the backup fails.
+//   - Throws an error if stopping the backup fails.
 func (b *Backup) StopBackup(js.Value, []js.Value) any {
 	err := b.api.StopBackup()
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
diff --git a/wasm/channels.go b/wasm/channels.go
index 4236d37e57f4dd0f6a84357dee8fd2a27594c2d7..653ff6b21506ea7a655d583dd6fd8e9be487502a 100644
--- a/wasm/channels.go
+++ b/wasm/channels.go
@@ -10,19 +10,17 @@
 package wasm
 
 import (
-	"crypto/ed25519"
 	"encoding/base64"
 	"encoding/json"
 	"errors"
-	"gitlab.com/elixxir/client/v4/channels"
-	"gitlab.com/elixxir/crypto/message"
-	channelsDb "gitlab.com/elixxir/xxdk-wasm/indexedDb/worker/channels"
-	"gitlab.com/xx_network/primitives/id"
 	"sync"
 	"syscall/js"
 
 	"gitlab.com/elixxir/client/v4/bindings"
-	"gitlab.com/elixxir/xxdk-wasm/utils"
+	"gitlab.com/elixxir/client/v4/channels"
+	"gitlab.com/elixxir/wasm-utils/exception"
+	"gitlab.com/elixxir/wasm-utils/utils"
+	channelsDb "gitlab.com/elixxir/xxdk-wasm/indexedDb/worker/channels"
 )
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -60,7 +58,6 @@ func newChannelsManagerJS(api *bindings.ChannelsManager) map[string]any {
 		"SendMessage":           js.FuncOf(cm.SendMessage),
 		"SendReply":             js.FuncOf(cm.SendReply),
 		"SendReaction":          js.FuncOf(cm.SendReaction),
-		"SendInvite":            js.FuncOf(cm.SendInvite),
 		"DeleteMessage":         js.FuncOf(cm.DeleteMessage),
 		"PinMessage":            js.FuncOf(cm.PinMessage),
 		"MuteUser":              js.FuncOf(cm.MuteUser),
@@ -80,6 +77,11 @@ func newChannelsManagerJS(api *bindings.ChannelsManager) map[string]any {
 
 		// Channel Receiving Logic and Callback Registration
 		"RegisterReceiveHandler": js.FuncOf(cm.RegisterReceiveHandler),
+
+		// Notifications
+		"SetMobileNotificationsLevel": js.FuncOf(
+			cm.SetMobileNotificationsLevel),
+		"GetNotificationLevel": js.FuncOf(cm.GetNotificationLevel),
 	}
 
 	return channelsManagerMap
@@ -106,11 +108,11 @@ func (cm *ChannelsManager) GetID(js.Value, []js.Value) any {
 //
 // Returns:
 //   - Marshalled bytes of [channel.PrivateIdentity] (Uint8Array).
-//   - Throws a TypeError if generating the identity fails.
+//   - Throws an error if generating the identity fails.
 func GenerateChannelIdentity(_ js.Value, args []js.Value) any {
 	pi, err := bindings.GenerateChannelIdentity(args[0].Int())
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -129,7 +131,7 @@ var identityMap sync.Map
 //
 // Returns:
 //   - JSON of [channel.Identity] (Uint8Array).
-//   - Throws a TypeError if constructing the identity fails.
+//   - Throws an error if constructing the identity fails.
 func ConstructIdentity(_ js.Value, args []js.Value) any {
 	// Note: This function is similar to constructIdentity below except that it
 	//  uses a sync.Map backend to increase efficiency for identities that were
@@ -145,7 +147,7 @@ func ConstructIdentity(_ js.Value, args []js.Value) any {
 	identity, err := bindings.ConstructIdentity(
 		utils.CopyBytesToGo(args[0]), args[1].Int())
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -163,12 +165,12 @@ func ConstructIdentity(_ js.Value, args []js.Value) any {
 //
 // Returns:
 //   - JSON of [channel.Identity] (Uint8Array).
-//   - Throws a TypeError if constructing the identity fails.
+//   - Throws an error if constructing the identity fails.
 func constructIdentity(_ js.Value, args []js.Value) any {
 	identity, err := bindings.ConstructIdentity(
 		utils.CopyBytesToGo(args[0]), args[1].Int())
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -185,14 +187,14 @@ func constructIdentity(_ js.Value, args []js.Value) any {
 //
 // Returns:
 //   - JSON of [channel.PrivateIdentity] (Uint8Array).
-//   - Throws a TypeError if importing the identity fails.
+//   - Throws an error if importing the identity fails.
 func ImportPrivateIdentity(_ js.Value, args []js.Value) any {
 	password := args[0].String()
 	data := utils.CopyBytesToGo(args[1])
 
 	pi, err := bindings.ImportPrivateIdentity(password, data)
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -207,13 +209,13 @@ func ImportPrivateIdentity(_ js.Value, args []js.Value) any {
 //
 // Returns:
 //   - JSON of the constructed [channel.Identity] (Uint8Array).
-//   - Throws a TypeError if unmarshalling the bytes or marshalling the identity
+//   - Throws an error if unmarshalling the bytes or marshalling the identity
 //     fails.
 func GetPublicChannelIdentity(_ js.Value, args []js.Value) any {
 	marshaledPublic := utils.CopyBytesToGo(args[0])
 	pi, err := bindings.GetPublicChannelIdentity(marshaledPublic)
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -230,14 +232,14 @@ func GetPublicChannelIdentity(_ js.Value, args []js.Value) any {
 //
 // Returns:
 //   - JSON of the public identity ([channel.Identity]) (Uint8Array).
-//   - Throws a TypeError if unmarshalling the bytes or marshalling the identity
+//   - Throws an error if unmarshalling the bytes or marshalling the identity
 //     fails.
 func GetPublicChannelIdentityFromPrivate(_ js.Value, args []js.Value) any {
 	marshaledPrivate := utils.CopyBytesToGo(args[0])
 	identity, err :=
 		bindings.GetPublicChannelIdentityFromPrivate(marshaledPrivate)
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -257,27 +259,35 @@ func GetPublicChannelIdentityFromPrivate(_ js.Value, args []js.Value) any {
 //     using [Cmix.GetID].
 //   - args[1] - Bytes of a private identity ([channel.PrivateIdentity]) that is
 //     generated by [GenerateChannelIdentity] (Uint8Array).
-//   - args[2] - JSON of an array of integers of [channels.ExtensionBuilder]
+//   - args[2] - A function that initialises and returns a Javascript object
+//     that matches the [bindings.EventModel] interface. The function must match
+//     the Build function in [bindings.EventModelBuilder].
+//   - args[3] - JSON of an array of integers of [channels.ExtensionBuilder]
 //     IDs. The ID can be retrieved from an object with an extension builder
 //     (e.g., [ChannelsFileTransfer.GetExtensionBuilderID]). Leave empty if not
 //     using extension builders. Example: `[2,11,5]` (Uint8Array).
-//   - args[3] - A function that initialises and returns a Javascript object
-//     that matches the [bindings.EventModel] interface. The function must match
-//     the Build function in [bindings.EventModelBuilder].
+//   - args[4] - ID of [Notifications] object in tracker. This can be retrieved
+//     using [Notifications.GetID] (int).
+//   - args[5] - A Javascript object that implements the function on
+//     [bindings.ChannelUICallbacks]. It is a callback that informs the UI about
+//     various events. The entire interface can be nil, but if defined, each
+//     method must be implemented.
 //
 // Returns:
 //   - Javascript representation of the [ChannelsManager] object.
-//   - Throws a TypeError if creating the manager fails.
+//   - Throws an error if creating the manager fails.
 func NewChannelsManager(_ js.Value, args []js.Value) any {
 	cmixId := args[0].Int()
 	privateIdentity := utils.CopyBytesToGo(args[1])
-	extensionBuilderIDsJSON := utils.CopyBytesToGo(args[2])
-	em := newEventModelBuilder(args[3])
+	em := newEventModelBuilder(args[2])
+	extensionBuilderIDsJSON := utils.CopyBytesToGo(args[3])
+	notificationsID := args[4].Int()
+	cUI := newChannelUI(args[5])
 
 	cm, err := bindings.NewChannelsManager(
-		cmixId, privateIdentity, extensionBuilderIDsJSON, em)
+		cmixId, privateIdentity, em, extensionBuilderIDsJSON, notificationsID, cUI)
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -297,18 +307,34 @@ func NewChannelsManager(_ js.Value, args []js.Value) any {
 //     using [Cmix.GetID].
 //   - args[1] - The storage tag associated with the previously created channel
 //     manager and retrieved with [ChannelsManager.GetStorageTag] (string).
-//   - args[2] - A function that initialises and returns a Javascript object
+//   - args[2] - A function that initializes and returns a Javascript object
 //     that matches the [bindings.EventModel] interface. The function must match
 //     the Build function in [bindings.EventModelBuilder].
+//   - args[3] - JSON of an array of integers of [channels.ExtensionBuilder]
+//     IDs. The ID can be retrieved from an object with an extension builder
+//     (e.g., [ChannelsFileTransfer.GetExtensionBuilderID]). Leave empty if not
+//     using extension builders. Example: `[2,11,5]`.
+//   - args[4] - ID of [Notifications] object in tracker. This can be retrieved
+//     using [Notifications.GetID] (int).
+//   - args[5] - A Javascript object that implements the function on
+//     [bindings.ChannelUICallbacks]. It is a callback that informs the UI about
+//     various events. The entire interface can be nil, but if defined, each
+//     method must be implemented.
 //
 // Returns:
 //   - Javascript representation of the [ChannelsManager] object.
-//   - Throws a TypeError if loading the manager fails.
+//   - Throws an error if loading the manager fails.
 func LoadChannelsManager(_ js.Value, args []js.Value) any {
+	cmixID := args[0].Int()
+	storageTag := args[1].String()
 	em := newEventModelBuilder(args[2])
-	cm, err := bindings.LoadChannelsManager(args[0].Int(), args[1].String(), em)
+	extensionBuilderIDsJSON := utils.CopyBytesToGo(args[3])
+	notificationsID := args[4].Int()
+	cUI := newChannelUI(args[5])
+	cm, err := bindings.LoadChannelsManager(
+		cmixID, storageTag, em, extensionBuilderIDsJSON, notificationsID, cUI)
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -336,48 +362,36 @@ func LoadChannelsManager(_ js.Value, args []js.Value) any {
 //     IDs. The ID can be retrieved from an object with an extension builder
 //     (e.g., [ChannelsFileTransfer.GetExtensionBuilderID]). Leave empty if not
 //     using extension builders. Example: `[2,11,5]` (Uint8Array).
-//   - args[4] - The received message callback, which is called everytime a
-//     message is added or changed in the database. It is a function that takes
-//     in the same parameters as [channels.MessageReceivedCallback]. On the
-//     Javascript side, the UUID is returned as an int and the channelID as a
-//     Uint8Array. The row in the database that was updated can be found using
-//     the UUID. The channel ID is provided so that the recipient can filter if
-//     they want to the processes the update now or not. An "update" bool is
-//     present which tells you if the row is new or if it is an edited old row.
-//   - args[5] - The deleted message callback, which is called everytime a
-//     message is deleted from the database. It is a function that takes in the
-//     same parameters as [indexedDb.DeletedMessageCallback]. On the Javascript
-//     side, the message ID is returned as a Uint8Array.
-//   - args[6] - The muted user callback, which is called everytime a user is
-//     muted or unmuted. It is a function that takes in the same parameters as
-//     [indexedDb.MutedUserCallback]. On the Javascript side, the channel ID and
-//     user public key are returned as Uint8Array.
-//   - args[7] - ID of [ChannelDbCipher] object in tracker (int). Create this
+//   - args[4] - ID of [Notifications] object in tracker. This can be retrieved
+//     using [Notifications.GetID] (int).
+//   - args[5] - A Javascript object that implements the function on
+//     [bindings.ChannelUICallbacks]. It is a callback that informs the UI about
+//     various events. The entire interface can be nil, but if defined, each
+//     method must be implemented.
+//   - args[6] - ID of [ChannelDbCipher] object in tracker (int). Create this
 //     object with [NewChannelsDatabaseCipher] and get its id with
 //     [ChannelDbCipher.GetID].
 //
 // Returns a promise:
 //   - Resolves to a Javascript representation of the [ChannelsManager] object.
 //   - Rejected with an error if loading indexedDb or the manager fails.
-//   - Throws a TypeError if the cipher ID does not correspond to a cipher.
+//   - Throws an error if the cipher ID does not correspond to a cipher.
 func NewChannelsManagerWithIndexedDb(_ js.Value, args []js.Value) any {
 	cmixID := args[0].Int()
 	wasmJsPath := args[1].String()
 	privateIdentity := utils.CopyBytesToGo(args[2])
 	extensionBuilderIDsJSON := utils.CopyBytesToGo(args[3])
-	messageReceivedCB := args[4]
-	deletedMessageCB := args[5]
-	mutedUserCB := args[6]
-	cipherID := args[7].Int()
+	notificationsID := args[4].Int()
+	cUI := newChannelUI(args[5])
+	cipherID := args[6].Int()
 
 	cipher, err := bindings.GetChannelDbCipherTrackerFromID(cipherID)
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 	}
 
 	return newChannelsManagerWithIndexedDb(cmixID, wasmJsPath, privateIdentity,
-		extensionBuilderIDsJSON, messageReceivedCB, deletedMessageCB,
-		mutedUserCB, cipher)
+		extensionBuilderIDsJSON, notificationsID, cUI, cipher)
 }
 
 // NewChannelsManagerWithIndexedDbUnsafe creates a new [ChannelsManager] from a
@@ -402,22 +416,12 @@ func NewChannelsManagerWithIndexedDb(_ js.Value, args []js.Value) any {
 //     IDs. The ID can be retrieved from an object with an extension builder
 //     (e.g., [ChannelsFileTransfer.GetExtensionBuilderID]). Leave empty if not
 //     using extension builders. Example: `[2,11,5]` (Uint8Array).
-//   - args[4] - The received message callback, which is called everytime a
-//     message is added or changed in the database. It is a function that takes
-//     in the same parameters as [indexedDb.MessageReceivedCallback]. On the
-//     Javascript side, the UUID is returned as an int and the channelID as a
-//     Uint8Array. The row in the database that was updated can be found using
-//     the UUID. The channel ID is provided so that the recipient can filter if
-//     they want to the processes the update now or not. An "update" bool is
-//     present which tells you if the row is new or if it is an edited old row.
-//   - args[5] - The deleted message callback, which is called everytime a
-//     message is deleted from the database. It is a function that takes in the
-//     same parameters as [indexedDb.DeletedMessageCallback]. On the Javascript
-//     side, the message ID is returned as a Uint8Array.
-//   - args[6] - The muted user callback, which is called everytime a user is
-//     muted or unmuted. It is a function that takes in the same parameters as
-//     [indexedDb.MutedUserCallback]. On the Javascript side, the channel ID and
-//     user public key are returned as Uint8Array.
+//   - args[4] - ID of [Notifications] object in tracker. This can be retrieved
+//     using [Notifications.GetID] (int).
+//   - args[5] - A Javascript object that implements the function on
+//     [bindings.ChannelUICallbacks]. It is a callback that informs the UI about
+//     various events. The entire interface can be nil, but if defined, each
+//     method must be implemented.
 //
 // Returns a promise:
 //   - Resolves to a Javascript representation of the [ChannelsManager] object.
@@ -429,40 +433,27 @@ func NewChannelsManagerWithIndexedDbUnsafe(_ js.Value, args []js.Value) any {
 	wasmJsPath := args[1].String()
 	privateIdentity := utils.CopyBytesToGo(args[2])
 	extensionBuilderIDsJSON := utils.CopyBytesToGo(args[3])
-	messageReceivedCB := args[4]
-	deletedMessageCB := args[5]
-	mutedUserCB := args[6]
+	notificationsID := args[4].Int()
+	cUI := newChannelUI(args[5])
 
 	return newChannelsManagerWithIndexedDb(cmixID, wasmJsPath, privateIdentity,
-		extensionBuilderIDsJSON, messageReceivedCB, deletedMessageCB,
-		mutedUserCB, nil)
+		extensionBuilderIDsJSON, notificationsID, cUI, nil)
 }
 
 func newChannelsManagerWithIndexedDb(cmixID int, wasmJsPath string,
-	privateIdentity, extensionBuilderIDsJSON []byte, messageReceivedCB,
-	deletedMessageCB, mutedUserCB js.Value, cipher *bindings.ChannelDbCipher) any {
-
-	messageReceived := func(uuid uint64, channelID *id.ID, update bool) {
-		messageReceivedCB.Invoke(uuid, utils.CopyBytesToJS(channelID.Marshal()), update)
-	}
-
-	deletedMessage := func(messageID message.ID) {
-		deletedMessageCB.Invoke(utils.CopyBytesToJS(messageID.Marshal()))
-	}
-
-	mutedUser := func(channelID *id.ID, pubKey ed25519.PublicKey, unmute bool) {
-		mutedUserCB.Invoke(utils.CopyBytesToJS(channelID.Marshal()),
-			utils.CopyBytesToJS(pubKey), unmute)
-	}
+	privateIdentity, extensionBuilderIDsJSON []byte, notificationsID int,
+	channelsCbs bindings.ChannelUICallbacks,
+	cipher *bindings.ChannelDbCipher) any {
 
 	model := channelsDb.NewWASMEventModelBuilder(
-		wasmJsPath, cipher, messageReceived, deletedMessage, mutedUser)
+		wasmJsPath, cipher, channelsCbs)
 
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
-		cm, err := bindings.NewChannelsManagerGoEventModel(
-			cmixID, privateIdentity, extensionBuilderIDsJSON, model)
+		cm, err := bindings.NewChannelsManagerGoEventModel(cmixID,
+			privateIdentity, extensionBuilderIDsJSON, model, notificationsID,
+			channelsCbs)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(newChannelsManagerJS(cm))
 		}
@@ -485,22 +476,16 @@ func newChannelsManagerWithIndexedDb(cmixID int, wasmJsPath string,
 //   - args[1] - Path to Javascript file that starts the worker (string).
 //   - args[2] - The storage tag associated with the previously created channel
 //     manager and retrieved with [ChannelsManager.GetStorageTag] (string).
-//   - args[3] - The received message callback, which is called everytime a
-//     message is added or changed in the database. It is a function that takes
-//     in the same parameters as [indexedDb.MessageReceivedCallback]. On the
-//     Javascript side, the UUID is returned as an int and the channelID as a
-//     Uint8Array. The row in the database that was updated can be found using
-//     the UUID. The channel ID is provided so that the recipient can filter if
-//     they want to the processes the update now or not. An "update" bool is
-//     present which tells you if the row is new or if it is an edited old row.
-//   - args[4] - The deleted message callback, which is called everytime a
-//     message is deleted from the database. It is a function that takes in the
-//     same parameters as [indexedDb.DeletedMessageCallback]. On the Javascript
-//     side, the message ID is returned as a Uint8Array.
-//   - args[5] - The muted user callback, which is called everytime a user is
-//     muted or unmuted. It is a function that takes in the same parameters as
-//     [indexedDb.MutedUserCallback]. On the Javascript side, the channel ID and
-//     user public key are returned as Uint8Array.
+//   - args[3] - JSON of an array of integers of [channels.ExtensionBuilder]
+//     IDs. The ID can be retrieved from an object with an extension builder
+//     (e.g., [ChannelsFileTransfer.GetExtensionBuilderID]). Leave empty if not
+//     using extension builders. Example: `[2,11,5]` (Uint8Array).
+//   - args[4] - ID of [Notifications] object in tracker. This can be retrieved
+//     using [Notifications.GetID] (int).
+//   - args[5] - A Javascript object that implements the function on
+//     [bindings.ChannelUICallbacks]. It is a callback that informs the UI about
+//     various events. The entire interface can be nil, but if defined, each
+//     method must be implemented.
 //   - args[6] - ID of [ChannelDbCipher] object in tracker (int). Create this
 //     object with [NewChannelsDatabaseCipher] and get its id with
 //     [ChannelDbCipher.GetID].
@@ -508,23 +493,23 @@ func newChannelsManagerWithIndexedDb(cmixID int, wasmJsPath string,
 // Returns a promise:
 //   - Resolves to a Javascript representation of the [ChannelsManager] object.
 //   - Rejected with an error if loading indexedDb or the manager fails.
-//   - Throws a TypeError if the cipher ID does not correspond to a cipher.
+//   - Throws an error if the cipher ID does not correspond to a cipher.
 func LoadChannelsManagerWithIndexedDb(_ js.Value, args []js.Value) any {
 	cmixID := args[0].Int()
 	wasmJsPath := args[1].String()
 	storageTag := args[2].String()
-	messageReceivedCB := args[3]
-	deletedMessageCB := args[4]
-	mutedUserCB := args[5]
+	extensionBuilderIDsJSON := utils.CopyBytesToGo(args[3])
+	notificationsID := args[4].Int()
+	channelsCbs := newChannelUI(args[5])
 	cipherID := args[6].Int()
 
 	cipher, err := bindings.GetChannelDbCipherTrackerFromID(cipherID)
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 	}
 
 	return loadChannelsManagerWithIndexedDb(cmixID, wasmJsPath, storageTag,
-		messageReceivedCB, deletedMessageCB, mutedUserCB, cipher)
+		extensionBuilderIDsJSON, notificationsID, channelsCbs, cipher)
 }
 
 // LoadChannelsManagerWithIndexedDbUnsafe loads an existing [ChannelsManager]
@@ -543,22 +528,16 @@ func LoadChannelsManagerWithIndexedDb(_ js.Value, args []js.Value) any {
 //   - args[1] - Path to Javascript file that starts the worker (string).
 //   - args[2] - The storage tag associated with the previously created channel
 //     manager and retrieved with [ChannelsManager.GetStorageTag] (string).
-//   - args[3] - The received message callback, which is called everytime a
-//     message is added or changed in the database. It is a function that takes
-//     in the same parameters as [indexedDb.MessageReceivedCallback]. On the
-//     Javascript side, the UUID is returned as an int and the channelID as a
-//     Uint8Array. The row in the database that was updated can be found using
-//     the UUID. The channel ID is provided so that the recipient can filter if
-//     they want to the processes the update now or not. An "update" bool is
-//     present which tells you if the row is new or if it is an edited old row.
-//   - args[4] - The deleted message callback, which is called everytime a
-//     message is deleted from the database. It is a function that takes in the
-//     same parameters as [indexedDb.DeletedMessageCallback]. On the Javascript
-//     side, the message ID is returned as a Uint8Array.
-//   - args[5] - The muted user callback, which is called everytime a user is
-//     muted or unmuted. It is a function that takes in the same parameters as
-//     [indexedDb.MutedUserCallback]. On the Javascript side, the channel ID and
-//     user public key are returned as Uint8Array.
+//   - args[3] - JSON of an array of integers of [channels.ExtensionBuilder]
+//     IDs. The ID can be retrieved from an object with an extension builder
+//     (e.g., [ChannelsFileTransfer.GetExtensionBuilderID]). Leave empty if not
+//     using extension builders. Example: `[2,11,5]` (Uint8Array).
+//   - args[4] - ID of [Notifications] object in tracker. This can be retrieved
+//     using [Notifications.GetID] (int).
+//   - args[5] - A Javascript object that implements the function on
+//     [bindings.ChannelUICallbacks]. It is a callback that informs the UI about
+//     various events. The entire interface can be nil, but if defined, each
+//     method must be implemented.
 //
 // Returns a promise:
 //   - Resolves to a Javascript representation of the [ChannelsManager] object.
@@ -567,39 +546,27 @@ func LoadChannelsManagerWithIndexedDbUnsafe(_ js.Value, args []js.Value) any {
 	cmixID := args[0].Int()
 	wasmJsPath := args[1].String()
 	storageTag := args[2].String()
-	messageReceivedCB := args[3]
-	deletedMessageCB := args[3]
-	mutedUserCB := args[4]
+	extensionBuilderIDsJSON := utils.CopyBytesToGo(args[3])
+	notificationsID := args[4].Int()
+	cUI := newChannelUI(args[5])
 
 	return loadChannelsManagerWithIndexedDb(cmixID, wasmJsPath, storageTag,
-		messageReceivedCB, deletedMessageCB, mutedUserCB, nil)
+		extensionBuilderIDsJSON, notificationsID, cUI, nil)
 }
 
 func loadChannelsManagerWithIndexedDb(cmixID int, wasmJsPath, storageTag string,
-	messageReceivedCB, deletedMessageCB, mutedUserCB js.Value,
+	extensionBuilderIDsJSON []byte, notificationsID int, channelsCbs bindings.ChannelUICallbacks,
 	cipher *bindings.ChannelDbCipher) any {
 
-	messageReceived := func(uuid uint64, channelID *id.ID, update bool) {
-		messageReceivedCB.Invoke(uuid, utils.CopyBytesToJS(channelID.Marshal()), update)
-	}
-
-	deletedMessage := func(messageID message.ID) {
-		deletedMessageCB.Invoke(utils.CopyBytesToJS(messageID.Marshal()))
-	}
-
-	mutedUser := func(channelID *id.ID, pubKey ed25519.PublicKey, unmute bool) {
-		mutedUserCB.Invoke(utils.CopyBytesToJS(channelID.Marshal()),
-			utils.CopyBytesToJS(pubKey), unmute)
-	}
-
 	model := channelsDb.NewWASMEventModelBuilder(
-		wasmJsPath, cipher, messageReceived, deletedMessage, mutedUser)
+		wasmJsPath, cipher, channelsCbs)
 
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
 		cm, err := bindings.LoadChannelsManagerGoEventModel(
-			cmixID, storageTag, model, nil)
+			cmixID, storageTag, model, extensionBuilderIDsJSON, notificationsID,
+			channelsCbs)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(newChannelsManagerJS(cm))
 		}
@@ -625,7 +592,7 @@ func loadChannelsManagerWithIndexedDb(cmixID int, wasmJsPath, storageTag string,
 func DecodePublicURL(_ js.Value, args []js.Value) any {
 	c, err := bindings.DecodePublicURL(args[0].String())
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -647,7 +614,7 @@ func DecodePublicURL(_ js.Value, args []js.Value) any {
 func DecodePrivateURL(_ js.Value, args []js.Value) any {
 	c, err := bindings.DecodePrivateURL(args[0].String(), args[1].String())
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -678,7 +645,7 @@ func DecodePrivateURL(_ js.Value, args []js.Value) any {
 func GetChannelJSON(_ js.Value, args []js.Value) any {
 	c, err := bindings.GetChannelJSON(args[0].String())
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -697,11 +664,11 @@ func GetChannelJSON(_ js.Value, args []js.Value) any {
 // Returns:
 //   - JSON of [bindings.ChannelInfo], which describes all relevant channel info
 //     (Uint8Array).
-//   - Throws a TypeError if getting the channel info fails.
+//   - Throws an error if getting the channel info fails.
 func GetChannelInfo(_ js.Value, args []js.Value) any {
 	ci, err := bindings.GetChannelInfo(args[0].String())
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -749,7 +716,7 @@ func (cm *ChannelsManager) GenerateChannel(_ js.Value, args []js.Value) any {
 		prettyPrint, err :=
 			cm.api.GenerateChannel(name, description, privacyLevel)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(prettyPrint)
 		}
@@ -779,7 +746,7 @@ func (cm *ChannelsManager) JoinChannel(_ js.Value, args []js.Value) any {
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
 		ci, err := cm.api.JoinChannel(channelPretty)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(utils.CopyBytesToJS(ci))
 		}
@@ -803,7 +770,7 @@ func (cm *ChannelsManager) LeaveChannel(_ js.Value, args []js.Value) any {
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
 		err := cm.api.LeaveChannel(marshalledChanId)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve()
 		}
@@ -822,13 +789,13 @@ func (cm *ChannelsManager) LeaveChannel(_ js.Value, args []js.Value) any {
 //   - args[0] - Marshalled bytes of the channel's [id.ID] (Uint8Array).
 //
 // Returns:
-//   - Throws a TypeError if the replay fails.
+//   - Throws an error if the replay fails.
 func (cm *ChannelsManager) ReplayChannel(_ js.Value, args []js.Value) any {
 	marshalledChanId := utils.CopyBytesToGo(args[0])
 
 	err := cm.api.ReplayChannel(marshalledChanId)
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -839,7 +806,7 @@ func (cm *ChannelsManager) ReplayChannel(_ js.Value, args []js.Value) any {
 //
 // Returns:
 //   - JSON of an array of marshalled [id.ID] (Uint8Array).
-//   - Throws a TypeError if getting the channels fails.
+//   - Throws an error if getting the channels fails.
 //
 // JSON Example:
 //
@@ -850,7 +817,7 @@ func (cm *ChannelsManager) ReplayChannel(_ js.Value, args []js.Value) any {
 func (cm *ChannelsManager) GetChannels(js.Value, []js.Value) any {
 	channelList, err := cm.api.GetChannels()
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -864,12 +831,12 @@ func (cm *ChannelsManager) GetChannels(js.Value, []js.Value) any {
 //   - args[0] - Marshalled bytes of the channel [id.ID] (Uint8Array).
 //
 // Returns:
-//   - Throws a TypeError if saving the DM token fails.
+//   - Throws an error if saving the DM token fails.
 func (cm *ChannelsManager) EnableDirectMessages(_ js.Value, args []js.Value) any {
 	marshalledChanId := utils.CopyBytesToGo(args[0])
 	err := cm.api.EnableDirectMessages(marshalledChanId)
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 	return nil
@@ -882,12 +849,12 @@ func (cm *ChannelsManager) EnableDirectMessages(_ js.Value, args []js.Value) any
 //   - args[0] - Marshalled bytes of the channel [id.ID] (Uint8Array).
 //
 // Returns:
-//   - Throws a TypeError if saving the DM token fails
+//   - Throws an error if saving the DM token fails
 func (cm *ChannelsManager) DisableDirectMessages(_ js.Value, args []js.Value) any {
 	marshalledChanId := utils.CopyBytesToGo(args[0])
 	err := cm.api.DisableDirectMessages(marshalledChanId)
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 	return nil
@@ -900,12 +867,12 @@ func (cm *ChannelsManager) DisableDirectMessages(_ js.Value, args []js.Value) an
 //
 // Returns:
 //   - enabled (bool) - status of dms for passed in channel ID, true if enabled
-//   - Throws a TypeError if unmarshalling the channel ID
+//   - Throws an error if unmarshalling the channel ID
 func (cm *ChannelsManager) AreDMsEnabled(_ js.Value, args []js.Value) any {
 	marshalledChanId := utils.CopyBytesToGo(args[0])
 	enabled, err := cm.api.AreDMsEnabled(marshalledChanId)
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return false
 	}
 	return enabled
@@ -948,7 +915,7 @@ type ShareURL struct {
 //
 // Returns:
 //   - JSON of [bindings.ShareURL] (Uint8Array).
-//   - Throws a TypeError if generating the URL fails.
+//   - Throws an error if generating the URL fails.
 func (cm *ChannelsManager) GetShareURL(_ js.Value, args []js.Value) any {
 	cmixID := args[0].Int()
 	host := args[1].String()
@@ -957,7 +924,7 @@ func (cm *ChannelsManager) GetShareURL(_ js.Value, args []js.Value) any {
 
 	su, err := cm.api.GetShareURL(cmixID, host, maxUses, marshalledChanId)
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -973,7 +940,7 @@ func (cm *ChannelsManager) GetShareURL(_ js.Value, args []js.Value) any {
 // Returns:
 //   - An int that corresponds to the [broadcast.PrivacyLevel] as outlined
 //     below (int).
-//   - Throws a TypeError if parsing the URL fails.
+//   - Throws an error if parsing the URL fails.
 //
 // Possible returns:
 //
@@ -983,7 +950,7 @@ func (cm *ChannelsManager) GetShareURL(_ js.Value, args []js.Value) any {
 func GetShareUrlType(_ js.Value, args []js.Value) any {
 	level, err := bindings.GetShareUrlType(args[0].String())
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -1027,6 +994,16 @@ func ValidForever(js.Value, []js.Value) any {
 //     to the user should be tracked while all actions should not be (boolean).
 //   - args[5] - JSON of [xxdk.CMIXParams]. If left empty
 //     [bindings.GetDefaultCMixParams] will be used internally (Uint8Array).
+//   - args[6] - JSON of a slice of public keys of users that should receive
+//     mobile notifications for the message.
+//
+// Example slice of public keys:
+//
+//	[
+//	  "FgJMvgSsY4rrKkS/jSe+vFOJOs5qSSyOUSW7UtF9/KU=",
+//	  "fPqcHtrJ398PAC35QyWXEU9PHzz8Z4BKQTCxSvpSygw=",
+//	  "JnjCgh7g/+hNiI9VPKW01aRSxGOFmNulNCymy3ImXAo="
+//	]
 //
 // Returns a promise:
 //   - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array).
@@ -1038,13 +1015,13 @@ func (cm *ChannelsManager) SendGeneric(_ js.Value, args []js.Value) any {
 	leaseTimeMS := int64(args[3].Int())
 	tracked := args[4].Bool()
 	cmixParamsJSON := utils.CopyBytesToGo(args[5])
+	pingsJSON := utils.CopyBytesToGo(args[6])
 
-	// fixme: add pings to wasm
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
 		sendReport, err := cm.api.SendGeneric(marshalledChanId, messageType,
-			msg, leaseTimeMS, tracked, cmixParamsJSON, nil)
+			msg, leaseTimeMS, tracked, cmixParamsJSON, pingsJSON)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(utils.CopyBytesToJS(sendReport))
 		}
@@ -1072,6 +1049,16 @@ func (cm *ChannelsManager) SendGeneric(_ js.Value, args []js.Value) any {
 //     be enumerated here. Use [ValidForever] to last the max message life.
 //   - args[3] - JSON of [xxdk.CMIXParams]. If left empty
 //     [bindings.GetDefaultCMixParams] will be used internally (Uint8Array).
+//   - args[4] - JSON of a slice of public keys of users that should receive
+//     mobile notifications for the message.
+//
+// Example slice of public keys:
+//
+//	[
+//	  "FgJMvgSsY4rrKkS/jSe+vFOJOs5qSSyOUSW7UtF9/KU=",
+//	  "fPqcHtrJ398PAC35QyWXEU9PHzz8Z4BKQTCxSvpSygw=",
+//	  "JnjCgh7g/+hNiI9VPKW01aRSxGOFmNulNCymy3ImXAo="
+//	]
 //
 // Returns a promise:
 //   - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array).
@@ -1081,13 +1068,13 @@ func (cm *ChannelsManager) SendMessage(_ js.Value, args []js.Value) any {
 	msg := args[1].String()
 	leaseTimeMS := int64(args[2].Int())
 	cmixParamsJSON := utils.CopyBytesToGo(args[3])
+	pingsJSON := utils.CopyBytesToGo(args[4])
 
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
-		// fixme: add pings to wasm
 		sendReport, err := cm.api.SendMessage(
-			marshalledChanId, msg, leaseTimeMS, cmixParamsJSON, nil)
+			marshalledChanId, msg, leaseTimeMS, cmixParamsJSON, pingsJSON)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(utils.CopyBytesToJS(sendReport))
 		}
@@ -1122,6 +1109,16 @@ func (cm *ChannelsManager) SendMessage(_ js.Value, args []js.Value) any {
 //     be enumerated here. Use [ValidForever] to last the max message life.
 //   - args[4] - JSON of [xxdk.CMIXParams]. If left empty
 //     [bindings.GetDefaultCMixParams] will be used internally (Uint8Array).
+//   - args[5] - JSON of a slice of public keys of users that should receive
+//     mobile notifications for the message.
+//
+// Example slice of public keys:
+//
+//	[
+//	  "FgJMvgSsY4rrKkS/jSe+vFOJOs5qSSyOUSW7UtF9/KU=",
+//	  "fPqcHtrJ398PAC35QyWXEU9PHzz8Z4BKQTCxSvpSygw=",
+//	  "JnjCgh7g/+hNiI9VPKW01aRSxGOFmNulNCymy3ImXAo="
+//	]
 //
 // Returns a promise:
 //   - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array).
@@ -1132,13 +1129,13 @@ func (cm *ChannelsManager) SendReply(_ js.Value, args []js.Value) any {
 	messageToReactTo := utils.CopyBytesToGo(args[2])
 	leaseTimeMS := int64(args[3].Int())
 	cmixParamsJSON := utils.CopyBytesToGo(args[4])
+	pingsJSON := utils.CopyBytesToGo(args[5])
 
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
-		// fixme: add pings to wasm
 		sendReport, err := cm.api.SendReply(marshalledChanId, msg,
-			messageToReactTo, leaseTimeMS, cmixParamsJSON, nil)
+			messageToReactTo, leaseTimeMS, cmixParamsJSON, pingsJSON)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(utils.CopyBytesToJS(sendReport))
 		}
@@ -1186,7 +1183,46 @@ func (cm *ChannelsManager) SendReaction(_ js.Value, args []js.Value) any {
 		sendReport, err := cm.api.SendReaction(marshalledChanId, reaction,
 			messageToReactTo, leaseTimeMS, cmixParamsJSON)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
+		} else {
+			resolve(utils.CopyBytesToJS(sendReport))
+		}
+	}
+
+	return utils.CreatePromise(promiseFn)
+}
+
+// SendSilent is used to send to a channel a message with no notifications.
+// Its primary purpose is to communicate new nicknames without calling
+// [SendMessage].
+//
+// It takes no payload intentionally as the message should be very lightweight.
+//
+// Parameters:
+//   - args[0] - Marshalled bytes of the channel [id.ID] (Uint8Array).
+//   - args[1] - The lease of the message. This will be how long the
+//     message is available from the network, in milliseconds (int). As per the
+//     [channels.Manager] documentation, this has different meanings depending
+//     on the use case. These use cases may be generic enough that they will not
+//     be enumerated here. Use [ValidForever] to last the max message life.
+//   - args[2] - JSON of [xxdk.CMIXParams]. If left empty
+//     [bindings.GetDefaultCMixParams] will be used internally (Uint8Array).
+//
+// Returns a promise:
+//   - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array).
+//   - Rejected with an error if sending fails.
+func (cm *ChannelsManager) SendSilent(_ js.Value, args []js.Value) any {
+	var (
+		marshalledChanId = utils.CopyBytesToGo(args[0])
+		leaseTimeMS      = int64(args[1].Int())
+		cmixParamsJSON   = utils.CopyBytesToGo(args[2])
+	)
+
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		sendReport, err := cm.api.SendSilent(
+			marshalledChanId, leaseTimeMS, cmixParamsJSON)
+		if err != nil {
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(utils.CopyBytesToJS(sendReport))
 		}
@@ -1215,6 +1251,16 @@ func (cm *ChannelsManager) SendReaction(_ js.Value, args []js.Value) any {
 //     be enumerated here. Use [ValidForever] to last the max message life.
 //   - args[6] - JSON of [xxdk.CMIXParams]. If left empty
 //     [bindings.GetDefaultCMixParams] will be used internally (Uint8Array).
+//   - args[7] - JSON of a slice of public keys of users that should receive
+//     mobile notifications for the message.
+//
+// Example slice of public keys:
+//
+//	[
+//	  "FgJMvgSsY4rrKkS/jSe+vFOJOs5qSSyOUSW7UtF9/KU=",
+//	  "fPqcHtrJ398PAC35QyWXEU9PHzz8Z4BKQTCxSvpSygw=",
+//	  "JnjCgh7g/+hNiI9VPKW01aRSxGOFmNulNCymy3ImXAo="
+//	]
 //
 // Returns a promise:
 //   - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array).
@@ -1228,15 +1274,15 @@ func (cm *ChannelsManager) SendInvite(_ js.Value, args []js.Value) any {
 		maxUses              = args[4].Int()
 		leaseTimeMS          = int64(args[5].Int())
 		cmixParamsJSON       = utils.CopyBytesToGo(args[6])
+		pingsJSON            = utils.CopyBytesToGo(args[7])
 	)
 
-	// fixme: add pings to wasm
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
 		sendReport, err := cm.api.SendInvite(marshalledChanId,
 			marshalledInviteToId, msg, host, maxUses, leaseTimeMS,
-			cmixParamsJSON, nil)
+			cmixParamsJSON, pingsJSON)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(utils.CopyBytesToJS(sendReport))
 		}
@@ -1293,7 +1339,7 @@ func (cm *ChannelsManager) SendAdminGeneric(_ js.Value, args []js.Value) any {
 		sendReport, err := cm.api.SendAdminGeneric(marshalledChanId,
 			messageType, msg, leaseTimeMS, tracked, cmixParamsJSON)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(utils.CopyBytesToJS(sendReport))
 		}
@@ -1332,7 +1378,7 @@ func (cm *ChannelsManager) DeleteMessage(_ js.Value, args []js.Value) any {
 		sendReport, err := cm.api.DeleteMessage(
 			channelIdBytes, targetMessageIdBytes, cmixParamsJSON)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(utils.CopyBytesToJS(sendReport))
 		}
@@ -1374,7 +1420,7 @@ func (cm *ChannelsManager) PinMessage(_ js.Value, args []js.Value) any {
 		sendReport, err := cm.api.PinMessage(channelIdBytes,
 			targetMessageIdBytes, undoAction, validUntilMS, cmixParamsJSON)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(utils.CopyBytesToJS(sendReport))
 		}
@@ -1415,7 +1461,7 @@ func (cm *ChannelsManager) MuteUser(_ js.Value, args []js.Value) any {
 		sendReport, err := cm.api.MuteUser(channelIdBytes, mutedUserPubKeyBytes,
 			undoAction, validUntilMS, cmixParamsJSON)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(utils.CopyBytesToJS(sendReport))
 		}
@@ -1437,7 +1483,7 @@ func (cm *ChannelsManager) MuteUser(_ js.Value, args []js.Value) any {
 func (cm *ChannelsManager) GetIdentity(js.Value, []js.Value) any {
 	i, err := cm.api.GetIdentity()
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -1456,7 +1502,7 @@ func (cm *ChannelsManager) GetIdentity(js.Value, []js.Value) any {
 func (cm *ChannelsManager) ExportPrivateIdentity(_ js.Value, args []js.Value) any {
 	i, err := cm.api.ExportPrivateIdentity(args[0].String())
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -1485,7 +1531,7 @@ func (cm *ChannelsManager) GetStorageTag(js.Value, []js.Value) any {
 func (cm *ChannelsManager) SetNickname(_ js.Value, args []js.Value) any {
 	err := cm.api.SetNickname(args[0].String(), utils.CopyBytesToGo(args[1]))
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -1503,7 +1549,7 @@ func (cm *ChannelsManager) SetNickname(_ js.Value, args []js.Value) any {
 func (cm *ChannelsManager) DeleteNickname(_ js.Value, args []js.Value) any {
 	err := cm.api.DeleteNickname(utils.CopyBytesToGo(args[0]))
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -1522,7 +1568,7 @@ func (cm *ChannelsManager) DeleteNickname(_ js.Value, args []js.Value) any {
 func (cm *ChannelsManager) GetNickname(_ js.Value, args []js.Value) any {
 	nickname, err := cm.api.GetNickname(utils.CopyBytesToGo(args[0]))
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -1544,7 +1590,7 @@ func (cm *ChannelsManager) GetNickname(_ js.Value, args []js.Value) any {
 func IsNicknameValid(_ js.Value, args []js.Value) any {
 	err := bindings.IsNicknameValid(args[0].String())
 	if err != nil {
-		return utils.JsError(err)
+		return exception.NewError(err)
 	}
 
 	return nil
@@ -1558,13 +1604,13 @@ func IsNicknameValid(_ js.Value, args []js.Value) any {
 // Returns:
 //   - Returns true if the user is muted in the channel and false otherwise
 //     (boolean).
-//   - Throws a TypeError if the channel ID cannot be unmarshalled.
+//   - Throws an error if the channel ID cannot be unmarshalled.
 func (cm *ChannelsManager) Muted(_ js.Value, args []js.Value) any {
 	channelIDBytes := utils.CopyBytesToGo(args[0])
 
 	muted, err := cm.api.Muted(channelIDBytes)
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -1581,7 +1627,7 @@ func (cm *ChannelsManager) Muted(_ js.Value, args []js.Value) any {
 // Returns:
 //   - JSON of an array of ed25519.PublicKey (Uint8Array). Look below for an
 //     example.
-//   - Throws a TypeError if the channel ID cannot be unmarshalled.
+//   - Throws an error if the channel ID cannot be unmarshalled.
 //
 // Example return:
 //
@@ -1590,13 +1636,151 @@ func (cm *ChannelsManager) GetMutedUsers(_ js.Value, args []js.Value) any {
 	channelIDBytes := utils.CopyBytesToGo(args[0])
 	mutedUsers, err := cm.api.GetMutedUsers(channelIDBytes)
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
 	return utils.CopyBytesToJS(mutedUsers)
 }
 
+////////////////////////////////////////////////////////////////////////////////
+// Notifications                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+// GetNotificationLevel returns the [channels.NotificationLevel] for the given
+// channel.
+//
+// Parameters:
+//   - args[0] - The marshalled bytes of the channel's [id.ID] (Uint8Array).
+//
+// Returns:
+//   - The [channels.NotificationLevel] for the channel (int).
+//   - Throws an error if the channel ID cannot be unmarshalled or the channel
+//     cannot be found.
+func (cm *ChannelsManager) GetNotificationLevel(_ js.Value, args []js.Value) any {
+	channelIDBytes := utils.CopyBytesToGo(args[0])
+
+	level, err := cm.api.GetNotificationLevel(channelIDBytes)
+	if err != nil {
+		exception.ThrowTrace(err)
+		return nil
+	}
+
+	return level
+}
+
+// SetMobileNotificationsLevel sets the notification level for the given
+// channel. The [channels.NotificationLevel] dictates the type of notifications
+// received and the status controls weather the notification is push or in-app.
+// If muted, both the level and status must be set to mute.
+//
+// To use push notifications, a token must be registered with the notification
+// manager. Note, when enabling push notifications, information may be shared
+// with third parties (i.e., Firebase and Google's Palantir) and may represent a
+// security risk to the user.
+//
+// Parameters:
+//   - args[0] - The marshalled bytes of the channel's [id.ID] (Uint8Array).
+//   - args[1] - The [channels.NotificationLevel] to set for the channel (int).
+//   - args[2] - The [notifications.NotificationState] to set for the channel
+//     (int).
+//
+// Returns:
+//   - Throws an error if setting the notification level fails.
+func (cm *ChannelsManager) SetMobileNotificationsLevel(_ js.Value, args []js.Value) any {
+	channelIDBytes := utils.CopyBytesToGo(args[0])
+	level := args[1].Int()
+	status := args[2].Int()
+
+	err := cm.api.SetMobileNotificationsLevel(channelIDBytes, level, status)
+	if err != nil {
+		exception.ThrowTrace(err)
+		return nil
+	}
+
+	return nil
+}
+
+// GetNotificationReportsForMe checks the notification data against the filter
+// list to determine which notifications belong to the user. A list of
+// notification reports is returned detailing all notifications for the user.
+//
+// Parameters:
+//   - notificationFilterJSON - JSON of a slice of [channels.NotificationFilter].
+//   - notificationDataJSON - JSON of a slice of [notifications.Data].
+//
+// Example JSON of a slice of [channels.NotificationFilter]:
+// [
+//
+//	  {
+//	    "identifier": "O8NUg0KaDo18ybTKajXM/sgqEYS37+lewPhGV/2sMAUDYXN5bUlkZW50aWZpZXI=",
+//	    "channelID": "O8NUg0KaDo18ybTKajXM/sgqEYS37+lewPhGV/2sMAUD",
+//	    "tags": ["6de69009a93d53793ee344e8fb48fae194eaf51861d3cc51c7348c337d13aedf-usrping"],
+//	    "allowLists": {
+//	      "allowWithTags": {},
+//	      "allowWithoutTags": {"102":{}, "2":{}}
+//	    }
+//	  },
+//	  {
+//	    "identifier": "O8NUg0KaDo18ybTKajXM/sgqEYS37+lewPhGV/2sMAUDc3ltSWRlbnRpZmllcg==",
+//	    "channelID": "O8NUg0KaDo18ybTKajXM/sgqEYS37+lewPhGV/2sMAUD",
+//	    "tags": ["6de69009a93d53793ee344e8fb48fae194eaf51861d3cc51c7348c337d13aedf-usrping"],
+//	    "allowLists": {
+//	      "allowWithTags": {},
+//	      "allowWithoutTags": {"1":{}, "40000":{}}
+//	    }
+//	  },
+//	  {
+//	    "identifier": "jCRgFRQvzzKOb8DJ0fqCRLgr9kiHN9LpqHXVhyHhhlQDYXN5bUlkZW50aWZpZXI=",
+//	    "channelID": "jCRgFRQvzzKOb8DJ0fqCRLgr9kiHN9LpqHXVhyHhhlQD",
+//	    "tags": ["6de69009a93d53793ee344e8fb48fae194eaf51861d3cc51c7348c337d13aedf-usrping"],
+//	    "allowLists": {
+//	      "allowWithTags": {},
+//	      "allowWithoutTags": {"102":{}, "2":{}}
+//	    }
+//	  }
+//	]
+//
+// Example JSON of a slice of [notifications.Data]:
+//
+//	[
+//	  {
+//	    "EphemeralID": -6475,
+//	    "RoundID": 875,
+//	    "IdentityFP": "jWG/UuxRjD80HEo0WX3KYIag5LCfgaWKAg==",
+//	    "MessageHash": "hDGE46QWa3d70y5nJTLbEaVmrFJHOyp2"
+//	  },
+//	  {
+//	    "EphemeralID": -2563,
+//	    "RoundID": 875,
+//	    "IdentityFP": "gL4nhCGKPNBm6YZ7KC0v4JThw65N9bRLTQ==",
+//	    "MessageHash": "WcS4vGrSWDK8Kj7JYOkMo8kSh1Xti94V"
+//	  },
+//	  {
+//	    "EphemeralID": -13247,
+//	    "RoundID": 875,
+//	    "IdentityFP": "qV3uD++VWPhD2rRMmvrP9j8hp+jpFSsUHg==",
+//	    "MessageHash": "VX6Tw7N48j7U2rRXYle20mFZi0If4CB1"
+//	  }
+//	]
+//
+// Returns:
+//   - The JSON of a slice of [channels.NotificationReport] (Uint8Array).
+//   - Throws an error if getting the report fails.
+func GetNotificationReportsForMe(_ js.Value, args []js.Value) any {
+	notificationFilterJSON := utils.CopyBytesToGo(args[0])
+	notificationDataJSON := utils.CopyBytesToGo(args[1])
+
+	report, err := bindings.GetNotificationReportsForMe(
+		notificationFilterJSON, notificationDataJSON)
+	if err != nil {
+		exception.ThrowTrace(err)
+		return nil
+	}
+
+	return utils.CopyBytesToJS(report)
+}
+
 ////////////////////////////////////////////////////////////////////////////////
 // Admin Management                                                           //
 ////////////////////////////////////////////////////////////////////////////////
@@ -1609,11 +1793,11 @@ func (cm *ChannelsManager) GetMutedUsers(_ js.Value, args []js.Value) any {
 // Returns:
 //   - True if the user is an admin in the channel and false otherwise
 //     (boolean).
-//   - Throws a TypeError if the channel ID cannot be unmarshalled.
+//   - Throws an error if the channel ID cannot be unmarshalled.
 func (cm *ChannelsManager) IsChannelAdmin(_ js.Value, args []js.Value) any {
 	isAdmin, err := cm.api.IsChannelAdmin(utils.CopyBytesToGo(args[0]))
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -1645,12 +1829,12 @@ func (cm *ChannelsManager) IsChannelAdmin(_ js.Value, args []js.Value) any {
 // Returns:
 //   - Portable string of the channel private key encrypted with the password
 //     (Uint8Array).
-//   - Throws a TypeError if the user is not an admin for the channel.
+//   - Throws an error if the user is not an admin for the channel.
 func (cm *ChannelsManager) ExportChannelAdminKey(_ js.Value, args []js.Value) any {
 	pk, err := cm.api.ExportChannelAdminKey(
 		utils.CopyBytesToGo(args[0]), args[1].String())
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 	return utils.CopyBytesToJS(pk)
@@ -1668,14 +1852,14 @@ func (cm *ChannelsManager) ExportChannelAdminKey(_ js.Value, args []js.Value) an
 // Returns:
 //   - Returns false if private key does not belong to the given channel ID
 //     (boolean).
-//   - Throws a TypeError if the password is invalid.
+//   - Throws an error if the password is invalid.
 //
 // Returns:
 //   - bool - True if the private key belongs to the channel and false
 //     otherwise.
-//   - Throws a TypeError with the message [channels.WrongPasswordErr] for an
+//   - Throws an error with the message [channels.WrongPasswordErr] for an
 //     invalid password.
-//   - Throws a TypeError with the message [channels.ChannelDoesNotExistsErr] i
+//   - Throws an error with the message [channels.ChannelDoesNotExistsErr] i
 //     the channel has not already been joined.
 func (cm *ChannelsManager) VerifyChannelAdminKey(_ js.Value, args []js.Value) any {
 	channelID := utils.CopyBytesToGo(args[0])
@@ -1684,7 +1868,7 @@ func (cm *ChannelsManager) VerifyChannelAdminKey(_ js.Value, args []js.Value) an
 	valid, err := cm.api.VerifyChannelAdminKey(
 		channelID, encryptionPassword, encryptedPrivKey)
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -1702,13 +1886,13 @@ func (cm *ChannelsManager) VerifyChannelAdminKey(_ js.Value, args []js.Value) an
 //   - args[2] - The encrypted channel private key packet (Uint8Array).
 //
 // Returns:
-//   - Throws a TypeError if the password is invalid or the private key does
+//   - Throws an error if the password is invalid or the private key does
 //     not match the channel ID.
-//   - Throws a TypeError with the message [channels.WrongPasswordErr] for an
+//   - Throws an error with the message [channels.WrongPasswordErr] for an
 //     invalid password.
-//   - Throws a TypeError with the message [channels.ChannelDoesNotExistsErr] if
+//   - Throws an error with the message [channels.ChannelDoesNotExistsErr] if
 //     the channel has not already been joined.
-//   - Throws a TypeError with the message [channels.WrongPrivateKeyErr] if the
+//   - Throws an error with the message [channels.WrongPrivateKeyErr] if the
 //     private key does not belong to the channel.
 func (cm *ChannelsManager) ImportChannelAdminKey(_ js.Value, args []js.Value) any {
 	channelID := utils.CopyBytesToGo(args[0])
@@ -1717,7 +1901,7 @@ func (cm *ChannelsManager) ImportChannelAdminKey(_ js.Value, args []js.Value) an
 	err := cm.api.ImportChannelAdminKey(
 		channelID, encryptionPassword, encryptedPrivKey)
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -1734,11 +1918,11 @@ func (cm *ChannelsManager) ImportChannelAdminKey(_ js.Value, args []js.Value) an
 //   - args[0] - The marshalled bytes of the channel's [id.ID] (Uint8Array)
 //
 // Returns:
-//   - Throws a TypeError if the deletion fails.
+//   - Throws an error if the deletion fails.
 func (cm *ChannelsManager) DeleteChannelAdminKey(_ js.Value, args []js.Value) any {
 	err := cm.api.DeleteChannelAdminKey(utils.CopyBytesToGo(args[0]))
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -1768,7 +1952,7 @@ type channelMessageReceptionCallback struct {
 func (cmrCB *channelMessageReceptionCallback) Callback(
 	receivedChannelMessageReport []byte, err error) int {
 	uuid := cmrCB.callback(
-		utils.CopyBytesToJS(receivedChannelMessageReport), utils.JsTrace(err))
+		utils.CopyBytesToJS(receivedChannelMessageReport), exception.NewTrace(err))
 
 	return uuid.Int()
 }
@@ -1796,7 +1980,7 @@ func (cmrCB *channelMessageReceptionCallback) Callback(
 //     users (boolean).
 //
 // Returns:
-//   - Throws a TypeError if registering the handler fails.
+//   - Throws an error if registering the handler fails.
 func (cm *ChannelsManager) RegisterReceiveHandler(_ js.Value, args []js.Value) any {
 	messageType := args[0].Int()
 	listenerCb := &channelMessageReceptionCallback{
@@ -1809,7 +1993,7 @@ func (cm *ChannelsManager) RegisterReceiveHandler(_ js.Value, args []js.Value) a
 	err := cm.api.RegisterReceiveHandler(
 		messageType, listenerCb, name, userSpace, adminSpace, mutedSpace)
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -1841,7 +2025,7 @@ func GetNoMessageErr(js.Value, []js.Value) any {
 // Returns
 //   - True if the error contains channels.NoMessageErr (boolean).
 func CheckNoMessageErr(_ js.Value, args []js.Value) any {
-	return bindings.CheckNoMessageErr(utils.JsErrorToJson(args[0]))
+	return bindings.CheckNoMessageErr(js.Error{Value: args[0]}.Error())
 }
 
 // eventModelBuilder adheres to the [bindings.EventModelBuilder] interface.
@@ -2227,7 +2411,7 @@ func newChannelDbCipherJS(api *bindings.ChannelDbCipher) map[string]any {
 //
 // Returns:
 //   - JavaScript representation of the [ChannelDbCipher] object.
-//   - Throws a TypeError if creating the cipher fails.
+//   - Throws an error if creating the cipher fails.
 func NewChannelsDatabaseCipher(_ js.Value, args []js.Value) any {
 	cmixId := args[0].Int()
 	password := utils.CopyBytesToGo(args[1])
@@ -2236,7 +2420,7 @@ func NewChannelsDatabaseCipher(_ js.Value, args []js.Value) any {
 	cipher, err := bindings.NewChannelsDatabaseCipher(
 		cmixId, password, plaintTextBlockSize)
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -2262,11 +2446,11 @@ func (c *ChannelDbCipher) GetID(js.Value, []js.Value) any {
 //
 // Returns:
 //   - The ciphertext of the plaintext passed in (Uint8Array).
-//   - Throws a TypeError if it fails to encrypt the plaintext.
+//   - Throws an error if it fails to encrypt the plaintext.
 func (c *ChannelDbCipher) Encrypt(_ js.Value, args []js.Value) any {
 	ciphertext, err := c.api.Encrypt(utils.CopyBytesToGo(args[0]))
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -2283,11 +2467,11 @@ func (c *ChannelDbCipher) Encrypt(_ js.Value, args []js.Value) any {
 //
 // Returns:
 //   - The plaintext of the ciphertext passed in (Uint8Array).
-//   - Throws a TypeError if it fails to encrypt the plaintext.
+//   - Throws an error if it fails to encrypt the plaintext.
 func (c *ChannelDbCipher) Decrypt(_ js.Value, args []js.Value) any {
 	plaintext, err := c.api.Decrypt(utils.CopyBytesToGo(args[0]))
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -2298,11 +2482,11 @@ func (c *ChannelDbCipher) Decrypt(_ js.Value, args []js.Value) any {
 //
 // Returns:
 //   - JSON of the cipher (Uint8Array).
-//   - Throws a TypeError if marshalling fails.
+//   - Throws an error if marshalling fails.
 func (c *ChannelDbCipher) MarshalJSON(js.Value, []js.Value) any {
 	data, err := c.api.MarshalJSON()
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -2319,12 +2503,32 @@ func (c *ChannelDbCipher) MarshalJSON(js.Value, []js.Value) any {
 //
 // Returns:
 //   - JSON of the cipher (Uint8Array).
-//   - Throws a TypeError if marshalling fails.
+//   - Throws an error if marshalling fails.
 func (c *ChannelDbCipher) UnmarshalJSON(_ js.Value, args []js.Value) any {
 	err := c.api.UnmarshalJSON(utils.CopyBytesToGo(args[0]))
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 	return nil
 }
+
+// newChannelUI maps the methods on the Javascript object to the
+// channelUI callbacks implementation struct.
+func newChannelUI(cbImpl js.Value) *channelUI {
+	return &channelUI{
+		eventUpdate: utils.WrapCB(cbImpl, "EventUpdate"),
+	}
+}
+
+// eventModel wraps Javascript callbacks to adhere to the
+// [bindings.ChannelUICallbacks] interface.
+type channelUI struct {
+	eventUpdate func(args ...any) js.Value
+}
+
+// EventUpdate implements
+// [bindings.ChannelUICallbacks.EventUpdate].
+func (c *channelUI) EventUpdate(eventType int64, jsonData []byte) {
+	c.eventUpdate(int(eventType), utils.CopyBytesToJS(jsonData))
+}
diff --git a/wasm/channelsFileTransfer.go b/wasm/channelsFileTransfer.go
index 5155e4ad4668bef0f3ad52f52efec748e7dbb0ff..2a76200423e1ac2aefb94a90857e8c70dc7777c1 100644
--- a/wasm/channelsFileTransfer.go
+++ b/wasm/channelsFileTransfer.go
@@ -11,7 +11,8 @@ package wasm
 
 import (
 	"gitlab.com/elixxir/client/v4/bindings"
-	"gitlab.com/elixxir/xxdk-wasm/utils"
+	"gitlab.com/elixxir/wasm-utils/exception"
+	"gitlab.com/elixxir/wasm-utils/utils"
 	"syscall/js"
 )
 
@@ -67,7 +68,7 @@ func InitChannelsFileTransfer(_ js.Value, args []js.Value) any {
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
 		cft, err := bindings.InitChannelsFileTransfer(e2eID, paramsJson)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(newChannelsFileTransferJS(cft))
 		}
@@ -164,7 +165,7 @@ func (cft *ChannelsFileTransfer) Upload(_ js.Value, args []js.Value) any {
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
 		fileID, err := cft.api.Upload(fileData, retry, progressCB, period)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(utils.CopyBytesToJS(fileID))
 		}
@@ -211,7 +212,7 @@ func (cft *ChannelsFileTransfer) Send(_ js.Value, args []js.Value) any {
 		fileID, err := cft.api.Send(channelIdBytes, fileLinkJSON, fileName,
 			fileType, preview, validUntilMS, cmixParamsJSON)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(utils.CopyBytesToJS(fileID))
 		}
@@ -264,7 +265,7 @@ func (cft *ChannelsFileTransfer) RegisterSentProgressCallback(
 		err := cft.api.RegisterSentProgressCallback(
 			fileIDBytes, progressCB, periodMS)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve()
 		}
@@ -305,7 +306,7 @@ func (cft *ChannelsFileTransfer) RetryUpload(_ js.Value, args []js.Value) any {
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
 		err := cft.api.RetryUpload(fileIDBytes, progressCB, periodMS)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve()
 		}
@@ -334,7 +335,7 @@ func (cft *ChannelsFileTransfer) CloseSend(_ js.Value, args []js.Value) any {
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
 		err := cft.api.CloseSend(fileIDBytes)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve()
 		}
@@ -386,7 +387,7 @@ func (cft *ChannelsFileTransfer) Download(_ js.Value, args []js.Value) any {
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
 		fileID, err := cft.api.Download(fileInfoJSON, progressCB, periodMS)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(utils.CopyBytesToJS(fileID))
 		}
@@ -438,7 +439,7 @@ func (cft *ChannelsFileTransfer) RegisterReceivedProgressCallback(
 		err := cft.api.RegisterReceivedProgressCallback(
 			fileIDBytes, progressCB, periodMS)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve()
 		}
@@ -490,7 +491,7 @@ type ftSentCallback struct {
 func (fsc *ftSentCallback) Callback(
 	payload []byte, t *bindings.ChFilePartTracker, err error) {
 	fsc.callback(utils.CopyBytesToJS(payload), newChFilePartTrackerJS(t),
-		utils.JsTrace(err))
+		exception.NewTrace(err))
 }
 
 // ftReceivedCallback wraps Javascript callbacks to adhere to the
@@ -518,7 +519,7 @@ type ftReceivedCallback struct {
 func (frc *ftReceivedCallback) Callback(
 	payload []byte, t *bindings.ChFilePartTracker, err error) {
 	frc.callback(utils.CopyBytesToJS(payload), newChFilePartTrackerJS(t),
-		utils.JsTrace(err))
+		exception.NewTrace(err))
 }
 
 ////////////////////////////////////////////////////////////////////////////////
diff --git a/wasm/channels_test.go b/wasm/channels_test.go
index 028e6cbf305e46bc87f398abca1a93918e1f3669..1b4d9eb555177951ea66e9fb81ed7420418f2b22 100644
--- a/wasm/channels_test.go
+++ b/wasm/channels_test.go
@@ -12,7 +12,7 @@ package wasm
 import (
 	"gitlab.com/elixxir/client/v4/bindings"
 	"gitlab.com/elixxir/crypto/channel"
-	"gitlab.com/elixxir/xxdk-wasm/utils"
+	"gitlab.com/elixxir/wasm-utils/utils"
 	"gitlab.com/xx_network/crypto/csprng"
 	"reflect"
 	"syscall/js"
diff --git a/wasm/cmix.go b/wasm/cmix.go
index 88c9853db7654fb065ab91a6fa9b0dc1fcbddd54..be7ad6d31f2052b6f9750703611b4789cff35009 100644
--- a/wasm/cmix.go
+++ b/wasm/cmix.go
@@ -10,9 +10,11 @@
 package wasm
 
 import (
-	"gitlab.com/elixxir/client/v4/bindings"
-	"gitlab.com/elixxir/xxdk-wasm/utils"
 	"syscall/js"
+
+	"gitlab.com/elixxir/client/v4/bindings"
+	"gitlab.com/elixxir/wasm-utils/exception"
+	"gitlab.com/elixxir/wasm-utils/utils"
 )
 
 // Cmix wraps the [bindings.Cmix] object so its methods can be wrapped to be
@@ -27,7 +29,11 @@ func newCmixJS(api *bindings.Cmix) map[string]any {
 	c := Cmix{api}
 	cmix := map[string]any{
 		// cmix.go
-		"GetID": js.FuncOf(c.GetID),
+		"GetID":          js.FuncOf(c.GetID),
+		"GetReceptionID": js.FuncOf(c.GetReceptionID),
+		"GetRemoteKV":    js.FuncOf(c.GetRemoteKV),
+		"EKVGet":         js.FuncOf(c.EKVGet),
+		"EKVSet":         js.FuncOf(c.EKVSet),
 
 		// identity.go
 		"MakeReceptionIdentity": js.FuncOf(
@@ -95,7 +101,7 @@ func NewCmix(_ js.Value, args []js.Value) any {
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
 		err := bindings.NewCmix(ndfJSON, storageDir, password, registrationCode)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve()
 		}
@@ -130,7 +136,38 @@ func LoadCmix(_ js.Value, args []js.Value) any {
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
 		net, err := bindings.LoadCmix(storageDir, password, cmixParamsJSON)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
+		} else {
+			resolve(newCmixJS(net))
+		}
+	}
+
+	return utils.CreatePromise(promiseFn)
+}
+
+// LoadSynchronizedCmix will [LoadCmix] using a RemoteStore to establish
+// a synchronized RemoteKV.
+//
+// Parameters:
+//   - args[0] - Storage directory path (string).
+//   - args[1] - Password used for storage (Uint8Array).
+//   - args[2] - Javascript [RemoteStore] implementation.
+//   - args[3] - JSON of [xxdk.CMIXParams] (Uint8Array).
+//
+// Returns a promise:
+//   - Resolves to a Javascript representation of the [Cmix] object.
+//   - Rejected with an error if loading [Cmix] fails.
+func LoadSynchronizedCmix(_ js.Value, args []js.Value) any {
+	storageDir := args[0].String()
+	password := utils.CopyBytesToGo(args[1])
+	rs := newRemoteStore(args[2])
+	cmixParamsJSON := utils.CopyBytesToGo(args[3])
+
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		net, err := bindings.LoadSynchronizedCmix(storageDir, password,
+			rs, cmixParamsJSON)
+		if err != nil {
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(newCmixJS(net))
 		}
@@ -146,3 +183,73 @@ func LoadCmix(_ js.Value, args []js.Value) any {
 func (c *Cmix) GetID(js.Value, []js.Value) any {
 	return c.api.GetID()
 }
+
+// GetReceptionID returns the default reception identity for this cMix instance.
+//
+// Returns:
+//   - Marshalled bytes of [id.ID] (Uint8Array).
+func (c *Cmix) GetReceptionID(js.Value, []js.Value) any {
+	return utils.CopyBytesToJS(c.api.GetReceptionID())
+}
+
+// GetRemoteKV returns the cMix RemoteKV
+//
+// Returns a promise:
+//   - Resolves with the RemoteKV object.
+func (c *Cmix) GetRemoteKV(_ js.Value, args []js.Value) any {
+
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		kv := c.api.GetRemoteKV()
+		resolve(newRemoteKvJS(kv))
+	}
+
+	return utils.CreatePromise(promiseFn)
+}
+
+// EKVGet allows access to a value inside the secure encrypted key value store.
+//
+// Parameters:
+//   - args[0] - Key (string).
+//
+// Returns a promise:
+//   - Resolves to the value (Uint8Array)
+//   - Rejected with an error if accessing the KV fails.
+func (c *Cmix) EKVGet(_ js.Value, args []js.Value) any {
+	key := args[0].String()
+
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		val, err := c.api.EKVGet(key)
+		if err != nil {
+			reject(exception.NewTrace(err))
+		} else {
+			resolve(utils.CopyBytesToJS(val))
+		}
+	}
+
+	return utils.CreatePromise(promiseFn)
+}
+
+// EKVSet sets a value inside the secure encrypted key value store.
+//
+// Parameters:
+//   - args[0] - Key (string).
+//   - args[1] - Value (Uint8Array).
+//
+// Returns a promise:
+//   - Resolves on a successful save (void).
+//   - Rejected with an error if saving fails.
+func (c *Cmix) EKVSet(_ js.Value, args []js.Value) any {
+	key := args[0].String()
+	val := utils.CopyBytesToGo(args[1])
+
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		err := c.api.EKVSet(key, val)
+		if err != nil {
+			reject(exception.NewTrace(err))
+		} else {
+			resolve(nil)
+		}
+	}
+
+	return utils.CreatePromise(promiseFn)
+}
diff --git a/wasm/collective.go b/wasm/collective.go
new file mode 100644
index 0000000000000000000000000000000000000000..e99f03aefab09cde52cde0107193033e6bb3ae16
--- /dev/null
+++ b/wasm/collective.go
@@ -0,0 +1,586 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+//go:build js && wasm
+
+package wasm
+
+import (
+	"syscall/js"
+
+	"gitlab.com/elixxir/client/v4/bindings"
+	"gitlab.com/elixxir/wasm-utils/exception"
+	"gitlab.com/elixxir/wasm-utils/utils"
+)
+
+////////////////////////////////////////////////////////////////////////////////
+// RemoteKV Methods                                                           //
+////////////////////////////////////////////////////////////////////////////////
+
+// RemoteKV wraps the [bindings.RemoteKV] object so its methods can be wrapped
+// to be Javascript compatible.
+type RemoteKV struct {
+	api *bindings.RemoteKV
+}
+
+// newRemoteKvJS creates a new Javascript compatible object (map[string]any)
+// that matches the [RemoteKV] structure.
+func newRemoteKvJS(api *bindings.RemoteKV) map[string]any {
+	rkv := RemoteKV{api}
+	rkvMap := map[string]any{
+		"Get":               js.FuncOf(rkv.Get),
+		"Delete":            js.FuncOf(rkv.Delete),
+		"Set":               js.FuncOf(rkv.Set),
+		"GetPrefix":         js.FuncOf(rkv.GetPrefix),
+		"HasPrefix":         js.FuncOf(rkv.HasPrefix),
+		"Prefix":            js.FuncOf(rkv.Prefix),
+		"Root":              js.FuncOf(rkv.Root),
+		"IsMemStore":        js.FuncOf(rkv.IsMemStore),
+		"GetFullKey":        js.FuncOf(rkv.GetFullKey),
+		"StoreMapElement":   js.FuncOf(rkv.StoreMapElement),
+		"StoreMap":          js.FuncOf(rkv.StoreMap),
+		"DeleteMapElement":  js.FuncOf(rkv.DeleteMapElement),
+		"GetMap":            js.FuncOf(rkv.GetMap),
+		"GetMapElement":     js.FuncOf(rkv.GetMapElement),
+		"ListenOnRemoteKey": js.FuncOf(rkv.ListenOnRemoteKey),
+		"ListenOnRemoteMap": js.FuncOf(rkv.ListenOnRemoteMap),
+	}
+
+	return rkvMap
+}
+
+// Get returns the object stored at the specified version.
+// returns a json of [versioned.Object].
+//
+// Parameters:
+//   - args[0] - key to access, a string
+//   - args[1] - version, an integer
+//
+// Returns a promise:
+//   - Resolves to JSON of a [versioned.Object], e.g.:
+//     {"Version":1,"Timestamp":"2023-05-13T00:50:03.889192694Z","Data":"bm90IHVwZ3JhZGVk"}
+//   - Rejected with an access error. Note: File does not exist errors
+//     are returned whent key is not set.
+func (r *RemoteKV) Get(_ js.Value, args []js.Value) any {
+	key := args[0].String()
+	version := int64(args[1].Int())
+
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		value, err := r.api.Get(key, version)
+		if err != nil {
+			reject(exception.NewTrace(err))
+		} else {
+			resolve(utils.CopyBytesToJS(value))
+		}
+	}
+
+	return utils.CreatePromise(promiseFn)
+}
+
+// Delete removes a given key from the data store.
+//
+// Parameters:
+//   - args[0] - key to access, a string
+//   - args[1] - version, an integer
+//
+// Returns a promise:
+//   - Rejected with an access error. Note: File does not exist errors
+//     are returned whent key is not set.
+func (r *RemoteKV) Delete(_ js.Value, args []js.Value) any {
+	key := args[0].String()
+	version := int64(args[1].Int())
+
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		err := r.api.Delete(key, version)
+		if err != nil {
+			reject(exception.NewTrace(err))
+		} else {
+			resolve()
+		}
+	}
+
+	return utils.CreatePromise(promiseFn)
+}
+
+// Set upserts new data into the storage
+// When calling this, you are responsible for prefixing the
+// key with the correct type optionally unique id! Call
+// GetFullKey() to do so.
+// The [Object] should contain the versioning if you are
+// maintaining such a functionality.
+//
+// Parameters:
+//   - args[0] - the key string
+//   - args[1] - the [versioned.Object] JSON value, e.g.:
+//     {"Version":1,"Timestamp":"2023-05-13T00:50:03.889192694Z",
+//     "Data":"bm90IHVwZ3JhZGVk"}
+//
+// Returns a promise:
+//   - Rejected with an access error.
+func (r *RemoteKV) Set(_ js.Value, args []js.Value) any {
+	key := args[0].String()
+	value := utils.CopyBytesToGo(args[1])
+
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		err := r.api.Set(key, value)
+		if err != nil {
+			reject(exception.NewTrace(err))
+		} else {
+			resolve()
+		}
+	}
+
+	return utils.CreatePromise(promiseFn)
+}
+
+// GetPrefix returns the full Prefix of the KV
+// Returns a string via a Promise
+func (r *RemoteKV) GetPrefix(_ js.Value, args []js.Value) any {
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		prefix := r.api.GetPrefix()
+		resolve(prefix)
+	}
+
+	return utils.CreatePromise(promiseFn)
+}
+
+// HasPrefix returns whether this prefix exists in the KV
+//
+// Parameters:
+//   - args[0] - the prefix string to check for.
+//
+// Returns a bool via a promise.
+func (r *RemoteKV) HasPrefix(_ js.Value, args []js.Value) any {
+	prefix := args[0].String()
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		resolve(r.api.HasPrefix(prefix))
+	}
+
+	return utils.CreatePromise(promiseFn)
+}
+
+// Prefix returns a new KV with the new prefix appending
+//
+// Parameters:
+//   - args[0] - the prefix to append to the list of prefixes
+//
+// Returns a promise:
+//   - Resolves to a new RemoteKV
+//   - Rejected with an error.
+func (r *RemoteKV) Prefix(_ js.Value, args []js.Value) any {
+	prefix := args[0].String()
+
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		newAPI, err := r.api.Prefix(prefix)
+
+		if err != nil {
+			reject(exception.NewTrace(err))
+		} else {
+			resolve(newRemoteKvJS(newAPI))
+		}
+	}
+
+	return utils.CreatePromise(promiseFn)
+}
+
+// Root returns the KV with no prefixes
+func (r *RemoteKV) Root(_ js.Value, args []js.Value) any {
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		newAPI, err := r.api.Root()
+
+		if err != nil {
+			reject(exception.NewTrace(err))
+		} else {
+			resolve(newRemoteKvJS(newAPI))
+		}
+	}
+
+	return utils.CreatePromise(promiseFn)
+}
+
+// IsMemStore returns true if the underlying KV is memory based
+func (r *RemoteKV) IsMemStore(_ js.Value, args []js.Value) any {
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		resolve(r.api.IsMemStore())
+	}
+
+	return utils.CreatePromise(promiseFn)
+}
+
+// GetFullKey returns the key with all prefixes appended
+func (r *RemoteKV) GetFullKey(_ js.Value, args []js.Value) any {
+	key := args[0].String()
+	version := int64(args[1].Int())
+
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		fullKey := r.api.GetFullKey(key, version)
+		resolve(fullKey)
+	}
+
+	return utils.CreatePromise(promiseFn)
+}
+
+// StoreMapElement stores a versioned map element into the KV. This relies
+// on the underlying remote [KV.StoreMapElement] function to lock and control
+// updates, but it uses [versioned.Object] values.
+// All Map storage functions update the remote.
+// valueJSON is a json of a versioned.Object
+//
+// Parameters:
+//   - args[0] - the mapName string
+//   - args[1] - the elementKey string
+//   - args[2] - the [versioned.Object] JSON value, e.g.:
+//     {"Version":1,"Timestamp":"2023-05-13T00:50:03.889192694Z",
+//     "Data":"bm90IHVwZ3JhZGVk"}
+//   - args[3] - the version int
+//
+// Returns a promise with an error if any
+func (r *RemoteKV) StoreMapElement(_ js.Value, args []js.Value) any {
+	mapName := args[0].String()
+	elementKey := args[1].String()
+	val := utils.CopyBytesToGo(args[2])
+	version := int64(args[3].Int())
+
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		err := r.api.StoreMapElement(mapName, elementKey, val, version)
+		if err != nil {
+			reject(exception.NewTrace(err))
+		} else {
+			resolve()
+		}
+	}
+
+	return utils.CreatePromise(promiseFn)
+}
+
+// StoreMap saves a versioned map element into the KV. This relies
+// on the underlying remote [KV.StoreMap] function to lock and control
+// updates, but it uses [versioned.Object] values.
+// All Map storage functions update the remote.
+// valueJSON is a json of map[string]*versioned.Object
+//
+// Parameters:
+//   - args[0] - the mapName string
+//   - args[1] - the [map[string]versioned.Object] JSON value, e.g.:
+//     {"elementKey": {"Version":1,"Timestamp":"2023-05-13T00:50:03.889192694Z",
+//     "Data":"bm90IHVwZ3JhZGVk"}}
+//   - args[2] - the version int
+//
+// Returns a promise with an error if any
+func (r *RemoteKV) StoreMap(_ js.Value, args []js.Value) any {
+	mapName := args[0].String()
+	val := utils.CopyBytesToGo(args[1])
+	version := int64(args[2].Int())
+
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		err := r.api.StoreMap(mapName, val, version)
+		if err != nil {
+			reject(exception.NewTrace(err))
+		} else {
+			resolve()
+		}
+	}
+
+	return utils.CreatePromise(promiseFn)
+}
+
+// DeleteMapElement removes a versioned map element from the KV.
+//
+// Parameters:
+//   - args[0] - the mapName string
+//   - args[1] - the elementKey string
+//   - args[2] - the version int
+//
+// Returns a promise with an error if any or the json of the deleted
+// [versioned.Object], e.g.:
+//
+//	{"Version":1,"Timestamp":"2023-05-13T00:50:03.889192694Z",
+//	"Data":"bm90IHVwZ3JhZGVk"}
+func (r *RemoteKV) DeleteMapElement(_ js.Value, args []js.Value) any {
+	mapName := args[0].String()
+	elementKey := args[1].String()
+	version := int64(args[2].Int())
+
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		deleted, err := r.api.DeleteMapElement(mapName, elementKey,
+			version)
+		if err != nil {
+			reject(exception.NewTrace(err))
+		} else {
+			resolve(utils.CopyBytesToJS(deleted))
+		}
+	}
+
+	return utils.CreatePromise(promiseFn)
+}
+
+// GetMap loads a versioned map from the KV. This relies
+// on the underlying remote [KV.GetMap] function to lock and control
+// updates, but it uses [versioned.Object] values.
+//
+// Parameters:
+//   - args[0] - the mapName string
+//   - args[1] - the version int
+//
+// Returns a promise with an error if any or the
+// the [map[string]versioned.Object] JSON value, e.g.:
+//
+//	{"elementKey": {"Version":1,"Timestamp":"2023-05-13T00:50:03.889192694Z",
+//	"Data":"bm90IHVwZ3JhZGVk"}}
+func (r *RemoteKV) GetMap(_ js.Value, args []js.Value) any {
+	mapName := args[0].String()
+	version := int64(args[1].Int())
+
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		mapJSON, err := r.api.GetMap(mapName, version)
+		if err != nil {
+			reject(exception.NewTrace(err))
+		} else {
+			resolve(utils.CopyBytesToJS(mapJSON))
+		}
+	}
+
+	return utils.CreatePromise(promiseFn)
+}
+
+// GetMapElement loads a versioned map element from the KV. This relies
+// on the underlying remote [KV.GetMapElement] function to lock and control
+// updates, but it uses [versioned.Object] values.
+// Parameters:
+//   - args[0] - the mapName string
+//   - args[1] - the elementKey string
+//   - args[2] - the version int
+//
+// Returns a promise with an error if any or the json of the
+// [versioned.Object], e.g.:
+//
+//	{"Version":1,"Timestamp":"2023-05-13T00:50:03.889192694Z",
+//	"Data":"bm90IHVwZ3JhZGVk"}
+func (r *RemoteKV) GetMapElement(_ js.Value, args []js.Value) any {
+	mapName := args[0].String()
+	elementKey := args[1].String()
+	version := int64(args[2].Int())
+
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		deleted, err := r.api.GetMapElement(mapName, elementKey,
+			version)
+		if err != nil {
+			reject(exception.NewTrace(err))
+		} else {
+			resolve(utils.CopyBytesToJS(deleted))
+		}
+	}
+
+	return utils.CreatePromise(promiseFn)
+}
+
+// ListenOnRemoteKey sets up a callback listener for the object specified
+// by the key and version. It returns the current [versioned.Object] JSON
+// of the value.
+// Parameters:
+//   - args[0] - the key string
+//   - args[1] - the version int
+//   - args[2] - the [KeyChangedByRemoteCallback] javascript callback
+//
+// Returns a promise with an error if any or the json of the existing
+// [versioned.Object], e.g.:
+//
+//	{"Version":1,"Timestamp":"2023-05-13T00:50:03.889192694Z",
+//	"Data":"bm90IHVwZ3JhZGVk"}
+func (r *RemoteKV) ListenOnRemoteKey(_ js.Value, args []js.Value) any {
+	key := args[0].String()
+	version := int64(args[1].Int())
+	cb := newKeyChangedByRemoteCallback(args[2])
+
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		deleted, err := r.api.ListenOnRemoteKey(key, version, cb)
+		if err != nil {
+			reject(exception.NewTrace(err))
+		} else {
+			resolve(utils.CopyBytesToJS(deleted))
+		}
+	}
+
+	return utils.CreatePromise(promiseFn)
+}
+
+// ListenOnRemoteMap allows the caller to receive updates when
+// the map or map elements are updated. Returns a JSON of
+// map[string]versioned.Object of the current map value.
+// Parameters:
+//   - args[0] - the mapName string
+//   - args[1] - the version int
+//   - args[2] - the [MapChangedByRemoteCallback] javascript callback
+//
+// Returns a promise with an error if any or the json of the existing
+// the [map[string]versioned.Object] JSON value, e.g.:
+//
+//	{"elementKey": {"Version":1,"Timestamp":"2023-05-13T00:50:03.889192694Z",
+//	"Data":"bm90IHVwZ3JhZGVk"}}
+func (r *RemoteKV) ListenOnRemoteMap(_ js.Value, args []js.Value) any {
+	mapName := args[0].String()
+	version := int64(args[1].Int())
+	cb := newMapChangedByRemoteCallback(args[2])
+
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		deleted, err := r.api.ListenOnRemoteMap(mapName, version, cb)
+		if err != nil {
+			reject(exception.NewTrace(err))
+		} else {
+			resolve(utils.CopyBytesToJS(deleted))
+		}
+	}
+
+	return utils.CreatePromise(promiseFn)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// RemoteStore                                                                //
+////////////////////////////////////////////////////////////////////////////////
+
+// RemoteStore wraps Javascript callbacks to adhere to the
+// [bindings.RemoteStore] interface.
+type RemoteStore struct {
+	read            func(args ...any) js.Value
+	write           func(args ...any) js.Value
+	getLastModified func(args ...any) js.Value
+	getLastWrite    func(args ...any) js.Value
+	readDir         func(args ...any) js.Value
+}
+
+// newRemoteStoreCallbacks maps the functions of the Javascript object matching
+// [bindings.RemoteStore] to a RemoteStoreCallbacks.
+func newRemoteStore(arg js.Value) *RemoteStore {
+	return &RemoteStore{
+		read:            utils.WrapCB(arg, "Read"),
+		write:           utils.WrapCB(arg, "Write"),
+		getLastModified: utils.WrapCB(arg, "GetLastModified"),
+		getLastWrite:    utils.WrapCB(arg, "GetLastWrite"),
+		readDir:         utils.WrapCB(arg, "ReadDir"),
+	}
+}
+
+// Read impelements [bindings.RemoteStore.Read]
+//
+// Parameters:
+//   - path - The file path to read from (string).
+//
+// Returns:
+//   - The file data (Uint8Array).
+//   - Catches any thrown errors (of type Error) and returns it as an error.
+func (rsCB *RemoteStore) Read(path string) ([]byte, error) {
+
+	fn := func() js.Value { return rsCB.read(path) }
+	v, err := exception.RunAndCatch(fn)
+	if err != nil {
+		return nil, err
+	}
+	return utils.CopyBytesToGo(v), err
+}
+
+// Write implements [bindings.RemoteStore.Write]
+//
+// Parameters:
+//   - path - The file path to write to (string).
+//   - data - The file data to write (Uint8Array).
+//
+// Returns:
+//   - Catches any thrown errors (of type Error) and returns it as an error.
+func (rsCB *RemoteStore) Write(path string, data []byte) error {
+	fn := func() js.Value { return rsCB.write(path, utils.CopyBytesToJS(data)) }
+	_, err := exception.RunAndCatch(fn)
+	return err
+}
+
+// GetLastModified implements [bindings.RemoteStore.GetLastModified]
+//
+// Parameters:
+//   - path - The file path (string).
+//
+// Returns:
+//   - JSON of [bindings.RemoteStoreReport] (Uint8Array).
+//   - Catches any thrown errors (of type Error) and returns it as an error.
+func (rsCB *RemoteStore) GetLastModified(path string) ([]byte, error) {
+	fn := func() js.Value { return rsCB.getLastModified(path) }
+	v, err := exception.RunAndCatch(fn)
+	if err != nil {
+		return nil, err
+	}
+	return utils.CopyBytesToGo(v), err
+}
+
+// GetLastWrite implements [bindings.RemoteStore.GetLastWrite()
+//
+// Returns:
+//   - JSON of [bindings.RemoteStoreReport] (Uint8Array).
+//   - Catches any thrown errors (of type Error) and returns it as an error.
+func (rsCB *RemoteStore) GetLastWrite() ([]byte, error) {
+	fn := func() js.Value { return rsCB.getLastWrite() }
+	v, err := exception.RunAndCatch(fn)
+	if err != nil {
+		return nil, err
+	}
+	return utils.CopyBytesToGo(v), err
+}
+
+// ReadDir implements [bindings.RemoteStore.ReadDir]
+//
+// Parameters:
+//   - path - The file path (string).
+//
+// Returns:
+//   - JSON of []string (Uint8Array).
+//   - Catches any thrown errors (of type Error) and returns it as an error.
+func (rsCB *RemoteStore) ReadDir(path string) ([]byte, error) {
+	fn := func() js.Value { return rsCB.readDir(path) }
+	v, err := exception.RunAndCatch(fn)
+	if err != nil {
+		return nil, err
+	}
+	return utils.CopyBytesToGo(v), err
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Callbacks                                                                  //
+////////////////////////////////////////////////////////////////////////////////
+
+// KeyChangedByRemoteCallback wraps the passed javascript function and
+// implements [bindings.KeyChangedByRemoteCallback]
+type KeyChangedByRemoteCallback struct {
+	callback func(args ...any) js.Value
+}
+
+func (k *KeyChangedByRemoteCallback) Callback(key string, old, new []byte,
+	opType int8) {
+	k.callback(key, utils.CopyBytesToJS(old), utils.CopyBytesToJS(new),
+		opType)
+}
+
+func newKeyChangedByRemoteCallback(
+	jsFunc js.Value) *KeyChangedByRemoteCallback {
+	return &KeyChangedByRemoteCallback{
+		callback: utils.WrapCB(jsFunc, "Callback"),
+	}
+}
+
+// MapChangedByRemoteCallback wraps the passed javascript function and
+// implements [bindings.KeyChangedByRemoteCallback]
+type MapChangedByRemoteCallback struct {
+	callback func(args ...any) js.Value
+}
+
+func (m *MapChangedByRemoteCallback) Callback(mapName string,
+	editsJSON []byte) {
+	m.callback(mapName, utils.CopyBytesToJS(editsJSON))
+}
+
+func newMapChangedByRemoteCallback(
+	jsFunc js.Value) *MapChangedByRemoteCallback {
+	return &MapChangedByRemoteCallback{
+		callback: utils.WrapCB(jsFunc, "Callback"),
+	}
+}
diff --git a/wasm/collective_test.go b/wasm/collective_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..8c512adce32127a2a448882167799e6268454fea
--- /dev/null
+++ b/wasm/collective_test.go
@@ -0,0 +1,57 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+//go:build js && wasm
+
+package wasm
+
+import (
+	"reflect"
+	"testing"
+
+	"gitlab.com/elixxir/client/v4/bindings"
+)
+
+// Tests that the map representing RemoteKV returned by newRemoteKvJS contains
+// all of the methods on RemoteKV.
+func Test_newRemoteKvJS(t *testing.T) {
+	rkvType := reflect.TypeOf(&RemoteKV{})
+
+	rkv := newRemoteKvJS(&bindings.RemoteKV{})
+	if len(rkv) != rkvType.NumMethod() {
+		t.Errorf("RemoteKV JS object does not have all methods."+
+			"\nexpected: %d\nreceived: %d", rkvType.NumMethod(), len(rkv))
+	}
+
+	for i := 0; i < rkvType.NumMethod(); i++ {
+		method := rkvType.Method(i)
+
+		if _, exists := rkv[method.Name]; !exists {
+			t.Errorf("Method %s does not exist.", method.Name)
+		}
+	}
+}
+
+// Tests that RemoteKV has all the methods that [bindings.RemoteKV] has.
+func Test_RemoteKVMethods(t *testing.T) {
+	rkvType := reflect.TypeOf(&RemoteKV{})
+	binRkvType := reflect.TypeOf(&bindings.RemoteKV{})
+
+	if binRkvType.NumMethod() != rkvType.NumMethod() {
+		t.Errorf("WASM RemoteKV object does not have all methods from "+
+			"bindings.\nexpected: %d\nreceived: %d",
+			binRkvType.NumMethod(), rkvType.NumMethod())
+	}
+
+	for i := 0; i < binRkvType.NumMethod(); i++ {
+		method := binRkvType.Method(i)
+
+		if _, exists := rkvType.MethodByName(method.Name); !exists {
+			t.Errorf("Method %s does not exist.", method.Name)
+		}
+	}
+}
diff --git a/wasm/connect.go b/wasm/connect.go
index bfb95a6d882f808277a856dfbda9937c3bb3d687..b6b78e6940f1314d6d189e737d016db89112110b 100644
--- a/wasm/connect.go
+++ b/wasm/connect.go
@@ -11,7 +11,8 @@ package wasm
 
 import (
 	"gitlab.com/elixxir/client/v4/bindings"
-	"gitlab.com/elixxir/xxdk-wasm/utils"
+	"gitlab.com/elixxir/wasm-utils/exception"
+	"gitlab.com/elixxir/wasm-utils/utils"
 	"syscall/js"
 )
 
@@ -68,7 +69,7 @@ func (c *Cmix) Connect(_ js.Value, args []js.Value) any {
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
 		api, err := c.api.Connect(e2eID, recipientContact, e2eParamsJSON)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(newConnectJS(api))
 		}
@@ -95,7 +96,7 @@ func (c *Connection) SendE2E(_ js.Value, args []js.Value) any {
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
 		sendReport, err := c.api.SendE2E(e2eID, payload)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(utils.CopyBytesToJS(sendReport))
 		}
@@ -107,11 +108,11 @@ func (c *Connection) SendE2E(_ js.Value, args []js.Value) any {
 // Close deletes this [Connection]'s [partner.Manager] and releases resources.
 //
 // Returns:
-//   - Throws a TypeError if closing fails.
+//   - Throws an error if closing fails.
 func (c *Connection) Close(js.Value, []js.Value) any {
 	err := c.api.Close()
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -153,12 +154,12 @@ func (l *listener) Name() string { return l.name().String() }
 //     [bindings.Listener] interface.
 //
 // Returns:
-//   - Throws a TypeError is registering the listener fails.
+//   - Throws an error is registering the listener fails.
 func (c *Connection) RegisterListener(_ js.Value, args []js.Value) any {
 	err := c.api.RegisterListener(args[0].Int(),
 		&listener{utils.WrapCB(args[1], "Hear"), utils.WrapCB(args[1], "Name")})
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
diff --git a/wasm/delivery.go b/wasm/delivery.go
index 327f15223426bf3ac19c4d001806b8e69abdfb9e..67bc677bc300ca0d5108b33dd5573f1f0d80c4ed 100644
--- a/wasm/delivery.go
+++ b/wasm/delivery.go
@@ -11,7 +11,8 @@ package wasm
 
 import (
 	"gitlab.com/elixxir/client/v4/bindings"
-	"gitlab.com/elixxir/xxdk-wasm/utils"
+	"gitlab.com/elixxir/wasm-utils/exception"
+	"gitlab.com/elixxir/wasm-utils/utils"
 	"syscall/js"
 )
 
@@ -79,7 +80,7 @@ func (mdc *messageDeliveryCallback) EventCallback(
 //     occurs, in milliseconds (int).
 //
 // Returns:
-//   - Throws a TypeError if the parameters are invalid or getting round results
+//   - Throws an error if the parameters are invalid or getting round results
 //     fails.
 func (c *Cmix) WaitForRoundResult(_ js.Value, args []js.Value) any {
 	roundList := utils.CopyBytesToGo(args[0])
@@ -87,7 +88,7 @@ func (c *Cmix) WaitForRoundResult(_ js.Value, args []js.Value) any {
 
 	err := c.api.WaitForRoundResult(roundList, mdc, args[2].Int())
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
diff --git a/wasm/dm.go b/wasm/dm.go
index 6b6ea90ab546580dbf0d6e312ab4211df4ff335f..7295c8d887c1cca03ee00ec0818a44ea622361de 100644
--- a/wasm/dm.go
+++ b/wasm/dm.go
@@ -20,8 +20,9 @@ import (
 	"gitlab.com/elixxir/client/v4/bindings"
 	"gitlab.com/elixxir/client/v4/dm"
 	"gitlab.com/elixxir/crypto/codename"
+	"gitlab.com/elixxir/wasm-utils/exception"
+	"gitlab.com/elixxir/wasm-utils/utils"
 	indexDB "gitlab.com/elixxir/xxdk-wasm/indexedDb/worker/dm"
-	"gitlab.com/elixxir/xxdk-wasm/utils"
 )
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -53,12 +54,20 @@ func newDMClientJS(api *bindings.DMClient) map[string]any {
 		"GetBlockedSenders":     js.FuncOf(cm.GetBlockedSenders),
 		"GetDatabaseName":       js.FuncOf(cm.GetDatabaseName),
 
+		// Share URL
+		"GetShareURL": js.FuncOf(cm.GetShareURL),
+
 		// DM Sending Methods and Reports
 		"SendText":     js.FuncOf(cm.SendText),
 		"SendReply":    js.FuncOf(cm.SendReply),
 		"SendReaction": js.FuncOf(cm.SendReaction),
 		"SendInvite":   js.FuncOf(cm.SendInvite),
+		"SendSilent":   js.FuncOf(cm.SendSilent),
 		"Send":         js.FuncOf(cm.Send),
+
+		// User Mute/Unmute
+		"BlockSender":   js.FuncOf(cm.BlockSender),
+		"UnblockSender": js.FuncOf(cm.UnblockSender),
 	}
 
 	return dmClientMap
@@ -83,14 +92,14 @@ func newDMClientJS(api *bindings.DMClient) map[string]any {
 //
 // Returns:
 //   - Javascript representation of the [DMClient] object.
-//   - Throws a TypeError if creating the manager fails.
+//   - Throws an error if creating the manager fails.
 func NewDMClient(_ js.Value, args []js.Value) any {
 	privateIdentity := utils.CopyBytesToGo(args[1])
 	em := newDMReceiverBuilder(args[2])
 
 	cm, err := bindings.NewDMClient(args[0].Int(), privateIdentity, em)
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -126,7 +135,7 @@ func NewDMClient(_ js.Value, args []js.Value) any {
 // Returns:
 //   - Resolves to a Javascript representation of the [DMClient] object.
 //   - Rejected with an error if loading indexedDbWorker or the manager fails.
-//   - Throws a TypeError if the cipher ID does not correspond to a cipher.
+//   - Throws an error if the cipher ID does not correspond to a cipher.
 func NewDMClientWithIndexedDb(_ js.Value, args []js.Value) any {
 	cmixID := args[0].Int()
 	wasmJsPath := args[1].String()
@@ -136,7 +145,7 @@ func NewDMClientWithIndexedDb(_ js.Value, args []js.Value) any {
 
 	cipher, err := bindings.GetDMDbCipherTrackerFromID(cipherID)
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 	}
 
 	return newDMClientWithIndexedDb(
@@ -194,19 +203,19 @@ func newDMClientWithIndexedDb(cmixID int, wasmJsPath string,
 
 		pi, err := codename.UnmarshalPrivateIdentity(privateIdentity)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		}
 		dmPath := base64.RawStdEncoding.EncodeToString(pi.PubKey[:])
 		model, err := indexDB.NewWASMEventModel(
 			dmPath, wasmJsPath, cipher, messageReceivedCB)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		}
 
 		cm, err := bindings.NewDMClientWithGoEventModel(
 			cmixID, privateIdentity, model)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(newDMClientJS(cm))
 		}
@@ -257,7 +266,7 @@ func (dmc *DMClient) GetIdentity(js.Value, []js.Value) any {
 func (dmc *DMClient) ExportPrivateIdentity(_ js.Value, args []js.Value) any {
 	i, err := dmc.api.ExportPrivateIdentity(args[0].String())
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -273,7 +282,7 @@ func (dmc *DMClient) ExportPrivateIdentity(_ js.Value, args []js.Value) any {
 func (dmc *DMClient) GetNickname(_ js.Value, _ []js.Value) any {
 	nickname, err := dmc.api.GetNickname()
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -357,7 +366,7 @@ func (dmc *DMClient) SendText(_ js.Value, args []js.Value) any {
 		sendReport, err := dmc.api.SendText(partnerPubKeyBytes, partnerToken,
 			message, leaseTimeMS, cmixParamsJSON)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(utils.CopyBytesToJS(sendReport))
 		}
@@ -415,7 +424,7 @@ func (dmc *DMClient) SendReply(_ js.Value, args []js.Value) any {
 		sendReport, err := dmc.api.SendReply(partnerPubKeyBytes, partnerToken,
 			replyMessage, replyToBytes, leaseTimeMS, cmixParamsJSON)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(utils.CopyBytesToJS(sendReport))
 		}
@@ -463,7 +472,43 @@ func (dmc *DMClient) SendReaction(_ js.Value, args []js.Value) any {
 		sendReport, err := dmc.api.SendReaction(partnerPubKeyBytes,
 			partnerToken, reaction, reactToBytes, cmixParamsJSON)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
+		} else {
+			resolve(utils.CopyBytesToJS(sendReport))
+		}
+	}
+
+	return utils.CreatePromise(promiseFn)
+}
+
+// SendSilent is used to send to a channel a message with no notifications.
+// Its primary purpose is to communicate new nicknames without calling [Send].
+//
+// It takes no payload intentionally as the message should be very lightweight.
+//
+// Parameters:
+//   - args[0] - The bytes of the public key of the partner's ED25519
+//     signing key (Uint8Array).
+//   - args[1] - The token used to derive the reception ID for the partner
+//     (int).
+//   - args[2] - JSON of [xxdk.CMIXParams]. If left empty
+//     [bindings.GetDefaultCMixParams] will be used internally (Uint8Array).
+//
+// Returns a promise:
+//   - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array).
+//   - Rejected with an error if sending fails.
+func (dmc *DMClient) SendSilent(_ js.Value, args []js.Value) any {
+	var (
+		partnerPubKeyBytes = utils.CopyBytesToGo(args[0])
+		partnerToken       = int32(args[1].Int())
+		cmixParamsJSON     = utils.CopyBytesToGo(args[2])
+	)
+
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		sendReport, err := dmc.api.SendSilent(
+			partnerPubKeyBytes, partnerToken, cmixParamsJSON)
+		if err != nil {
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(utils.CopyBytesToJS(sendReport))
 		}
@@ -511,7 +556,7 @@ func (dmc *DMClient) SendInvite(_ js.Value, args []js.Value) any {
 			partnerPubKeyBytes, partnerToken, marshalledInviteToId, msg, host,
 			maxUses, cmixParamsJSON)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(utils.CopyBytesToJS(sendReport))
 		}
@@ -562,7 +607,7 @@ func (dmc *DMClient) Send(_ js.Value, args []js.Value) any {
 		sendReport, err := dmc.api.Send(partnerPubKeyBytes, partnerToken,
 			messageType, plaintext, leaseTimeMS, cmixParamsJSON)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(utils.CopyBytesToJS(sendReport))
 		}
@@ -581,6 +626,30 @@ func (dmc *DMClient) GetDatabaseName(js.Value, []js.Value) any {
 		"_speakeasy_dm"
 }
 
+// BlockSender blocks the provided sender public key from sending DMs
+//
+// Parameters:
+//   - args[0] - [ed25519.PublicKey] (Uint8Array)
+//
+// Returns nothing
+func (dmc *DMClient) BlockSender(_ js.Value, args []js.Value) any {
+	senderKey := utils.CopyBytesToGo(args[0])
+	dmc.api.BlockSender(senderKey)
+	return nil
+}
+
+// UnblockSender unblocks the provided sender public key to allow sending DMs
+//
+// Parameters:
+//   - args[0] - [ed25519.PublicKey] (Uint8Array)
+//
+// Returns nothing
+func (dmc *DMClient) UnblockSender(_ js.Value, args []js.Value) any {
+	senderKey := utils.CopyBytesToGo(args[0])
+	dmc.api.UnblockSender(senderKey)
+	return nil
+}
+
 ////////////////////////////////////////////////////////////////////////////////
 // DM Share URL                                                          //
 ////////////////////////////////////////////////////////////////////////////////
@@ -625,7 +694,7 @@ func (dmc *DMClient) GetShareURL(_ js.Value, args []js.Value) any {
 	host := args[0].String()
 	urlReport, err := dmc.api.GetShareURL(host)
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -645,7 +714,7 @@ func DecodeDMShareURL(_ js.Value, args []js.Value) any {
 	url := args[0].String()
 	report, err := bindings.DecodeDMShareURL(url)
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -676,7 +745,7 @@ func (cmrCB *dmReceptionCallback) Callback(
 	receivedChannelMessageReport []byte, err error) int {
 	uuid := cmrCB.callback(
 		utils.CopyBytesToJS(receivedChannelMessageReport),
-		utils.JsTrace(err))
+		exception.NewTrace(err))
 
 	return uuid.Int()
 }
@@ -1008,7 +1077,7 @@ func newDMDbCipherJS(api *bindings.DMDbCipher) map[string]any {
 //
 // Returns:
 //   - JavaScript representation of the [DMDbCipher] object.
-//   - Throws a TypeError if creating the cipher fails.
+//   - Throws an error if creating the cipher fails.
 func NewDMsDatabaseCipher(_ js.Value, args []js.Value) any {
 	cmixId := args[0].Int()
 	password := utils.CopyBytesToGo(args[1])
@@ -1017,7 +1086,7 @@ func NewDMsDatabaseCipher(_ js.Value, args []js.Value) any {
 	cipher, err := bindings.NewDMsDatabaseCipher(
 		cmixId, password, plaintTextBlockSize)
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -1043,11 +1112,11 @@ func (c *DMDbCipher) GetID(js.Value, []js.Value) any {
 //
 // Returns:
 //   - The ciphertext of the plaintext passed in (Uint8Array).
-//   - Throws a TypeError if it fails to encrypt the plaintext.
+//   - Throws an error if it fails to encrypt the plaintext.
 func (c *DMDbCipher) Encrypt(_ js.Value, args []js.Value) any {
 	ciphertext, err := c.api.Encrypt(utils.CopyBytesToGo(args[0]))
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -1064,11 +1133,11 @@ func (c *DMDbCipher) Encrypt(_ js.Value, args []js.Value) any {
 //
 // Returns:
 //   - The plaintext of the ciphertext passed in (Uint8Array).
-//   - Throws a TypeError if it fails to encrypt the plaintext.
+//   - Throws an error if it fails to encrypt the plaintext.
 func (c *DMDbCipher) Decrypt(_ js.Value, args []js.Value) any {
 	plaintext, err := c.api.Decrypt(utils.CopyBytesToGo(args[0]))
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -1079,11 +1148,11 @@ func (c *DMDbCipher) Decrypt(_ js.Value, args []js.Value) any {
 //
 // Returns:
 //   - JSON of the cipher (Uint8Array).
-//   - Throws a TypeError if marshalling fails.
+//   - Throws an error if marshalling fails.
 func (c *DMDbCipher) MarshalJSON(js.Value, []js.Value) any {
 	data, err := c.api.MarshalJSON()
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -1101,11 +1170,11 @@ func (c *DMDbCipher) MarshalJSON(js.Value, []js.Value) any {
 //
 // Returns:
 //   - JSON of the cipher (Uint8Array).
-//   - Throws a TypeError if marshalling fails.
+//   - Throws an error if marshalling fails.
 func (c *DMDbCipher) UnmarshalJSON(_ js.Value, args []js.Value) any {
 	err := c.api.UnmarshalJSON(utils.CopyBytesToGo(args[0]))
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 	return nil
diff --git a/wasm/dummy.go b/wasm/dummy.go
index 82b9128ce52cb3521056da6a77358bb44a4a5e5c..9efbc687eee46005192148f75f3164b81421a531 100644
--- a/wasm/dummy.go
+++ b/wasm/dummy.go
@@ -11,7 +11,7 @@ package wasm
 
 import (
 	"gitlab.com/elixxir/client/v4/bindings"
-	"gitlab.com/elixxir/xxdk-wasm/utils"
+	"gitlab.com/elixxir/wasm-utils/exception"
 	"syscall/js"
 )
 
@@ -53,12 +53,12 @@ func newDummyTrafficJS(newDT *bindings.DummyTraffic) map[string]any {
 //
 // Returns:
 //   - Javascript representation of the DummyTraffic object.
-//   - Throws a TypeError if creating the manager fails.
+//   - Throws an error if creating the manager fails.
 func NewDummyTrafficManager(_ js.Value, args []js.Value) any {
 	dt, err := bindings.NewDummyTrafficManager(
 		args[0].Int(), args[1].Int(), args[2].Int(), args[3].Int())
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -75,12 +75,12 @@ func NewDummyTrafficManager(_ js.Value, args []js.Value) any {
 // thread will then be prevented from beginning another round of sending.
 //
 // Returns:
-//   - Throws a TypeError if it fails to send a pause signal to the sending
+//   - Throws an error if it fails to send a pause signal to the sending
 //     thread.
 func (dt *DummyTraffic) Pause(js.Value, []js.Value) any {
 	err := dt.api.Pause()
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -98,12 +98,12 @@ func (dt *DummyTraffic) Pause(js.Value, []js.Value) any {
 // sending interval after a call to Start.
 //
 // Returns:
-//   - Throws a TypeError if it fails to send a start signal to the sending
+//   - Throws an error if it fails to send a start signal to the sending
 //     thread.
 func (dt *DummyTraffic) Start(js.Value, []js.Value) any {
 	err := dt.api.Start()
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
diff --git a/wasm/e2e.go b/wasm/e2e.go
index 8d8f4ce15fa0c47f71c3bbbeccdf9484a4ccde39..dcb4853d23745f0d85a879284a6b2c918a1d5f39 100644
--- a/wasm/e2e.go
+++ b/wasm/e2e.go
@@ -11,7 +11,8 @@ package wasm
 
 import (
 	"gitlab.com/elixxir/client/v4/bindings"
-	"gitlab.com/elixxir/xxdk-wasm/utils"
+	"gitlab.com/elixxir/wasm-utils/exception"
+	"gitlab.com/elixxir/wasm-utils/utils"
 	"syscall/js"
 )
 
@@ -90,7 +91,7 @@ func (e *E2e) GetID(js.Value, []js.Value) any {
 //
 // Returns:
 //   - Javascript representation of the [E2e] object.
-//   - Throws a TypeError if logging in fails.
+//   - Throws an error if logging in fails.
 func Login(_ js.Value, args []js.Value) any {
 	callbacks := newAuthCallbacks(args[1])
 	identity := utils.CopyBytesToGo(args[2])
@@ -99,7 +100,7 @@ func Login(_ js.Value, args []js.Value) any {
 	newE2E, err := bindings.Login(
 		args[0].Int(), callbacks, identity, e2eParamsJSON)
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -120,7 +121,7 @@ func Login(_ js.Value, args []js.Value) any {
 //
 // Returns:
 //   - Javascript representation of the [E2e] object.
-//   - Throws a TypeError if logging in fails.
+//   - Throws an error if logging in fails.
 func LoginEphemeral(_ js.Value, args []js.Value) any {
 	callbacks := newAuthCallbacks(args[1])
 	identity := utils.CopyBytesToGo(args[2])
@@ -129,7 +130,7 @@ func LoginEphemeral(_ js.Value, args []js.Value) any {
 	newE2E, err := bindings.LoginEphemeral(
 		args[0].Int(), callbacks, identity, e2eParamsJSON)
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -167,11 +168,11 @@ func (e *E2e) GetUdCertFromNdf(js.Value, []js.Value) any {
 //
 // Returns
 //   - Marshalled bytes of [contact.Contact] (Uint8Array).
-//   - Throws a TypeError if the contact file cannot be loaded.
+//   - Throws an error if the contact file cannot be loaded.
 func (e *E2e) GetUdContactFromNdf(js.Value, []js.Value) any {
 	b, err := e.api.GetUdContactFromNdf()
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
diff --git a/wasm/e2eAuth.go b/wasm/e2eAuth.go
index eb69ef75cd17d26b22715d0eba08bf6b5f0fc02a..351ed123db2b69c315ceb61bcd5885146149e3dd 100644
--- a/wasm/e2eAuth.go
+++ b/wasm/e2eAuth.go
@@ -10,7 +10,8 @@
 package wasm
 
 import (
-	"gitlab.com/elixxir/xxdk-wasm/utils"
+	"gitlab.com/elixxir/wasm-utils/exception"
+	"gitlab.com/elixxir/wasm-utils/utils"
 	"syscall/js"
 )
 
@@ -46,7 +47,7 @@ func (e *E2e) Request(_ js.Value, args []js.Value) any {
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
 		rid, err := e.api.Request(partnerContact, factsListJson)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(rid)
 		}
@@ -83,7 +84,7 @@ func (e *E2e) Confirm(_ js.Value, args []js.Value) any {
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
 		rid, err := e.api.Confirm(partnerContact)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(rid)
 		}
@@ -118,7 +119,7 @@ func (e *E2e) Reset(_ js.Value, args []js.Value) any {
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
 		rid, err := e.api.Reset(partnerContact)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(rid)
 		}
@@ -147,7 +148,7 @@ func (e *E2e) ReplayConfirm(_ js.Value, args []js.Value) any {
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
 		rid, err := e.api.ReplayConfirm(partnerContact)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(rid)
 		}
@@ -174,7 +175,7 @@ func (e *E2e) DeleteRequest(_ js.Value, args []js.Value) any {
 	partnerContact := utils.CopyBytesToGo(args[0])
 	err := e.api.DeleteRequest(partnerContact)
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -188,7 +189,7 @@ func (e *E2e) DeleteRequest(_ js.Value, args []js.Value) any {
 func (e *E2e) DeleteAllRequests(js.Value, []js.Value) any {
 	err := e.api.DeleteAllRequests()
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -202,7 +203,7 @@ func (e *E2e) DeleteAllRequests(js.Value, []js.Value) any {
 func (e *E2e) DeleteSentRequests(js.Value, []js.Value) any {
 	err := e.api.DeleteSentRequests()
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -216,7 +217,7 @@ func (e *E2e) DeleteSentRequests(js.Value, []js.Value) any {
 func (e *E2e) DeleteReceiveRequests(js.Value, []js.Value) any {
 	err := e.api.DeleteReceiveRequests()
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -235,7 +236,7 @@ func (e *E2e) GetReceivedRequest(_ js.Value, args []js.Value) any {
 	partnerContact := utils.CopyBytesToGo(args[0])
 	c, err := e.api.GetReceivedRequest(partnerContact)
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -260,7 +261,7 @@ func (e *E2e) VerifyOwnership(_ js.Value, args []js.Value) any {
 	isValid, err := e.api.VerifyOwnership(
 		receivedContact, verifiedContact, args[2].Int())
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -282,7 +283,7 @@ func (e *E2e) AddPartnerCallback(_ js.Value, args []js.Value) any {
 	callbacks := newAuthCallbacks(args[1])
 	err := e.api.AddPartnerCallback(partnerID, callbacks)
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -301,7 +302,7 @@ func (e *E2e) DeletePartnerCallback(_ js.Value, args []js.Value) any {
 	partnerID := utils.CopyBytesToGo(args[0])
 	err := e.api.DeletePartnerCallback(partnerID)
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
diff --git a/wasm/e2eHandler.go b/wasm/e2eHandler.go
index bff44e6aa48567e5c6afae5dca439b6fa93a6fa1..e483fc7e46cf9e0624881aefaf80a9c5ffcae2af 100644
--- a/wasm/e2eHandler.go
+++ b/wasm/e2eHandler.go
@@ -10,8 +10,10 @@
 package wasm
 
 import (
-	"gitlab.com/elixxir/xxdk-wasm/utils"
 	"syscall/js"
+
+	"gitlab.com/elixxir/wasm-utils/exception"
+	"gitlab.com/elixxir/wasm-utils/utils"
 )
 
 // GetReceptionID returns the marshalled default IDs.
@@ -32,7 +34,7 @@ func (e *E2e) GetReceptionID(js.Value, []js.Value) any {
 func (e *E2e) DeleteContact(_ js.Value, args []js.Value) any {
 	err := e.api.DeleteContact(utils.CopyBytesToGo(args[0]))
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 	return nil
@@ -47,7 +49,7 @@ func (e *E2e) DeleteContact(_ js.Value, args []js.Value) any {
 func (e *E2e) GetAllPartnerIDs(js.Value, []js.Value) any {
 	partnerIDs, err := e.api.GetAllPartnerIDs()
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 	return utils.CopyBytesToJS(partnerIDs)
@@ -100,7 +102,7 @@ func (e *E2e) FirstPartitionSize(js.Value, []js.Value) any {
 func (e *E2e) GetHistoricalDHPrivkey(js.Value, []js.Value) any {
 	privKey, err := e.api.GetHistoricalDHPrivkey()
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 	return utils.CopyBytesToJS(privKey)
@@ -115,7 +117,7 @@ func (e *E2e) GetHistoricalDHPrivkey(js.Value, []js.Value) any {
 func (e *E2e) GetHistoricalDHPubkey(js.Value, []js.Value) any {
 	pubKey, err := e.api.GetHistoricalDHPubkey()
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 	return utils.CopyBytesToJS(pubKey)
@@ -133,7 +135,7 @@ func (e *E2e) GetHistoricalDHPubkey(js.Value, []js.Value) any {
 func (e *E2e) HasAuthenticatedChannel(_ js.Value, args []js.Value) any {
 	exists, err := e.api.HasAuthenticatedChannel(utils.CopyBytesToGo(args[0]))
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 	return exists
@@ -149,7 +151,7 @@ func (e *E2e) HasAuthenticatedChannel(_ js.Value, args []js.Value) any {
 func (e *E2e) RemoveService(_ js.Value, args []js.Value) any {
 	err := e.api.RemoveService(args[0].String())
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -178,7 +180,7 @@ func (e *E2e) SendE2E(_ js.Value, args []js.Value) any {
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
 		sendReport, err := e.api.SendE2E(mt, recipientId, payload, e2eParams)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(utils.CopyBytesToJS(sendReport))
 		}
@@ -199,13 +201,18 @@ type processor struct {
 //
 // Parameters:
 //   - message - Returns the message contents (Uint8Array).
+//   - tags - The tags on the message (Uint8Array).
+//   - metadata - Other arbitrary metadata (Uint8Array).
 //   - receptionId - Returns the marshalled bytes of the sender's [id.ID]
 //     (Uint8Array).
 //   - ephemeralId - Returns the ephemeral ID of the sender (int).
 //   - roundId - Returns the ID of the round sent on (int).
-func (p *processor) Process(
-	message, receptionId []byte, ephemeralId, roundId int64) {
-	p.process(utils.CopyBytesToJS(message), utils.CopyBytesToJS(receptionId),
+func (p *processor) Process(message, tags, metadata, receptionId []byte,
+	ephemeralId, roundId int64) {
+	p.process(utils.CopyBytesToJS(message),
+		utils.CopyBytesToJS(tags),
+		utils.CopyBytesToJS(metadata),
+		utils.CopyBytesToJS(receptionId),
 		ephemeralId, roundId)
 }
 
@@ -232,11 +239,13 @@ func (p *processor) String() string {
 //   - Throws TypeError if registering the service fails.
 func (e *E2e) AddService(_ js.Value, args []js.Value) any {
 	p := &processor{
-		utils.WrapCB(args[1], "Process"), utils.WrapCB(args[1], "String")}
+		utils.WrapCB(args[1], "Process"),
+		utils.WrapCB(args[1], "String"),
+	}
 
 	err := e.api.AddService(args[0].String(), p)
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -261,7 +270,7 @@ func (e *E2e) RegisterListener(_ js.Value, args []js.Value) any {
 
 	err := e.api.RegisterListener(recipientId, args[1].Int(), l)
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
diff --git a/wasm/emoji.go b/wasm/emoji.go
index fbde2b5717a4c745fc7f7339d96f157703138cad..7be8ce8e45461c4e2ffb1a0f1f215cff313421a4 100644
--- a/wasm/emoji.go
+++ b/wasm/emoji.go
@@ -13,7 +13,8 @@ import (
 	"syscall/js"
 
 	"gitlab.com/elixxir/client/v4/bindings"
-	"gitlab.com/elixxir/xxdk-wasm/utils"
+	"gitlab.com/elixxir/wasm-utils/exception"
+	"gitlab.com/elixxir/wasm-utils/utils"
 )
 
 // SupportedEmojis returns a list of emojis that are supported by the backend.
@@ -21,7 +22,7 @@ import (
 //
 // Returns:
 //   - JSON of an array of emoji.Emoji (Uint8Array).
-//   - Throws a TypeError if marshalling the JSON fails.
+//   - Throws an error if marshalling the JSON fails.
 //
 // Example JSON:
 //
@@ -56,7 +57,7 @@ import (
 func SupportedEmojis(js.Value, []js.Value) any {
 	data, err := bindings.SupportedEmojis()
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -68,7 +69,7 @@ func SupportedEmojis(js.Value, []js.Value) any {
 //
 // Returns:
 //   - JSON of a map of emoji.Emoji (Uint8Array).
-//   - Throws a TypeError if marshalling the JSON fails.
+//   - Throws an error if marshalling the JSON fails.
 //
 // Example JSON:
 //
@@ -101,7 +102,7 @@ func SupportedEmojis(js.Value, []js.Value) any {
 func SupportedEmojisMap(js.Value, []js.Value) any {
 	data, err := bindings.SupportedEmojisMap()
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -121,7 +122,7 @@ func SupportedEmojisMap(js.Value, []js.Value) any {
 func ValidateReaction(_ js.Value, args []js.Value) any {
 	err := bindings.ValidateReaction(args[0].String())
 	if err != nil {
-		return utils.JsError(err)
+		return exception.NewError(err)
 	}
 
 	return nil
diff --git a/wasm/errors.go b/wasm/errors.go
index 9299c60e0d9c02e81a1ea54755c26d7be62b87bf..2b2bab8c2ce6ebc762bad96c8e317de313207e6f 100644
--- a/wasm/errors.go
+++ b/wasm/errors.go
@@ -11,7 +11,7 @@ package wasm
 
 import (
 	"gitlab.com/elixxir/client/v4/bindings"
-	"gitlab.com/elixxir/xxdk-wasm/utils"
+	"gitlab.com/elixxir/wasm-utils/exception"
 	"syscall/js"
 )
 
@@ -49,11 +49,11 @@ func CreateUserFriendlyErrorMessage(_ js.Value, args []js.Value) any {
 //	}
 //
 // Returns:
-//   - Throws a TypeError if the JSON cannot be unmarshalled.
+//   - Throws an error if the JSON cannot be unmarshalled.
 func UpdateCommonErrors(_ js.Value, args []js.Value) any {
 	err := bindings.UpdateCommonErrors(args[0].String())
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
diff --git a/wasm/fileTransfer.go b/wasm/fileTransfer.go
index 0083a98a78b2544f90b96623b88157b9dd70acb2..033c410c1231685db46a9c88ea999d1c2be727f3 100644
--- a/wasm/fileTransfer.go
+++ b/wasm/fileTransfer.go
@@ -11,7 +11,8 @@ package wasm
 
 import (
 	"gitlab.com/elixxir/client/v4/bindings"
-	"gitlab.com/elixxir/xxdk-wasm/utils"
+	"gitlab.com/elixxir/wasm-utils/exception"
+	"gitlab.com/elixxir/wasm-utils/utils"
 	"syscall/js"
 )
 
@@ -84,7 +85,7 @@ type fileTransferSentProgressCallback struct {
 func (spc *fileTransferSentProgressCallback) Callback(
 	payload []byte, t *bindings.FilePartTracker, err error) {
 	spc.callback(utils.CopyBytesToJS(payload), newFilePartTrackerJS(t),
-		utils.JsTrace(err))
+		exception.NewTrace(err))
 }
 
 // fileTransferReceiveProgressCallback wraps Javascript callbacks to adhere to
@@ -105,7 +106,7 @@ type fileTransferReceiveProgressCallback struct {
 func (rpc *fileTransferReceiveProgressCallback) Callback(
 	payload []byte, t *bindings.FilePartTracker, err error) {
 	rpc.callback(utils.CopyBytesToJS(payload), newFilePartTrackerJS(t),
-		utils.JsTrace(err))
+		exception.NewTrace(err))
 }
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -124,7 +125,7 @@ func (rpc *fileTransferReceiveProgressCallback) Callback(
 //
 // Returns:
 //   - Javascript representation of the [FileTransfer] object.
-//   - Throws a TypeError initialising the file transfer manager fails.
+//   - Throws an error initialising the file transfer manager fails.
 func InitFileTransfer(_ js.Value, args []js.Value) any {
 	rfc := &receiveFileCallback{utils.WrapCB(args[1], "Callback")}
 	e2eFileTransferParamsJson := utils.CopyBytesToGo(args[2])
@@ -133,7 +134,7 @@ func InitFileTransfer(_ js.Value, args []js.Value) any {
 	api, err := bindings.InitFileTransfer(
 		args[0].Int(), rfc, e2eFileTransferParamsJson, fileTransferParamsJson)
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -163,7 +164,7 @@ func (f *FileTransfer) Send(_ js.Value, args []js.Value) any {
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
 		ftID, err := f.api.Send(payload, recipientID, retry, spc, args[4].Int())
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(utils.CopyBytesToJS(ftID))
 		}
@@ -185,12 +186,12 @@ func (f *FileTransfer) Send(_ js.Value, args []js.Value) any {
 //
 // Returns:
 //   - File contents (Uint8Array).
-//   - Throws a TypeError the file transfer is incomplete or Receive has already
+//   - Throws an error the file transfer is incomplete or Receive has already
 //     been called.
 func (f *FileTransfer) Receive(_ js.Value, args []js.Value) any {
 	file, err := f.api.Receive(utils.CopyBytesToGo(args[0]))
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -208,11 +209,11 @@ func (f *FileTransfer) Receive(_ js.Value, args []js.Value) any {
 //   - args[0] - File transfer [fileTransfer.TransferID] (Uint8Array).
 //
 // Returns:
-//   - Throws a TypeError if the file transfer is incomplete.
+//   - Throws an error if the file transfer is incomplete.
 func (f *FileTransfer) CloseSend(_ js.Value, args []js.Value) any {
 	err := f.api.CloseSend(utils.CopyBytesToGo(args[0]))
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -237,7 +238,7 @@ func (f *FileTransfer) CloseSend(_ js.Value, args []js.Value) any {
 //     triggering (int).
 //
 // Returns:
-//   - Throws a TypeError if registering the callback fails.
+//   - Throws an error if registering the callback fails.
 func (f *FileTransfer) RegisterSentProgressCallback(
 	_ js.Value, args []js.Value) any {
 	tidBytes := utils.CopyBytesToGo(args[0])
@@ -245,7 +246,7 @@ func (f *FileTransfer) RegisterSentProgressCallback(
 
 	err := f.api.RegisterSentProgressCallback(tidBytes, spc, args[2].Int())
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -265,7 +266,7 @@ func (f *FileTransfer) RegisterSentProgressCallback(
 //     triggering (int).
 //
 // Returns:
-//   - Throws a TypeError if registering the callback fails.
+//   - Throws an error if registering the callback fails.
 func (f *FileTransfer) RegisterReceivedProgressCallback(
 	_ js.Value, args []js.Value) any {
 	tidBytes := utils.CopyBytesToGo(args[0])
@@ -274,7 +275,7 @@ func (f *FileTransfer) RegisterReceivedProgressCallback(
 	err := f.api.RegisterReceivedProgressCallback(
 		tidBytes, rpc, args[2].Int())
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
diff --git a/wasm/follow.go b/wasm/follow.go
index 7f573609e037aa3a7c78f286b3cb3e51de016bc5..a0fdeb73b4db5926ddba8f604bf7583fe07c7ac8 100644
--- a/wasm/follow.go
+++ b/wasm/follow.go
@@ -10,9 +10,11 @@
 package wasm
 
 import (
-	"gitlab.com/elixxir/xxdk-wasm/storage"
-	"gitlab.com/elixxir/xxdk-wasm/utils"
 	"syscall/js"
+
+	"gitlab.com/elixxir/wasm-utils/exception"
+	"gitlab.com/elixxir/wasm-utils/utils"
+	"gitlab.com/elixxir/xxdk-wasm/storage"
 )
 
 // StartNetworkFollower kicks off the tracking of the network. It starts long-
@@ -53,11 +55,11 @@ import (
 //   - args[0] - Timeout when stopping threads in milliseconds (int).
 //
 // Returns:
-//   - Throws a TypeError if starting the network follower fails.
+//   - Throws an error if starting the network follower fails.
 func (c *Cmix) StartNetworkFollower(_ js.Value, args []js.Value) any {
 	err := c.api.StartNetworkFollower(args[0].Int())
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -71,12 +73,12 @@ func (c *Cmix) StartNetworkFollower(_ js.Value, args []js.Value) any {
 // most likely be in an unrecoverable state and need to be trashed.
 //
 // Returns:
-//   - Throws a TypeError if the follower is in the wrong state to stop or if it
+//   - Throws an error if the follower is in the wrong state to stop or if it
 //     fails to stop.
 func (c *Cmix) StopNetworkFollower(js.Value, []js.Value) any {
 	err := c.api.StopNetworkFollower()
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -87,6 +89,21 @@ func (c *Cmix) StopNetworkFollower(js.Value, []js.Value) any {
 // SetTrackNetworkPeriod allows changing the frequency that follower threads
 // are started.
 //
+// Note that the frequency of the follower threads affect the power usage
+// of the device following the network.
+//   - Low period -> Higher frequency of polling -> Higher battery usage
+//   - High period -> Lower frequency of polling -> Lower battery usage
+//
+// This may be used to enable a low power (or battery optimization) mode
+// for the end user.
+//
+// Suggested values are provided, however there are no guarantees that these
+// values will perfectly fit what the end user's device would require to match
+// the user's expectations:
+//   - Low Power Usage: 5000 milliseconds
+//   - High Power Usage: 1000 milliseconds (default, see
+//     [cmix.DefaultFollowPeriod]
+//
 // Parameters:
 //   - args[0] - The duration of the period, in milliseconds (int).
 func (c *Cmix) SetTrackNetworkPeriod(_ js.Value, args []js.Value) any {
@@ -150,7 +167,7 @@ func (c *Cmix) NetworkFollowerStatus(js.Value, []js.Value) any {
 func (c *Cmix) GetNodeRegistrationStatus(js.Value, []js.Value) any {
 	b, err := c.api.GetNodeRegistrationStatus()
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -171,7 +188,7 @@ func (c *Cmix) GetNodeRegistrationStatus(js.Value, []js.Value) any {
 func (c *Cmix) IsReady(_ js.Value, args []js.Value) any {
 	isReadyInfo, err := c.api.IsReady(args[0].Float())
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -190,7 +207,7 @@ func (c *Cmix) IsReady(_ js.Value, args []js.Value) any {
 func (c *Cmix) PauseNodeRegistrations(_ js.Value, args []js.Value) any {
 	err := c.api.PauseNodeRegistrations(args[0].Int())
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -210,7 +227,7 @@ func (c *Cmix) PauseNodeRegistrations(_ js.Value, args []js.Value) any {
 func (c *Cmix) ChangeNumberOfNodeRegistrations(_ js.Value, args []js.Value) any {
 	err := c.api.ChangeNumberOfNodeRegistrations(args[0].Int(), args[1].Int())
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -256,7 +273,7 @@ func (c *Cmix) IsHealthy(js.Value, []js.Value) any {
 func (c *Cmix) GetRunningProcesses(js.Value, []js.Value) any {
 	list, err := c.api.GetRunningProcesses()
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -362,7 +379,52 @@ type trackServicesCallback struct {
 //	  },
 //	]
 func (tsc *trackServicesCallback) Callback(marshalData []byte, err error) {
-	tsc.callback(utils.CopyBytesToJS(marshalData), utils.JsTrace(err))
+	tsc.callback(utils.CopyBytesToJS(marshalData), exception.NewTrace(err))
+}
+
+// trackCompressedServicesCallback adheres to the
+// [bindings.TrackCompressedServicesCallback] interface.
+type trackCompressedServicesCallback struct {
+	callback func(args ...any) js.Value
+}
+
+// Callback is the callback for [Cmix.TrackServices] that passes a
+// JSON-marshalled list of compressed backend services. If an error occurs while
+// retrieving or marshalling the service list, then err will be non-null.
+//
+// Parameters:
+//   - marshalData - JSON of [message.CompressedServiceList] (Uint8Array),
+//     which is a map of [id.ID] to an array of [message.CompressedService].
+//   - err - Error that occurs during retrieval or marshalling. Null otherwise
+//     (Error).
+//
+// Example JSON:
+//
+//		{
+//	   "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD": [
+//	     {
+//	       "Identifier": null,
+//	       "Tags": ["test"],
+//	       "Metadata": null
+//	     }
+//	   ],
+//	   "AAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD": [
+//	     {
+//	       "Identifier": null,
+//	       "Tags": ["test"],
+//	       "Metadata": null
+//	     }
+//	   ],
+//	   "AAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD": [
+//	     {
+//	       "Identifier": null,
+//	       "Tags": ["test"],
+//	       "Metadata": null
+//	     }
+//	   ]
+//	 }
+func (tsc *trackCompressedServicesCallback) Callback(marshalData []byte, err error) {
+	tsc.callback(utils.CopyBytesToJS(marshalData), exception.NewTrace(err))
 }
 
 // TrackServicesWithIdentity will return via a callback the list of services the
@@ -374,14 +436,18 @@ func (tsc *trackServicesCallback) Callback(marshalData []byte, err error) {
 //   - args[0] - ID of [E2e] object in tracker (int).
 //   - args[1] - Javascript object that has functions that implement the
 //     [bindings.ClientError] interface.
+//   - args[2] - Javascript object that has functions that implement the
+//     [bindings.TrackCompressedServicesCallback], which will be passed the JSON
+//     of [message.CompressedServiceList].
 //
 // Returns:
 //   - Throws TypeError if the [E2e] ID is invalid.
 func (c *Cmix) TrackServicesWithIdentity(_ js.Value, args []js.Value) any {
 	err := c.api.TrackServicesWithIdentity(args[0].Int(),
-		&trackServicesCallback{utils.WrapCB(args[0], "Callback")})
+		&trackServicesCallback{utils.WrapCB(args[0], "Callback")},
+		&trackCompressedServicesCallback{utils.WrapCB(args[0], "Callback")})
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
diff --git a/wasm/group.go b/wasm/group.go
index f3e7710a9144ba86fd33322b05c44092dd5c89bc..e824d4ee85ce7610f99e6e63dba6ab56e0e58f2a 100644
--- a/wasm/group.go
+++ b/wasm/group.go
@@ -11,7 +11,8 @@ package wasm
 
 import (
 	"gitlab.com/elixxir/client/v4/bindings"
-	"gitlab.com/elixxir/xxdk-wasm/utils"
+	"gitlab.com/elixxir/wasm-utils/exception"
+	"gitlab.com/elixxir/wasm-utils/utils"
 	"syscall/js"
 )
 
@@ -54,7 +55,7 @@ func newGroupChatJS(api *bindings.GroupChat) map[string]any {
 //
 // Returns:
 //   - Javascript representation of the [GroupChat] object.
-//   - Throws a TypeError if creating the [GroupChat] fails.
+//   - Throws an error if creating the [GroupChat] fails.
 func NewGroupChat(_ js.Value, args []js.Value) any {
 	requestFunc := &groupRequest{utils.WrapCB(args[1], "Callback")}
 	p := &groupChatProcessor{
@@ -62,7 +63,7 @@ func NewGroupChat(_ js.Value, args []js.Value) any {
 
 	api, err := bindings.NewGroupChat(args[0].Int(), requestFunc, p)
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -93,7 +94,7 @@ func (g *GroupChat) MakeGroup(_ js.Value, args []js.Value) any {
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
 		sendReport, err := g.api.MakeGroup(membershipBytes, message, name)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(utils.CopyBytesToJS(sendReport))
 		}
@@ -117,7 +118,7 @@ func (g *GroupChat) ResendRequest(_ js.Value, args []js.Value) any {
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
 		sendReport, err := g.api.ResendRequest(groupId)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(utils.CopyBytesToJS(sendReport))
 		}
@@ -135,11 +136,11 @@ func (g *GroupChat) ResendRequest(_ js.Value, args []js.Value) any {
 //     object returned over the bindings (Uint8Array).
 //
 // Returns:
-//   - Throws a TypeError if joining the group fails.
+//   - Throws an error if joining the group fails.
 func (g *GroupChat) JoinGroup(_ js.Value, args []js.Value) any {
 	err := g.api.JoinGroup(utils.CopyBytesToGo(args[0]))
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -153,11 +154,11 @@ func (g *GroupChat) JoinGroup(_ js.Value, args []js.Value) any {
 //     can be found in the report returned by [GroupChat.MakeGroup].
 //
 // Returns:
-//   - Throws a TypeError if leaving the group fails.
+//   - Throws an error if leaving the group fails.
 func (g *GroupChat) LeaveGroup(_ js.Value, args []js.Value) any {
 	err := g.api.LeaveGroup(utils.CopyBytesToGo(args[0]))
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -187,7 +188,7 @@ func (g *GroupChat) Send(_ js.Value, args []js.Value) any {
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
 		sendReport, err := g.api.Send(groupId, message, tag)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(utils.CopyBytesToJS(sendReport))
 		}
@@ -200,11 +201,11 @@ func (g *GroupChat) Send(_ js.Value, args []js.Value) any {
 //
 // Returns:
 //   - JSON of array of [id.ID] representing all group ID's (Uint8Array).
-//   - Throws a TypeError if getting the groups fails.
+//   - Throws an error if getting the groups fails.
 func (g *GroupChat) GetGroups(js.Value, []js.Value) any {
 	groups, err := g.api.GetGroups()
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -220,11 +221,11 @@ func (g *GroupChat) GetGroups(js.Value, []js.Value) any {
 //
 // Returns:
 //   - Javascript representation of the [GroupChat] object.
-//   - Throws a TypeError if getting the group fails.
+//   - Throws an error if getting the group fails.
 func (g *GroupChat) GetGroup(_ js.Value, args []js.Value) any {
 	grp, err := g.api.GetGroup(utils.CopyBytesToGo(args[0]))
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -314,11 +315,11 @@ func (g *Group) GetCreatedMS(js.Value, []js.Value) any {
 //
 // Returns:
 //   - JSON of [group.Membership] (Uint8Array).
-//   - Throws a TypeError if marshalling fails.
+//   - Throws an error if marshalling fails.
 func (g *Group) GetMembership(js.Value, []js.Value) any {
 	membership, err := g.api.GetMembership()
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -341,11 +342,11 @@ func (g *Group) Serialize(js.Value, []js.Value) any {
 //
 // Returns:
 //   - Javascript representation of the [GroupChat] object.
-//   - Throws a TypeError if getting the group fails.
+//   - Throws an error if getting the group fails.
 func DeserializeGroup(_ js.Value, args []js.Value) any {
 	grp, err := bindings.DeserializeGroup(utils.CopyBytesToGo(args[0]))
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -393,7 +394,7 @@ func (gcp *groupChatProcessor) Process(decryptedMessage, msg,
 	receptionId []byte, ephemeralId, roundId int64, roundURL string, err error) {
 	gcp.process(utils.CopyBytesToJS(decryptedMessage),
 		utils.CopyBytesToJS(msg), utils.CopyBytesToJS(receptionId), ephemeralId,
-		roundId, roundURL, utils.JsTrace(err))
+		roundId, roundURL, exception.NewTrace(err))
 }
 
 // String returns a name identifying this processor. Used for debugging.
diff --git a/wasm/identity.go b/wasm/identity.go
index 4141b1f1b9dc0b6a2cf0f374b428118b19899363..0800fffa8d209ab1bfd2c004998ade87c45cf2b3 100644
--- a/wasm/identity.go
+++ b/wasm/identity.go
@@ -12,7 +12,8 @@ package wasm
 import (
 	"gitlab.com/elixxir/client/v4/bindings"
 	"gitlab.com/elixxir/client/v4/xxdk"
-	"gitlab.com/elixxir/xxdk-wasm/utils"
+	"gitlab.com/elixxir/wasm-utils/exception"
+	"gitlab.com/elixxir/wasm-utils/utils"
 	"syscall/js"
 )
 
@@ -31,14 +32,14 @@ import (
 //   - args[2] - ID of [Cmix] object in tracker (int).
 //
 // Returns:
-//   - Throws a TypeError if the identity cannot be stored in storage.
+//   - Throws an error if the identity cannot be stored in storage.
 func StoreReceptionIdentity(_ js.Value, args []js.Value) any {
 	identity := utils.CopyBytesToGo(args[1])
 	err := bindings.StoreReceptionIdentity(
 		args[0].String(), identity, args[2].Int())
 
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -54,11 +55,11 @@ func StoreReceptionIdentity(_ js.Value, args []js.Value) any {
 //
 // Returns:
 //   - JSON of the stored [xxdk.ReceptionIdentity] object (Uint8Array).
-//   - Throws a TypeError if the identity cannot be retrieved from storage.
+//   - Throws an error if the identity cannot be retrieved from storage.
 func LoadReceptionIdentity(_ js.Value, args []js.Value) any {
 	ri, err := bindings.LoadReceptionIdentity(args[0].String(), args[1].Int())
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -75,7 +76,7 @@ func (c *Cmix) MakeReceptionIdentity(js.Value, []js.Value) any {
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
 		ri, err := c.api.MakeReceptionIdentity()
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(utils.CopyBytesToJS(ri))
 		}
@@ -94,7 +95,7 @@ func (c *Cmix) MakeLegacyReceptionIdentity(js.Value, []js.Value) any {
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
 		ri, err := c.api.MakeLegacyReceptionIdentity()
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(utils.CopyBytesToJS(ri))
 		}
@@ -126,13 +127,13 @@ func (c *Cmix) GetReceptionRegistrationValidationSignature(
 //
 // Returns:
 //   - Marshalled bytes of [contact.Contact] (string).
-//   - Throws a TypeError if unmarshalling the identity fails.
+//   - Throws an error if unmarshalling the identity fails.
 func GetContactFromReceptionIdentity(_ js.Value, args []js.Value) any {
 	// Note that this function does not appear in normal bindings
 	identityJSON := utils.CopyBytesToGo(args[0])
 	identity, err := xxdk.UnmarshalReceptionIdentity(identityJSON)
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -146,11 +147,11 @@ func GetContactFromReceptionIdentity(_ js.Value, args []js.Value) any {
 //
 // Returns:
 //   - Marshalled bytes of [id.ID] (Uint8Array).
-//   - Throws a TypeError if loading the ID from the contact file fails.
+//   - Throws an error if loading the ID from the contact file fails.
 func GetIDFromContact(_ js.Value, args []js.Value) any {
 	cID, err := bindings.GetIDFromContact(utils.CopyBytesToGo(args[0]))
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -165,11 +166,11 @@ func GetIDFromContact(_ js.Value, args []js.Value) any {
 //
 // Returns:
 //   - Bytes of the [cyclic.Int] object (Uint8Array).
-//   - Throws a TypeError if loading the public key from the contact file fails.
+//   - Throws an error if loading the public key from the contact file fails.
 func GetPubkeyFromContact(_ js.Value, args []js.Value) any {
 	key, err := bindings.GetPubkeyFromContact([]byte(args[0].String()))
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -189,13 +190,13 @@ func GetPubkeyFromContact(_ js.Value, args []js.Value) any {
 //
 // Returns:
 //   - Marshalled bytes of the modified [contact.Contact] (string).
-//   - Throws a TypeError if loading or modifying the contact fails.
+//   - Throws an error if loading or modifying the contact fails.
 func SetFactsOnContact(_ js.Value, args []js.Value) any {
 	marshaledContact := utils.CopyBytesToGo(args[0])
 	factListJSON := utils.CopyBytesToGo(args[1])
 	c, err := bindings.SetFactsOnContact(marshaledContact, factListJSON)
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -209,11 +210,11 @@ func SetFactsOnContact(_ js.Value, args []js.Value) any {
 //
 // Returns:
 //   - JSON of [fact.FactList] (Uint8Array).
-//   - Throws a TypeError if loading the contact fails.
+//   - Throws an error if loading the contact fails.
 func GetFactsFromContact(_ js.Value, args []js.Value) any {
 	fl, err := bindings.GetFactsFromContact(utils.CopyBytesToGo(args[0]))
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
diff --git a/wasm/logging.go b/wasm/logging.go
index 8199edc02a829a8e8b81b59dbfc73cddb4d46857..1ae9bb7127447f1a786833788eb34250ed783979 100644
--- a/wasm/logging.go
+++ b/wasm/logging.go
@@ -10,35 +10,10 @@
 package wasm
 
 import (
-	"gitlab.com/elixxir/client/v4/bindings"
-	"gitlab.com/elixxir/xxdk-wasm/logging"
 	"syscall/js"
-)
 
-// LogLevel sets level of logging. All logs at the set level and below will be
-// displayed (e.g., when log level is ERROR, only ERROR, CRITICAL, and FATAL
-// messages will be printed).
-//
-// Log level options:
-//
-//	TRACE    - 0
-//	DEBUG    - 1
-//	INFO     - 2
-//	WARN     - 3
-//	ERROR    - 4
-//	CRITICAL - 5
-//	FATAL    - 6
-//
-// The default log level without updates is INFO.
-//
-// Parameters:
-//   - args[0] - Log level (int).
-//
-// Returns:
-//   - Throws TypeError if the log level is invalid.
-func LogLevel(this js.Value, args []js.Value) any {
-	return logging.LogLevelJS(this, args)
-}
+	"gitlab.com/elixxir/client/v4/bindings"
+)
 
 // logWriter wraps Javascript callbacks to adhere to the [bindings.LogWriter]
 // interface.
diff --git a/wasm/ndf.go b/wasm/ndf.go
index 69441fea2c03e5f369a0e9a76a5936b7e43bf67f..292fe3ecfb42d57a2a7964c03265bdfd7c39d14c 100644
--- a/wasm/ndf.go
+++ b/wasm/ndf.go
@@ -11,7 +11,8 @@ package wasm
 
 import (
 	"gitlab.com/elixxir/client/v4/bindings"
-	"gitlab.com/elixxir/xxdk-wasm/utils"
+	"gitlab.com/elixxir/wasm-utils/exception"
+	"gitlab.com/elixxir/wasm-utils/utils"
 	"syscall/js"
 )
 
@@ -34,7 +35,7 @@ func DownloadAndVerifySignedNdfWithUrl(_ js.Value, args []js.Value) any {
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
 		ndf, err := bindings.DownloadAndVerifySignedNdfWithUrl(url, cert)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(utils.CopyBytesToJS(ndf))
 		}
diff --git a/wasm/notifications.go b/wasm/notifications.go
new file mode 100644
index 0000000000000000000000000000000000000000..26154318b16c4b4e9f54270ea97b40ec7faaa32d
--- /dev/null
+++ b/wasm/notifications.go
@@ -0,0 +1,130 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+//go:build js && wasm
+
+package wasm
+
+import (
+	"syscall/js"
+
+	"gitlab.com/elixxir/client/v4/bindings"
+	"gitlab.com/elixxir/wasm-utils/exception"
+)
+
+type Notifications struct {
+	api *bindings.Notifications
+}
+
+// newNotificationsJS wrapts the bindings Noticiation object and implements
+// wrappers in JS for all it's functionality.
+func newNotificationsJS(api *bindings.Notifications) map[string]any {
+	n := Notifications{api}
+	notificationsImplJS := map[string]any{
+		"AddToken":    js.FuncOf(n.AddToken),
+		"RemoveToken": js.FuncOf(n.RemoveToken),
+		"SetMaxState": js.FuncOf(n.SetMaxState),
+		"GetMaxState": js.FuncOf(n.GetMaxState),
+		"GetID":       js.FuncOf(n.GetID),
+	}
+	return notificationsImplJS
+}
+
+// LoadNotifications returns a JS wrapped implementation of
+// [bindings.Notifications].
+//
+// Parameters:
+//   - args[0] - the cMixID integer
+//
+// Returns a notifications object or throws an error
+func LoadNotifications(_ js.Value, args []js.Value) any {
+	cMixID := args[0].Int()
+	api, err := bindings.LoadNotifications(cMixID)
+	if err != nil {
+		exception.ThrowTrace(err)
+		return nil
+	}
+
+	return newNotificationsJS(api)
+}
+
+// LoadNotificationsDummy returns a JS wrapped implementation of
+// [bindings.Notifications] with a dummy notifications implementation.
+//
+// Parameters:
+//   - args[0] - the cMixID integer
+//
+// Returns a notifications object or throws an error
+func LoadNotificationsDummy(_ js.Value, args []js.Value) any {
+	cMixID := args[0].Int()
+	api, err := bindings.LoadNotificationsDummy(cMixID)
+	if err != nil {
+		exception.ThrowTrace(err)
+		return nil
+	}
+
+	return newNotificationsJS(api)
+}
+
+// GetID returns the bindings ID for the [bindings.Notifications] object
+func (n *Notifications) GetID(js.Value, []js.Value) any {
+	return n.api.GetID()
+}
+
+// AddToken implements [bindings.Notifications.AddToken].
+//
+// Parameters:
+//   - args[0] - newToken string
+//   - args[1] - app string
+//
+// Returns nothing or an error (throwable)
+func (n *Notifications) AddToken(_ js.Value, args []js.Value) any {
+	newToken := args[0].String()
+	app := args[1].String()
+
+	err := n.api.AddToken(newToken, app)
+	if err != nil {
+		exception.ThrowTrace(err)
+	}
+
+	return nil
+}
+
+// RemoveToken implements [bindings.Notifications.RemoveToken].
+//
+// Returns nothing or throws an error.
+func (n *Notifications) RemoveToken(_ js.Value, args []js.Value) any {
+	err := n.api.RemoveToken()
+	if err != nil {
+		exception.ThrowTrace(err)
+	}
+	return nil
+}
+
+// SetMaxState implements [bindings.Notifications.SetMaxState]
+//
+// Parameters:
+//   - args[0] - maxState integer
+//
+// Returns nothing or throws an error
+func (n *Notifications) SetMaxState(_ js.Value, args []js.Value) any {
+	maxState := int64(args[0].Int())
+
+	err := n.api.SetMaxState(maxState)
+	if err != nil {
+		exception.ThrowTrace(err)
+	}
+
+	return nil
+}
+
+// GetMaxState implements [bindings.Notifications.GetMaxState]
+//
+// Returns the current maxState integer
+func (n *Notifications) GetMaxState(_ js.Value, args []js.Value) any {
+	return int64(n.api.GetMaxState())
+}
diff --git a/wasm/params.go b/wasm/params.go
index 01f307c6c029f1643e674fe7b77db44d4830b2fe..5b8dac2f78656de84be3050705cc4c72415a5ee7 100644
--- a/wasm/params.go
+++ b/wasm/params.go
@@ -11,7 +11,7 @@ package wasm
 
 import (
 	"gitlab.com/elixxir/client/v4/bindings"
-	"gitlab.com/elixxir/xxdk-wasm/utils"
+	"gitlab.com/elixxir/wasm-utils/utils"
 	"syscall/js"
 )
 
diff --git a/wasm/restlike.go b/wasm/restlike.go
index 0cf802a32bb0c638859c77d1a32fc609cd2994b8..d56ff586279f292042d2faa7df39d38714273272 100644
--- a/wasm/restlike.go
+++ b/wasm/restlike.go
@@ -11,7 +11,8 @@ package wasm
 
 import (
 	"gitlab.com/elixxir/client/v4/bindings"
-	"gitlab.com/elixxir/xxdk-wasm/utils"
+	"gitlab.com/elixxir/wasm-utils/exception"
+	"gitlab.com/elixxir/wasm-utils/utils"
 	"syscall/js"
 )
 
@@ -39,7 +40,7 @@ func RestlikeRequest(_ js.Value, args []js.Value) any {
 		msg, err := bindings.RestlikeRequest(
 			cmixId, connectionID, request, e2eParamsJSON)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(utils.CopyBytesToJS(msg))
 		}
@@ -72,7 +73,7 @@ func RestlikeRequestAuth(_ js.Value, args []js.Value) any {
 		msg, err := bindings.RestlikeRequestAuth(
 			cmixId, authConnectionID, request, e2eParamsJSON)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(utils.CopyBytesToJS(msg))
 		}
diff --git a/wasm/restlikeSingle.go b/wasm/restlikeSingle.go
index 391e40c01a2711aa630468ae59d06e15b5312bab..41118d7f6d6ae32960712ea2f0436a927d28255d 100644
--- a/wasm/restlikeSingle.go
+++ b/wasm/restlikeSingle.go
@@ -11,7 +11,8 @@ package wasm
 
 import (
 	"gitlab.com/elixxir/client/v4/bindings"
-	"gitlab.com/elixxir/xxdk-wasm/utils"
+	"gitlab.com/elixxir/wasm-utils/exception"
+	"gitlab.com/elixxir/wasm-utils/utils"
 	"syscall/js"
 )
 
@@ -27,7 +28,7 @@ type restlikeCallback struct {
 //   - payload - JSON of [restlike.Message] (Uint8Array).
 //   - err - Returns an error on failure (Error).
 func (rlc *restlikeCallback) Callback(payload []byte, err error) {
-	rlc.callback(utils.CopyBytesToJS(payload), utils.JsTrace(err))
+	rlc.callback(utils.CopyBytesToJS(payload), exception.NewTrace(err))
 }
 
 // RequestRestLike sends a restlike request to a given contact.
@@ -54,7 +55,7 @@ func RequestRestLike(_ js.Value, args []js.Value) any {
 		msg, err := bindings.RequestRestLike(
 			e2eID, recipient, request, paramsJSON)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(utils.CopyBytesToJS(msg))
 		}
@@ -79,7 +80,7 @@ func RequestRestLike(_ js.Value, args []js.Value) any {
 //     [bindings.RestlikeCallback] interface.
 //
 // Returns:
-//   - Throws a TypeError if parsing the parameters or making the request fails.
+//   - Throws an error if parsing the parameters or making the request fails.
 func AsyncRequestRestLike(_ js.Value, args []js.Value) any {
 	e2eID := args[0].Int()
 	recipient := utils.CopyBytesToGo(args[1])
@@ -91,7 +92,7 @@ func AsyncRequestRestLike(_ js.Value, args []js.Value) any {
 		err := bindings.AsyncRequestRestLike(
 			e2eID, recipient, request, paramsJSON, cb)
 		if err != nil {
-			utils.Throw(utils.TypeError, err)
+			exception.ThrowTrace(err)
 		}
 	}()
 
diff --git a/wasm/secrets.go b/wasm/secrets.go
index d310fcbcd4cd7e2593ec6d360274ed5cf9eea48c..4cba18e0495a251eb7a803bafd968c8f16757e50 100644
--- a/wasm/secrets.go
+++ b/wasm/secrets.go
@@ -11,7 +11,7 @@ package wasm
 
 import (
 	"gitlab.com/elixxir/client/v4/bindings"
-	"gitlab.com/elixxir/xxdk-wasm/utils"
+	"gitlab.com/elixxir/wasm-utils/utils"
 	"syscall/js"
 )
 
diff --git a/wasm/single.go b/wasm/single.go
index 8198bb80fc58de0b21a045c47b39194efd149cfa..3528a0bea1d27896c28e35c94960a9cf10fa1034 100644
--- a/wasm/single.go
+++ b/wasm/single.go
@@ -11,7 +11,8 @@ package wasm
 
 import (
 	"gitlab.com/elixxir/client/v4/bindings"
-	"gitlab.com/elixxir/xxdk-wasm/utils"
+	"gitlab.com/elixxir/wasm-utils/exception"
+	"gitlab.com/elixxir/wasm-utils/utils"
 	"syscall/js"
 )
 
@@ -49,7 +50,7 @@ func TransmitSingleUse(_ js.Value, args []js.Value) any {
 		sendReport, err := bindings.TransmitSingleUse(
 			e2eID, recipient, tag, payload, paramsJSON, responseCB)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(utils.CopyBytesToJS(sendReport))
 		}
@@ -71,12 +72,12 @@ func TransmitSingleUse(_ js.Value, args []js.Value) any {
 // Returns:
 //   - Javascript representation of the [Stopper] object, an interface
 //     containing a function used to stop the listener.
-//   - Throws a TypeError if listening fails.
+//   - Throws an error if listening fails.
 func Listen(_ js.Value, args []js.Value) any {
 	cb := &singleUseCallback{utils.WrapCB(args[2], "Callback")}
 	api, err := bindings.Listen(args[0].Int(), args[1].String(), cb)
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -128,7 +129,7 @@ type singleUseCallback struct {
 //     (Uint8Array).
 //   - err - Returns an error on failure (Error).
 func (suc *singleUseCallback) Callback(callbackReport []byte, err error) {
-	suc.callback(utils.CopyBytesToJS(callbackReport), utils.JsTrace(err))
+	suc.callback(utils.CopyBytesToJS(callbackReport), exception.NewTrace(err))
 }
 
 // singleUseResponse wraps Javascript callbacks to adhere to the
@@ -145,5 +146,5 @@ type singleUseResponse struct {
 //     (Uint8Array).
 //   - err - Returns an error on failure (Error).
 func (sur *singleUseResponse) Callback(responseReport []byte, err error) {
-	sur.callback(utils.CopyBytesToJS(responseReport), utils.JsTrace(err))
+	sur.callback(utils.CopyBytesToJS(responseReport), exception.NewTrace(err))
 }
diff --git a/wasm/timeNow.go b/wasm/timeNow.go
index 627a16b4e344089d782edf435fdcd3225c6c230b..ca3ebe6037161df753d80f7ed08e01c4e3d5b335 100644
--- a/wasm/timeNow.go
+++ b/wasm/timeNow.go
@@ -11,7 +11,7 @@ package wasm
 
 import (
 	"gitlab.com/elixxir/client/v4/bindings"
-	"gitlab.com/elixxir/xxdk-wasm/utils"
+	"gitlab.com/elixxir/wasm-utils/utils"
 	"syscall/js"
 )
 
diff --git a/wasm/ud.go b/wasm/ud.go
index 74bc2b11b628a4139040168194af9b6d21009a72..cce7c8a7fecc09f6690c42de5891b0910ba3d5f2 100644
--- a/wasm/ud.go
+++ b/wasm/ud.go
@@ -11,7 +11,8 @@ package wasm
 
 import (
 	"gitlab.com/elixxir/client/v4/bindings"
-	"gitlab.com/elixxir/xxdk-wasm/utils"
+	"gitlab.com/elixxir/wasm-utils/exception"
+	"gitlab.com/elixxir/wasm-utils/utils"
 	"syscall/js"
 )
 
@@ -99,7 +100,7 @@ func (uns *udNetworkStatus) UdNetworkStatus() int {
 // Returns:
 //   - Javascript representation of the [UserDiscovery] object that is
 //     registered to the specified UD service.
-//   - Throws a TypeError if creating or loading fails.
+//   - Throws an error if creating or loading fails.
 func NewOrLoadUd(_ js.Value, args []js.Value) any {
 	e2eID := args[0].Int()
 	follower := &udNetworkStatus{utils.WrapCB(args[1], "UdNetworkStatus")}
@@ -112,7 +113,7 @@ func NewOrLoadUd(_ js.Value, args []js.Value) any {
 	api, err := bindings.NewOrLoadUd(e2eID, follower, username,
 		registrationValidationSignature, cert, contactFile, address)
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -145,7 +146,7 @@ func NewOrLoadUd(_ js.Value, args []js.Value) any {
 // Returns:
 //   - Javascript representation of the [UserDiscovery] object that is loaded
 //     from backup.
-//   - Throws a TypeError if getting UD from backup fails.
+//   - Throws an error if getting UD from backup fails.
 func NewUdManagerFromBackup(_ js.Value, args []js.Value) any {
 	e2eID := args[0].Int()
 	follower := &udNetworkStatus{utils.WrapCB(args[1], "UdNetworkStatus")}
@@ -157,7 +158,7 @@ func NewUdManagerFromBackup(_ js.Value, args []js.Value) any {
 		e2eID, follower, cert,
 		contactFile, address)
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -182,7 +183,7 @@ func (ud *UserDiscovery) GetFacts(js.Value, []js.Value) any {
 func (ud *UserDiscovery) GetContact(js.Value, []js.Value) any {
 	c, err := ud.api.GetContact()
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -203,7 +204,7 @@ func (ud *UserDiscovery) GetContact(js.Value, []js.Value) any {
 func (ud *UserDiscovery) ConfirmFact(_ js.Value, args []js.Value) any {
 	err := ud.api.ConfirmFact(args[0].String(), args[1].String())
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -228,7 +229,7 @@ func (ud *UserDiscovery) ConfirmFact(_ js.Value, args []js.Value) any {
 func (ud *UserDiscovery) SendRegisterFact(_ js.Value, args []js.Value) any {
 	confirmationID, err := ud.api.SendRegisterFact(utils.CopyBytesToGo(args[0]))
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -247,7 +248,7 @@ func (ud *UserDiscovery) SendRegisterFact(_ js.Value, args []js.Value) any {
 func (ud *UserDiscovery) PermanentDeleteAccount(_ js.Value, args []js.Value) any {
 	err := ud.api.PermanentDeleteAccount(utils.CopyBytesToGo(args[0]))
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -265,7 +266,7 @@ func (ud *UserDiscovery) PermanentDeleteAccount(_ js.Value, args []js.Value) any
 func (ud *UserDiscovery) RemoveFact(_ js.Value, args []js.Value) any {
 	err := ud.api.RemoveFact(utils.CopyBytesToGo(args[0]))
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -290,7 +291,7 @@ type udLookupCallback struct {
 //     the lookup, or nil if an error occurs (Uint8Array).
 //   - err - Returns an error on failure (Error).
 func (ulc *udLookupCallback) Callback(contactBytes []byte, err error) {
-	ulc.callback(utils.CopyBytesToJS(contactBytes), utils.JsTrace(err))
+	ulc.callback(utils.CopyBytesToJS(contactBytes), exception.NewTrace(err))
 }
 
 // LookupUD returns the public key of the passed ID as known by the user
@@ -322,7 +323,7 @@ func LookupUD(_ js.Value, args []js.Value) any {
 		sendReport, err := bindings.LookupUD(
 			e2eID, udContact, cb, lookupId, singleRequestParamsJSON)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(utils.CopyBytesToJS(sendReport))
 		}
@@ -357,7 +358,7 @@ type udSearchCallback struct {
 //	  "<xxc(2)d7RJTu61Vy1lDThDMn8rYIiKSe1uXA/RCvvcIhq5Yg4DEgB7Ugdw/BAr6RsCABkWAFV1c2VybmFtZTI7N3XWrxIUpR29atpFMkcR6A==xxc>"
 //	}
 func (usc *udSearchCallback) Callback(contactListJSON []byte, err error) {
-	usc.callback(utils.CopyBytesToJS(contactListJSON), utils.JsTrace(err))
+	usc.callback(utils.CopyBytesToJS(contactListJSON), exception.NewTrace(err))
 }
 
 // SearchUD searches user discovery for the passed Facts. The searchCallback
@@ -389,7 +390,7 @@ func SearchUD(_ js.Value, args []js.Value) any {
 		sendReport, err := bindings.SearchUD(
 			e2eID, udContact, cb, factListJSON, singleRequestParamsJSON)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(utils.CopyBytesToJS(sendReport))
 		}
diff --git a/wasm/version.go b/wasm/version.go
index ee1a921cc666ef3a5e2491b5f43371ee098e2f3f..048c49b585eb3196d8e1c3926cb87a6c7bf9556e 100644
--- a/wasm/version.go
+++ b/wasm/version.go
@@ -14,8 +14,9 @@ import (
 	"syscall/js"
 
 	"gitlab.com/elixxir/client/v4/bindings"
+	"gitlab.com/elixxir/wasm-utils/exception"
+	"gitlab.com/elixxir/wasm-utils/utils"
 	"gitlab.com/elixxir/xxdk-wasm/storage"
-	"gitlab.com/elixxir/xxdk-wasm/utils"
 )
 
 // GetVersion returns the current xxDK WASM semantic version.
@@ -66,7 +67,7 @@ type VersionInfo struct {
 //
 // Returns:
 //   - JSON of [VersionInfo] (Uint8Array).
-//   - Throws a TypeError if getting the version failed.
+//   - Throws an error if getting the version failed.
 func GetWasmSemanticVersion(js.Value, []js.Value) any {
 	vi := VersionInfo{
 		Current: storage.SEMVER,
@@ -80,7 +81,7 @@ func GetWasmSemanticVersion(js.Value, []js.Value) any {
 
 	data, err := json.Marshal(vi)
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 	}
 
 	return utils.CopyBytesToJS(data)
@@ -91,7 +92,7 @@ func GetWasmSemanticVersion(js.Value, []js.Value) any {
 //
 // Returns:
 //   - JSON of [VersionInfo] (Uint8Array).
-//   - Throws a TypeError if getting the version failed.
+//   - Throws an error if getting the version failed.
 func GetXXDKSemanticVersion(js.Value, []js.Value) any {
 	vi := VersionInfo{
 		Current: bindings.GetVersion(),
@@ -104,7 +105,7 @@ func GetXXDKSemanticVersion(js.Value, []js.Value) any {
 
 	data, err := json.Marshal(vi)
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 	}
 
 	return utils.CopyBytesToJS(data)
diff --git a/wasm_exec.js b/wasm_exec.js
index c6ae00c713c4bb089eb2d76d61d3e88aa0835839..5365a841cd2ff018149c25666e4b525a42bc1344 100644
--- a/wasm_exec.js
+++ b/wasm_exec.js
@@ -453,7 +453,7 @@
 					},
 
 					// func Throw(exception string, message string)
-					'gitlab.com/elixxir/xxdk-wasm/utils.throw': (sp) => {
+					'gitlab.com/elixxir/wasm-utils/exception.throw': (sp) => {
 						const exception = loadString(sp + 8)
 						const message = loadString(sp + 24)
 						throw globalThis[exception](message)
diff --git a/wasm_test.go b/wasm_test.go
index 2d36f7b0c8d3ae5a024ea837f6da9526dc42d3c8..b415ece5623aa4e235511fb1f9cb24058e02e384 100644
--- a/wasm_test.go
+++ b/wasm_test.go
@@ -63,6 +63,12 @@ func TestPublicFunctions(t *testing.T) {
 		// C-Library specific bindings not needed by the browser
 		"GetDMInstance":   {},
 		"GetCMixInstance": {},
+
+		// Logging has been moved to startup flags
+		"LogLevel": {},
+
+		// NewFilesystemRemoteStorage is internal for bindings.
+		"NewFileSystemRemoteStorage": {},
 	}
 	wasmFuncs := getPublicFunctions("wasm", t)
 	bindingsFuncs := getPublicFunctions(
diff --git a/worker/README.md b/worker/README.md
index e5027c3fe7d366d5096bb9b09406ea6ab6edaa2f..18d21f12fb5115a92941451c2f9aa0667ca3fa9a 100644
--- a/worker/README.md
+++ b/worker/README.md
@@ -18,7 +18,7 @@ package main
 
 import (
 	"fmt"
-	"gitlab.com/elixxir/xxdk-wasm/utils/worker"
+	"gitlab.com/elixxir/wasm-utils/utils/worker"
 )
 
 func main() {
@@ -51,4 +51,4 @@ wm, err := worker.NewManager("workerWasm.js", "exampleWebWorker")
 if err != nil {
 	return nil, err
 }
-```
\ No newline at end of file
+```
diff --git a/worker/manager.go b/worker/manager.go
index c38438facdf8b502d8b2fc00ba0fe4855b5cfaf8..2809a40922609923f300593a3120082afc46f5a5 100644
--- a/worker/manager.go
+++ b/worker/manager.go
@@ -18,7 +18,7 @@ import (
 	"github.com/pkg/errors"
 	jww "github.com/spf13/jwalterweatherman"
 
-	"gitlab.com/elixxir/xxdk-wasm/utils"
+	"gitlab.com/elixxir/wasm-utils/utils"
 )
 
 // initID is the ID for the first item in the callback list. If the list only
@@ -41,7 +41,7 @@ const (
 // put on.
 const receiveQueueChanSize = 100
 
-// ReceptionCallback is the function that handles incoming data from the worker.
+// ReceptionCallback is called with a message received from the worker.
 type ReceptionCallback func(data []byte)
 
 // Manager manages the handling of messages received from the worker.
@@ -64,7 +64,7 @@ type Manager struct {
 
 	// receiveQueue is the channel that all received messages are queued on
 	// while they wait to be processed.
-	receiveQueue chan []byte
+	receiveQueue chan js.Value
 
 	// quit, when triggered, stops the thread that processes received messages.
 	quit chan struct{}
@@ -89,7 +89,7 @@ func NewManager(aURL, name string, messageLogging bool) (*Manager, error) {
 		worker:         js.Global().Get("Worker").New(aURL, opts),
 		callbacks:      make(map[Tag]map[uint64]ReceptionCallback),
 		responseIDs:    make(map[Tag]uint64),
-		receiveQueue:   make(chan []byte, receiveQueueChanSize),
+		receiveQueue:   make(chan js.Value, receiveQueueChanSize),
 		quit:           make(chan struct{}),
 		name:           name,
 		messageLogging: messageLogging,
@@ -138,11 +138,24 @@ func (m *Manager) processThread() {
 		case <-m.quit:
 			jww.INFO.Printf("[WW] [%s] Quitting process thread.", m.name)
 			return
-		case message := <-m.receiveQueue:
-			err := m.processReceivedMessage(message)
-			if err != nil {
-				jww.ERROR.Printf("[WW] [%s] Failed to process received "+
-					"message from worker: %+v", m.name, err)
+		case msgData := <-m.receiveQueue:
+
+			switch msgData.Type() {
+			case js.TypeObject:
+				if msgData.Get("constructor").Equal(utils.Uint8Array) {
+					err := m.processReceivedMessage(utils.CopyBytesToGo(msgData))
+					if err != nil {
+						jww.ERROR.Printf("[WW] [%s] Failed to process received "+
+							"message from worker: %+v", m.name, err)
+					}
+					break
+				}
+				fallthrough
+
+			default:
+				jww.ERROR.Printf("[WW] [%s] Cannot handle data of type %s "+
+					"from worker: %s", m.name, msgData.Type(),
+					utils.JsToJson(msgData))
 			}
 		}
 	}
@@ -174,12 +187,12 @@ func (m *Manager) SendMessage(
 			"ID %d going to worker: %+v", m.name, msg, tag, id, err)
 	}
 
-	go m.postMessage(string(payload))
+	go m.postMessage(payload)
 }
 
 // receiveMessage is registered with the Javascript event listener and is called
 // every time a new message from the worker is received.
-func (m *Manager) receiveMessage(data []byte) {
+func (m *Manager) receiveMessage(data js.Value) {
 	m.receiveQueue <- data
 }
 
@@ -303,7 +316,7 @@ func (m *Manager) addEventListeners() {
 	// occurs when a message is received from the worker.
 	// Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker/message_event
 	messageEvent := js.FuncOf(func(_ js.Value, args []js.Value) any {
-		m.receiveMessage([]byte(args[0].Get("data").String()))
+		m.receiveMessage(args[0].Get("data"))
 		return nil
 	})
 
@@ -312,8 +325,8 @@ func (m *Manager) addEventListeners() {
 	// Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker/error_event
 	errorEvent := js.FuncOf(func(_ js.Value, args []js.Value) any {
 		event := args[0]
-		jww.ERROR.Printf("[WW] [%s] Main received error event: %s",
-			m.name, utils.JsErrorToJson(event))
+		jww.FATAL.Panicf("[WW] [%s] Main received error event: %+v",
+			m.name, js.Error{Value: event})
 		return nil
 	})
 
@@ -322,8 +335,8 @@ func (m *Manager) addEventListeners() {
 	// Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker/messageerror_event
 	messageerrorEvent := js.FuncOf(func(_ js.Value, args []js.Value) any {
 		event := args[0]
-		jww.ERROR.Printf("[WW] [%s] Main received message error event: %s",
-			m.name, utils.JsErrorToJson(event))
+		jww.ERROR.Printf("[WW] [%s] Main received message error event: %+v",
+			m.name, js.Error{Value: event})
 		return nil
 	})
 
@@ -336,20 +349,19 @@ func (m *Manager) addEventListeners() {
 
 // postMessage sends a message to the worker.
 //
-// message is the object to deliver to the worker; this will be in the data
-// field in the event delivered to the worker. It must be a js.Value or a
-// primitive type that can be converted via js.ValueOf. The Javascript object
-// must be "any value or JavaScript object handled by the structured clone
-// algorithm, which includes cyclical references.". See the doc for more
-// information.
+// msg is the object to deliver to the worker; this will be in the data
+// field in the event delivered to the worker. It must be a transferable object
+// because this function transfers ownership of the message instead of copying
+// it for better performance. See the doc for more information.
 //
 // If the message parameter is not provided, a SyntaxError will be thrown by the
 // parser. If the data to be passed to the worker is unimportant, js.Null or
 // js.Undefined can be passed explicitly.
 //
 // Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker/postMessage
-func (m *Manager) postMessage(msg any) {
-	m.worker.Call("postMessage", msg)
+func (m *Manager) postMessage(msg []byte) {
+	buffer := utils.CopyBytesToJS(msg)
+	m.worker.Call("postMessage", buffer, []any{buffer.Get("buffer")})
 }
 
 // terminate immediately terminates the Worker. This does not offer the worker
diff --git a/worker/manager_test.go b/worker/manager_test.go
index 49a395a9c45a33f25892cc1efc86e263ccf06063..b071c7e437d149eb49aec41da593f858c4c01354 100644
--- a/worker/manager_test.go
+++ b/worker/manager_test.go
@@ -21,7 +21,7 @@ func TestManager_processReceivedMessage(t *testing.T) {
 	m := &Manager{callbacks: make(map[Tag]map[uint64]ReceptionCallback)}
 
 	msg := Message{Tag: readyTag, ID: 5}
-	cbChan := make(chan struct{})
+	cbChan := make(chan struct{}, 1)
 	cb := func([]byte) { cbChan <- struct{}{} }
 	m.callbacks[msg.Tag] = map[uint64]ReceptionCallback{msg.ID: cb}
 
@@ -31,16 +31,16 @@ func TestManager_processReceivedMessage(t *testing.T) {
 	}
 
 	go func() {
-		select {
-		case <-cbChan:
-		case <-time.After(10 * time.Millisecond):
-			t.Error("Timed out waiting for callback to be called.")
+		err = m.processReceivedMessage(data)
+		if err != nil {
+			t.Errorf("Failed to receive message: %+v", err)
 		}
 	}()
 
-	err = m.processReceivedMessage(data)
-	if err != nil {
-		t.Errorf("Failed to receive message: %+v", err)
+	select {
+	case <-cbChan:
+	case <-time.After(10 * time.Millisecond):
+		t.Error("Timed out waiting for callback to be called.")
 	}
 }
 
@@ -97,7 +97,7 @@ func TestManager_RegisterCallback(t *testing.T) {
 	m := &Manager{callbacks: make(map[Tag]map[uint64]ReceptionCallback)}
 
 	msg := Message{Tag: readyTag, ID: initID}
-	cbChan := make(chan struct{})
+	cbChan := make(chan struct{}, 1)
 	cb := func([]byte) { cbChan <- struct{}{} }
 	m.RegisterCallback(msg.Tag, cb)
 
@@ -107,16 +107,16 @@ func TestManager_RegisterCallback(t *testing.T) {
 	}
 
 	go func() {
-		select {
-		case <-cbChan:
-		case <-time.After(10 * time.Millisecond):
-			t.Error("Timed out waiting for callback to be called.")
+		err = m.processReceivedMessage(data)
+		if err != nil {
+			t.Errorf("Failed to receive message: %+v", err)
 		}
 	}()
 
-	err = m.processReceivedMessage(data)
-	if err != nil {
-		t.Errorf("Failed to receive message: %+v", err)
+	select {
+	case <-cbChan:
+	case <-time.After(10 * time.Millisecond):
+		t.Error("Timed out waiting for callback to be called.")
 	}
 }
 
@@ -129,7 +129,7 @@ func TestManager_registerReplyCallback(t *testing.T) {
 	}
 
 	msg := Message{Tag: readyTag, ID: 5}
-	cbChan := make(chan struct{})
+	cbChan := make(chan struct{}, 1)
 	cb := func([]byte) { cbChan <- struct{}{} }
 	m.registerReplyCallback(msg.Tag, cb)
 	m.callbacks[msg.Tag] = map[uint64]ReceptionCallback{msg.ID: cb}
@@ -140,16 +140,16 @@ func TestManager_registerReplyCallback(t *testing.T) {
 	}
 
 	go func() {
-		select {
-		case <-cbChan:
-		case <-time.After(10 * time.Millisecond):
-			t.Error("Timed out waiting for callback to be called.")
+		err = m.processReceivedMessage(data)
+		if err != nil {
+			t.Errorf("Failed to receive message: %+v", err)
 		}
 	}()
 
-	err = m.processReceivedMessage(data)
-	if err != nil {
-		t.Errorf("Failed to receive message: %+v", err)
+	select {
+	case <-cbChan:
+	case <-time.After(10 * time.Millisecond):
+		t.Error("Timed out waiting for callback to be called.")
 	}
 }
 
diff --git a/worker/thread.go b/worker/thread.go
index 07591fc7d8f7a905f521b98215096b2d8d5ba044..393ff7661db3f0c773932fdff932a15a3bfe92b9 100644
--- a/worker/thread.go
+++ b/worker/thread.go
@@ -17,11 +17,12 @@ import (
 	"github.com/pkg/errors"
 	jww "github.com/spf13/jwalterweatherman"
 
-	"gitlab.com/elixxir/xxdk-wasm/utils"
+	"gitlab.com/elixxir/wasm-utils/utils"
 )
 
-// ThreadReceptionCallback is the function that handles incoming data from the
-// main thread.
+// ThreadReceptionCallback is called with a message received from the main
+// thread. Any bytes returned are sent as a response back to the main thread.
+// Any returned errors are printed to the log.
 type ThreadReceptionCallback func(data []byte) ([]byte, error)
 
 // ThreadManager queues incoming messages from the main thread and handles them
@@ -34,9 +35,9 @@ type ThreadManager struct {
 	// main thread keyed on the callback tag.
 	callbacks map[Tag]ThreadReceptionCallback
 
-	// receiveQueue is the channel that all received messages are queued on
-	// while they wait to be processed.
-	receiveQueue chan []byte
+	// receiveQueue is the channel that all received MessageEvent.data are
+	// queued on while they wait to be processed.
+	receiveQueue chan js.Value
 
 	// quit, when triggered, stops the thread that processes received messages.
 	quit chan struct{}
@@ -56,7 +57,7 @@ func NewThreadManager(name string, messageLogging bool) *ThreadManager {
 	tm := &ThreadManager{
 		messages:       make(chan js.Value, 100),
 		callbacks:      make(map[Tag]ThreadReceptionCallback),
-		receiveQueue:   make(chan []byte, receiveQueueChanSize),
+		receiveQueue:   make(chan js.Value, receiveQueueChanSize),
 		quit:           make(chan struct{}),
 		name:           name,
 		messageLogging: messageLogging,
@@ -88,14 +89,24 @@ func (tm *ThreadManager) processThread() {
 		case <-tm.quit:
 			jww.INFO.Printf("[WW] [%s] Quitting worker process thread.", tm.name)
 			return
-		case message := <-tm.receiveQueue:
-			if tm.messageLogging {
-				jww.INFO.Printf("[WW] Worker processors received message: %q", message)
-			}
-			err := tm.processReceivedMessage(message)
-			if err != nil {
-				jww.ERROR.Printf("[WW] [%s] Failed to receive message from "+
-					"main thread: %+v", tm.name, err)
+		case msgData := <-tm.receiveQueue:
+
+			switch msgData.Type() {
+			case js.TypeObject:
+				if msgData.Get("constructor").Equal(utils.Uint8Array) {
+					err := tm.processReceivedMessage(utils.CopyBytesToGo(msgData))
+					if err != nil {
+						jww.ERROR.Printf("[WW] [%s] Failed to process message "+
+							"received from main thread: %+v", tm.name, err)
+					}
+					break
+				}
+				fallthrough
+
+			default:
+				jww.ERROR.Printf("[WW] [%s] Cannot handle data of type %s "+
+					"from main thread: %s",
+					tm.name, msgData.Type(), utils.JsToJson(msgData))
 			}
 		}
 	}
@@ -128,7 +139,7 @@ func (tm *ThreadManager) SendMessage(tag Tag, data []byte) {
 			"to main: %+v", tm.name, msg, tag, err)
 	}
 
-	go tm.postMessage(string(payload))
+	go tm.postMessage(payload)
 }
 
 // sendResponse sends a reply to the main thread with the given tag and ID.
@@ -151,14 +162,14 @@ func (tm *ThreadManager) sendResponse(tag Tag, id uint64, data []byte) error {
 			"%d going to main: %+v", msg, tag, id, err)
 	}
 
-	go tm.postMessage(string(payload))
+	go tm.postMessage(payload)
 
 	return nil
 }
 
 // receiveMessage is registered with the Javascript event listener and is called
 // every time a new message from the main thread is received.
-func (tm *ThreadManager) receiveMessage(data []byte) {
+func (tm *ThreadManager) receiveMessage(data js.Value) {
 	tm.receiveQueue <- data
 }
 
@@ -224,7 +235,7 @@ func (tm *ThreadManager) addEventListeners() {
 	// occurs when a message is received from the main thread.
 	// Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker/message_event
 	messageEvent := js.FuncOf(func(_ js.Value, args []js.Value) any {
-		tm.receiveMessage([]byte(args[0].Get("data").String()))
+		tm.receiveMessage(args[0].Get("data"))
 		return nil
 	})
 
@@ -233,8 +244,8 @@ func (tm *ThreadManager) addEventListeners() {
 	// Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker/error_event
 	errorEvent := js.FuncOf(func(_ js.Value, args []js.Value) any {
 		event := args[0]
-		jww.ERROR.Printf("[WW] [%s] Worker received error event: %s",
-			tm.name, utils.JsErrorToJson(event))
+		jww.ERROR.Printf("[WW] [%s] Worker received error event: %+v",
+			tm.name, js.Error{Value: event})
 		return nil
 	})
 
@@ -243,8 +254,8 @@ func (tm *ThreadManager) addEventListeners() {
 	// Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker/messageerror_event
 	messageerrorEvent := js.FuncOf(func(_ js.Value, args []js.Value) any {
 		event := args[0]
-		jww.ERROR.Printf("[WW] [%s] Worker received message error event: %s",
-			tm.name, utils.JsErrorToJson(event))
+		jww.ERROR.Printf("[WW] [%s] Worker received message error event: %+v",
+			tm.name, js.Error{Value: event})
 		return nil
 	})
 
@@ -260,10 +271,16 @@ func (tm *ThreadManager) addEventListeners() {
 // aMessage must be a js.Value or a primitive type that can be converted via
 // js.ValueOf. The Javascript object must be "any value or JavaScript object
 // handled by the structured clone algorithm". See the doc for more information.
+
+// aMessage is the object to deliver to the main thread; this will be in the
+// data field in the event delivered to the thread. It must be a transferable
+// object because this function transfers ownership of the message instead of
+// copying it for better performance. See the doc for more information.
 //
 // Doc: https://developer.mozilla.org/docs/Web/API/DedicatedWorkerGlobalScope/postMessage
-func (tm *ThreadManager) postMessage(aMessage any) {
-	js.Global().Call("postMessage", aMessage)
+func (tm *ThreadManager) postMessage(aMessage []byte) {
+	buffer := utils.CopyBytesToJS(aMessage)
+	js.Global().Call("postMessage", buffer, []any{buffer.Get("buffer")})
 }
 
 // close discards any tasks queued in the worker's event loop, effectively
diff --git a/worker/thread_test.go b/worker/thread_test.go
index ada6de8fc00916699b45eaee1483830a277b9fc9..d738580aad5d66ad4eab81ee934db52b24313825 100644
--- a/worker/thread_test.go
+++ b/worker/thread_test.go
@@ -20,7 +20,7 @@ func TestThreadManager_processReceivedMessage(t *testing.T) {
 	tm := &ThreadManager{callbacks: make(map[Tag]ThreadReceptionCallback)}
 
 	msg := Message{Tag: readyTag, ID: 5}
-	cbChan := make(chan struct{})
+	cbChan := make(chan struct{}, 1)
 	cb := func([]byte) ([]byte, error) { cbChan <- struct{}{}; return nil, nil }
 	tm.callbacks[msg.Tag] = cb
 
@@ -30,16 +30,16 @@ func TestThreadManager_processReceivedMessage(t *testing.T) {
 	}
 
 	go func() {
-		select {
-		case <-cbChan:
-		case <-time.After(10 * time.Millisecond):
-			t.Error("Timed out waiting for callback to be called.")
+		err = tm.processReceivedMessage(data)
+		if err != nil {
+			t.Errorf("Failed to receive message: %+v", err)
 		}
 	}()
 
-	err = tm.processReceivedMessage(data)
-	if err != nil {
-		t.Errorf("Failed to receive message: %+v", err)
+	select {
+	case <-cbChan:
+	case <-time.After(10 * time.Millisecond):
+		t.Error("Timed out waiting for callback to be called.")
 	}
 }
 
@@ -49,7 +49,7 @@ func TestThreadManager_RegisterCallback(t *testing.T) {
 	tm := &ThreadManager{callbacks: make(map[Tag]ThreadReceptionCallback)}
 
 	msg := Message{Tag: readyTag, ID: 5}
-	cbChan := make(chan struct{})
+	cbChan := make(chan struct{}, 1)
 	cb := func([]byte) ([]byte, error) { cbChan <- struct{}{}; return nil, nil }
 	tm.RegisterCallback(msg.Tag, cb)
 
@@ -59,15 +59,15 @@ func TestThreadManager_RegisterCallback(t *testing.T) {
 	}
 
 	go func() {
-		select {
-		case <-cbChan:
-		case <-time.After(10 * time.Millisecond):
-			t.Error("Timed out waiting for callback to be called.")
+		err = tm.processReceivedMessage(data)
+		if err != nil {
+			t.Errorf("Failed to receive message: %+v", err)
 		}
 	}()
 
-	err = tm.processReceivedMessage(data)
-	if err != nil {
-		t.Errorf("Failed to receive message: %+v", err)
+	select {
+	case <-cbChan:
+	case <-time.After(10 * time.Millisecond):
+		t.Error("Timed out waiting for callback to be called.")
 	}
 }