diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 7ab520cd02563b548eabe925202ef87324725e2c..f70c6360c2609120af3b32b7fad230c47e90d07e 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -38,11 +38,11 @@ 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"
+    - 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:
@@ -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..1567103e23775c56f6d6139b777d2e2274638ab8 100644
--- a/Makefile
+++ b/Makefile
@@ -11,6 +11,7 @@ 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
@@ -18,11 +19,12 @@ update_release:
 	GOFLAGS="" go get -d gitlab.com/elixxir/client/v4@release
 
 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 a48cbe4930867d8decbb48e2c2204ead5b11122e..a9ae4bdb98fe41ff0246255ae5d6fe5e44132a41 100644
--- a/go.mod
+++ b/go.mod
@@ -3,26 +3,32 @@ module gitlab.com/elixxir/xxdk-wasm
 go 1.19
 
 require (
+	github.com/aquilax/truncate v1.0.0
 	github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2
 	github.com/hack-pad/go-indexeddb v0.2.0
+	github.com/hack-pad/safejs v0.1.1
 	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.20230726212659-ac910c86f8ea
-	gitlab.com/elixxir/crypto v0.0.7-0.20230512203519-3aad22b6413b
-	gitlab.com/elixxir/primitives v0.0.3-0.20230724190035-efb1f377c08a
-	gitlab.com/xx_network/crypto v0.0.5-0.20230724190222-a1fd6f70e6cb
-	gitlab.com/xx_network/primitives v0.0.4-0.20230724185812-bc6fc6e5341b
+	github.com/stretchr/testify v1.8.2
+	gitlab.com/elixxir/client/v4 v4.6.4-0.20230724165849-04fc01e5b706
+	gitlab.com/elixxir/crypto v0.0.7-0.20230614183801-387e0cb8e76f
+	gitlab.com/elixxir/primitives v0.0.3-0.20230613193928-8cf8bdd777ef
+	gitlab.com/elixxir/wasm-utils v0.0.0-20230615222914-185dd3a6fa08
+	gitlab.com/xx_network/crypto v0.0.5-0.20230214003943-8a09396e95dd
+	gitlab.com/xx_network/primitives v0.0.4-0.20230522171102-940cdd68e516
 	golang.org/x/crypto v0.5.0
 )
 
 require (
 	filippo.io/edwards25519 v1.0.0 // indirect
 	git.xx.network/elixxir/grpc-web-go-client v0.0.0-20230214175953-5b5a8c33d28a // indirect
+	github.com/Max-Sum/base32768 v0.0.0-20230304063302-18e6ce5945fd // indirect
 	github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect
 	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
@@ -38,6 +44,7 @@ require (
 	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
@@ -49,6 +56,7 @@ 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
@@ -63,8 +71,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.20230310205528-f06faa0d2f0b // indirect
-	gitlab.com/elixxir/ekv v0.2.2 // indirect
+	gitlab.com/elixxir/comms v0.0.4-0.20230718154315-08043221466a // indirect
+	gitlab.com/elixxir/ekv v0.3.1-0.20230620180825-838848b00f19 // 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
@@ -73,7 +81,7 @@ require (
 	go.uber.org/atomic v1.10.0 // indirect
 	go.uber.org/ratelimit v0.2.0 // indirect
 	golang.org/x/net v0.5.0 // indirect
-	golang.org/x/sys v0.10.0 // indirect
+	golang.org/x/sys v0.4.0 // indirect
 	golang.org/x/text v0.6.0 // indirect
 	google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc // indirect
 	google.golang.org/grpc v1.49.0 // indirect
diff --git a/go.sum b/go.sum
index ac82ec52357b833fe7ea0e6f93daf6ac45f2fb8c..482c8d0854c09850ee533b6db2552c8ddd7c4b74 100644
--- a/go.sum
+++ b/go.sum
@@ -43,6 +43,8 @@ git.xx.network/elixxir/grpc-web-go-client v0.0.0-20230214175953-5b5a8c33d28a/go.
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
 github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
+github.com/Max-Sum/base32768 v0.0.0-20230304063302-18e6ce5945fd h1:nzE1YQBdx1bq9IlZinHa+HVffy+NmVRoKr+wHN8fpLE=
+github.com/Max-Sum/base32768 v0.0.0-20230304063302-18e6ce5945fd/go.mod h1:C8yoIfvESpM3GD07OCHU7fqI7lhwyZ2Td1rbNbTAhnc=
 github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
 github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
 github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
@@ -56,6 +58,8 @@ github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 h1:MzBOUgng9or
 github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129/go.mod h1:rFgpPQZYZ8vdbc+48xibu8ALc3yeyd64IhHS+PU6Yyg=
 github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
 github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
+github.com/aquilax/truncate v1.0.0 h1:UgIGS8U/aZ4JyOJ2h3xcF5cSQ06+gGBnjxH2RUHJe0U=
+github.com/aquilax/truncate v1.0.0/go.mod h1:BeMESIDMlvlS3bmg4BVvBbbZUNwWtS8uzYPAKXwwhLw=
 github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
 github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 h1:7Ip0wMmLHLRJdrloDxZfhMm0xrLXZS8+COSu2bXmEQs=
 github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
@@ -97,6 +101,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=
@@ -238,6 +243,8 @@ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgf
 github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
 github.com/hack-pad/go-indexeddb v0.2.0 h1:QHDM6gLrtCJvHdHUK8UdibJu4xWQlIDs4+l8L65AUdA=
 github.com/hack-pad/go-indexeddb v0.2.0/go.mod h1:NH8CaojufPNcKYDhy5JkjfyBXE/72oJPeiywlabN/lM=
+github.com/hack-pad/safejs v0.1.1 h1:d5qPO0iQ7h2oVtpzGnLExE+Wn9AtytxIfltcS2b9KD8=
+github.com/hack-pad/safejs v0.1.1/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio=
 github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE=
 github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
 github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -304,8 +311,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=
@@ -423,6 +434,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=
@@ -506,24 +519,30 @@ 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.20230726212659-ac910c86f8ea h1:ozegoQF2GZI03+KulCATRy4d3+dcB5gRB7A5DOn+/HU=
-gitlab.com/elixxir/client/v4 v4.6.4-0.20230726212659-ac910c86f8ea/go.mod h1:4Xne/cpHWT3rJI1HshYpwzr50scQHKnPSv+dckm9VZo=
-gitlab.com/elixxir/comms v0.0.4-0.20230310205528-f06faa0d2f0b h1:8AVK93UEs/aufoqtFgyMVt9gf0oJ8F4pA60ZvEVvG+s=
-gitlab.com/elixxir/comms v0.0.4-0.20230310205528-f06faa0d2f0b/go.mod h1:z+qW0D9VpY5QKTd7wRlb5SK4kBNqLYsa4DXBcUXue9Q=
-gitlab.com/elixxir/crypto v0.0.7-0.20230512203519-3aad22b6413b h1:+3iFGyrfjEbzIg3hoGJkDJAbTlqaMx1PuOth7HcL2yk=
-gitlab.com/elixxir/crypto v0.0.7-0.20230512203519-3aad22b6413b/go.mod h1:/SLOlvkYVVJf6IU+vEjMLnS7cjjcoTlPV45g6tv6INc=
-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/primitives v0.0.3-0.20230724190035-efb1f377c08a h1:hPp0oYlRM8GJseskzBb2Xr9zEzQRNjkiQg3n7PfpZ/A=
-gitlab.com/elixxir/primitives v0.0.3-0.20230724190035-efb1f377c08a/go.mod h1:0KOMjw62km9QI4Ow/XNJqg4P+gu1OQUGDhHM5gMYGS8=
+gitlab.com/elixxir/client/v4 v4.6.4-0.20230630173422-ac9c5506b1fb h1:991u/qqqP2FI3MaH5y7MZE60HsXKXJ4lqCd2dYJcS88=
+gitlab.com/elixxir/client/v4 v4.6.4-0.20230630173422-ac9c5506b1fb/go.mod h1:wSeJ9pk+qqUrKHwhd4qZW1CnNlakK75n+1fOjJ7k1Ns=
+gitlab.com/elixxir/client/v4 v4.6.4-0.20230717200544-5e105cfcd312 h1:SHiuZDKobnDyL8XPWxfiQnpgPovwv+hTdtx86voIu/U=
+gitlab.com/elixxir/client/v4 v4.6.4-0.20230717200544-5e105cfcd312/go.mod h1:wSeJ9pk+qqUrKHwhd4qZW1CnNlakK75n+1fOjJ7k1Ns=
+gitlab.com/elixxir/client/v4 v4.6.4-0.20230724165849-04fc01e5b706 h1:cB66N82jdeARHVQBYcqu7IwcyvBlK5zUJMRi7Y3vy08=
+gitlab.com/elixxir/client/v4 v4.6.4-0.20230724165849-04fc01e5b706/go.mod h1:taeAIsjcAgdK+HytutNR8jabSSmiHaliCAnQk8UPvnc=
+gitlab.com/elixxir/comms v0.0.4-0.20230613220741-7de1d2ca4a1c h1:0TpLn4AdarrqCwUMvnz4Md+9gLyk9wrQ73J3W9U5zJo=
+gitlab.com/elixxir/comms v0.0.4-0.20230613220741-7de1d2ca4a1c/go.mod h1:z+qW0D9VpY5QKTd7wRlb5SK4kBNqLYsa4DXBcUXue9Q=
+gitlab.com/elixxir/comms v0.0.4-0.20230718154315-08043221466a h1:+cG6UF7WDW5aFX/xOoLa77k765HnKqxT/2LSDd70nKk=
+gitlab.com/elixxir/comms v0.0.4-0.20230718154315-08043221466a/go.mod h1:z+qW0D9VpY5QKTd7wRlb5SK4kBNqLYsa4DXBcUXue9Q=
+gitlab.com/elixxir/crypto v0.0.7-0.20230614183801-387e0cb8e76f h1:T0Jvhq5nCELiwkVr07Ti/Ew9ICdexviYeCkFV19kk9A=
+gitlab.com/elixxir/crypto v0.0.7-0.20230614183801-387e0cb8e76f/go.mod h1:lAib0KO9TeTLWbwgFk2uszRxPkHeu843xqnYdkzdEB0=
+gitlab.com/elixxir/ekv v0.3.1-0.20230620180825-838848b00f19 h1:GZLTKoxr6r5QhG5BvZoQeImeXrpnn03EhG8RTxrLyl0=
+gitlab.com/elixxir/ekv v0.3.1-0.20230620180825-838848b00f19/go.mod h1:8sVMOf3GFQCnd3z8Nnlg8Udno+Lcc1qNXxleblhzXTA=
+gitlab.com/elixxir/primitives v0.0.3-0.20230613193928-8cf8bdd777ef h1:31Uavv2Y1lNnPsxPe8lAsJClf7h6y9c3NrqMIvA9ae8=
+gitlab.com/elixxir/primitives v0.0.3-0.20230613193928-8cf8bdd777ef/go.mod h1:phun4PLkHJA6wcL4JIhhxZztrmCyJHWPNppBP3DUD2Y=
+gitlab.com/elixxir/wasm-utils v0.0.0-20230615222914-185dd3a6fa08 h1:7Uqf6CHEBj/q1CIHEwnWcYLFG5cpMtH8oyVYGikgYWo=
+gitlab.com/elixxir/wasm-utils v0.0.0-20230615222914-185dd3a6fa08/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.20230724190222-a1fd6f70e6cb h1:bGcPR0oeiUgGK264QE5mXzIttjeqQ8+kJaspOuY74dc=
-gitlab.com/xx_network/crypto v0.0.5-0.20230724190222-a1fd6f70e6cb/go.mod h1:Q1tnwsNnopT0L35VAtlLizf08///cBR8MFIbuZtEYng=
-gitlab.com/xx_network/primitives v0.0.4-0.20230724185812-bc6fc6e5341b h1:oymmRpA+5/SeBp+MgFeYyuB8S9agfetGDnxBXXq9utE=
-gitlab.com/xx_network/primitives v0.0.4-0.20230724185812-bc6fc6e5341b/go.mod h1:vI6JXexgqihcIVFGsZAwGlHWGT14XC24NwEB2c6q9nc=
+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.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=
@@ -729,8 +748,8 @@ golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
-golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
+golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -912,6 +931,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 205a5e0df78b3cb6125042a963e5de8026fd4e08..8eeadabeb2a61236bded58a5abb77d9d263d27a3 100644
--- a/indexedDb/impl/channels/callbacks.go
+++ b/indexedDb/impl/channels/callbacks.go
@@ -10,21 +10,23 @@
 package main
 
 import (
-	"crypto/ed25519"
 	"encoding/json"
+	"time"
+
 	"github.com/pkg/errors"
 	jww "github.com/spf13/jwalterweatherman"
+
 	"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/fastRNG"
+	idbCrypto "gitlab.com/elixxir/crypto/indexedDb"
 	"gitlab.com/elixxir/crypto/message"
+	"gitlab.com/elixxir/wasm-utils/exception"
 	wChannels "gitlab.com/elixxir/xxdk-wasm/indexedDb/worker/channels"
 	"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}
@@ -45,8 +47,8 @@ func (m *manager) registerCallbacks() {
 	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.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)
@@ -54,119 +56,100 @@ func (m *manager) registerCallbacks() {
 
 // newWASMEventModelCB is the callback for NewWASMEventModel. Returns an empty
 // slice on success or an error message on failure.
-func (m *manager) newWASMEventModelCB(data []byte) ([]byte, error) {
+func (m *manager) newWASMEventModelCB(message []byte, reply func(message []byte)) {
 	var msg wChannels.NewWASMEventModelMessage
-	err := json.Unmarshal(data, &msg)
+	err := json.Unmarshal(message, &msg)
 	if err != nil {
-		return []byte{}, errors.Errorf(
-			"failed to JSON unmarshal %T from main thread: %+v", msg, err)
+		reply([]byte(errors.Wrapf(err,
+			"failed to JSON unmarshal %T from main thread", msg).Error()))
+		return
 	}
 
 	// Create new encryption cipher
 	rng := fastRNG.NewStreamGenerator(12, 1024, csprng.NewSystemRNG)
-	encryption, err := cryptoChannel.NewCipherFromJSON(
+	encryption, err := idbCrypto.NewCipherFromJSON(
 		[]byte(msg.EncryptionJSON), rng.GetStream())
 	if err != nil {
-		return []byte{}, errors.Errorf(
-			"failed to JSON unmarshal Cipher from main thread: %+v", err)
+		reply([]byte(errors.Wrap(err,
+			"failed to JSON unmarshal Cipher from main thread").Error()))
+		return
 	}
 
-	m.model, err = NewWASMEventModel(msg.DatabaseName, encryption,
-		m.messageReceivedCallback, m.deletedMessageCallback, m.mutedUserCallback)
+	m.model, err = NewWASMEventModel(
+		msg.DatabaseName, encryption, m.eventUpdateCallback)
 	if err != nil {
-		return []byte(err.Error()), nil
+		reply([]byte(err.Error()))
+		return
 	}
 
-	return []byte{}, nil
+	reply(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)
+// eventUpdateCallback JSON marshals the interface and sends it to the main
+// thread the with the event type to be sent on the EventUpdate callback.
+func (m *manager) eventUpdateCallback(eventType int64, jsonMarshallable any) {
+	jsonData, err := json.Marshal(jsonMarshallable)
 	if err != nil {
-		jww.ERROR.Printf("Could not JSON marshal %T: %+v", msg, err)
-		return
+		jww.FATAL.Panicf("[CH] Failed to JSON marshal %T for EventUpdate "+
+			"callback: %+v", jsonMarshallable, err)
 	}
 
-	// Send it to the main thread
-	m.wtm.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.wtm.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) {
 	// 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 {
-		jww.ERROR.Printf("Could not JSON marshal %T: %+v", msg, err)
-		return
+		exception.Throwf("[CH] Could not JSON marshal %T for EventUpdate "+
+			"callback: %+v", msg, err)
 	}
 
 	// Send it to the main thread
-	m.wtm.SendMessage(wChannels.MutedUserCallbackTag, data)
+	err = m.wtm.SendNoResponse(wChannels.EventUpdateCallbackTag, data)
+	if err != nil {
+		exception.Throwf(
+			"[CH] Could not send message for EventUpdate callback: %+v", err)
+	}
 }
 
 // joinChannelCB is the callback for wasmModel.JoinChannel. Always returns nil;
 // meaning, no response is supplied (or expected).
-func (m *manager) joinChannelCB(data []byte) ([]byte, error) {
+func (m *manager) joinChannelCB(message []byte, _ func([]byte)) {
 	var channel cryptoBroadcast.Channel
-	err := json.Unmarshal(data, &channel)
+	err := json.Unmarshal(message, &channel)
 	if err != nil {
-		return nil, errors.Errorf(
-			"failed to JSON unmarshal %T from main thread: %+v", channel, err)
+		jww.ERROR.Printf("[CH] Could not JSON unmarshal %T from main thread: "+
+			"%+v", channel, err)
+		return
 	}
 
 	m.model.JoinChannel(&channel)
-	return nil, nil
 }
 
 // leaveChannelCB is the callback for wasmModel.LeaveChannel. Always returns
 // nil; meaning, no response is supplied (or expected).
-func (m *manager) leaveChannelCB(data []byte) ([]byte, error) {
-	channelID, err := id.Unmarshal(data)
+func (m *manager) leaveChannelCB(message []byte, _ func([]byte)) {
+	channelID, err := id.Unmarshal(message)
 	if err != nil {
-		return nil, errors.Errorf(
-			"failed to JSON unmarshal %T from main thread: %+v", channelID, err)
+		jww.ERROR.Printf("[CH] Could not JSON unmarshal %T from main thread: "+
+			"%+v", channelID, err)
+		return
 	}
 
 	m.model.LeaveChannel(channelID)
-	return nil, nil
 }
 
 // receiveMessageCB is the callback for wasmModel.ReceiveMessage. Returns a UUID
 // of 0 on error or the JSON marshalled UUID (uint64) on success.
-func (m *manager) receiveMessageCB(data []byte) ([]byte, error) {
+func (m *manager) receiveMessageCB(message []byte, reply func(message []byte)) {
 	var msg channels.ModelMessage
-	err := json.Unmarshal(data, &msg)
+	err := json.Unmarshal(message, &msg)
 	if err != nil {
-		return zeroUUID, errors.Errorf(
-			"failed to JSON unmarshal %T from main thread: %+v", msg, err)
+		jww.ERROR.Printf("[CH] Could not JSON unmarshal payload for "+
+			"ReceiveMessage from main thread: %+v", err)
+		reply(zeroUUID)
+		return
 	}
 
 	uuid := m.model.ReceiveMessage(msg.ChannelID, msg.MessageID, msg.Nickname,
@@ -174,21 +157,25 @@ func (m *manager) receiveMessageCB(data []byte) ([]byte, error) {
 		msg.Timestamp, msg.Lease, rounds.Round{ID: msg.Round}, msg.Type,
 		msg.Status, msg.Hidden)
 
-	uuidData, err := json.Marshal(uuid)
+	replyMsg, err := json.Marshal(uuid)
 	if err != nil {
-		return zeroUUID, errors.Errorf("failed to JSON marshal UUID: %+v", err)
+		exception.Throwf(
+			"[CH] Could not JSON marshal UUID for ReceiveMessage: %+v", err)
 	}
-	return uuidData, nil
+
+	reply(replyMsg)
 }
 
 // receiveReplyCB is the callback for wasmModel.ReceiveReply. Returns a UUID of
 // 0 on error or the JSON marshalled UUID (uint64) on success.
-func (m *manager) receiveReplyCB(data []byte) ([]byte, error) {
+func (m *manager) receiveReplyCB(message []byte, reply func(message []byte)) {
 	var msg wChannels.ReceiveReplyMessage
-	err := json.Unmarshal(data, &msg)
+	err := json.Unmarshal(message, &msg)
 	if err != nil {
-		return zeroUUID, errors.Errorf(
-			"failed to JSON unmarshal %T from main thread: %+v", msg, err)
+		jww.ERROR.Printf("[CH] Could not JSON unmarshal payload for "+
+			"ReceiveReply from main thread: %+v", err)
+		reply(zeroUUID)
+		return
 	}
 
 	uuid := m.model.ReceiveReply(msg.ChannelID, msg.MessageID, msg.ReactionTo,
@@ -196,21 +183,25 @@ func (m *manager) receiveReplyCB(data []byte) ([]byte, error) {
 		msg.CodesetVersion, msg.Timestamp, msg.Lease,
 		rounds.Round{ID: msg.Round}, msg.Type, msg.Status, msg.Hidden)
 
-	uuidData, err := json.Marshal(uuid)
+	replyMsg, err := json.Marshal(uuid)
 	if err != nil {
-		return zeroUUID, errors.Errorf("failed to JSON marshal UUID: %+v", err)
+		exception.Throwf(
+			"[CH] Could not JSON marshal UUID for ReceiveReply: %+v", err)
 	}
-	return uuidData, nil
+
+	reply(replyMsg)
 }
 
 // receiveReactionCB is the callback for wasmModel.ReceiveReaction. Returns a
 // UUID of 0 on error or the JSON marshalled UUID (uint64) on success.
-func (m *manager) receiveReactionCB(data []byte) ([]byte, error) {
+func (m *manager) receiveReactionCB(message []byte, reply func(message []byte)) {
 	var msg wChannels.ReceiveReplyMessage
-	err := json.Unmarshal(data, &msg)
+	err := json.Unmarshal(message, &msg)
 	if err != nil {
-		return zeroUUID, errors.Errorf(
-			"failed to JSON unmarshal %T from main thread: %+v", msg, err)
+		jww.ERROR.Printf("[CH] Could not JSON unmarshal payload for "+
+			"ReceiveReaction from main thread: %+v", err)
+		reply(zeroUUID)
+		return
 	}
 
 	uuid := m.model.ReceiveReaction(msg.ChannelID, msg.MessageID,
@@ -218,22 +209,26 @@ func (m *manager) receiveReactionCB(data []byte) ([]byte, error) {
 		msg.DmToken, msg.CodesetVersion, msg.Timestamp, msg.Lease,
 		rounds.Round{ID: msg.Round}, msg.Type, msg.Status, msg.Hidden)
 
-	uuidData, err := json.Marshal(uuid)
+	replyMsg, err := json.Marshal(uuid)
 	if err != nil {
-		return zeroUUID, errors.Errorf("failed to JSON marshal UUID: %+v", err)
+		exception.Throwf(
+			"[CH] Could not JSON marshal UUID for ReceiveReaction: %+v", err)
 	}
-	return uuidData, nil
+
+	reply(replyMsg)
 }
 
-// updateFromUUIDCB is the callback for wasmModel.UpdateFromUUID. Always returns
+// updateFromUuidCB is the callback for wasmModel.UpdateFromUUID. Always returns
 // nil; meaning, no response is supplied (or expected).
-func (m *manager) updateFromUUIDCB(data []byte) ([]byte, error) {
+func (m *manager) updateFromUuidCB(messageData []byte, reply func(message []byte)) {
 	var msg wChannels.MessageUpdateInfo
-	err := json.Unmarshal(data, &msg)
+	err := json.Unmarshal(messageData, &msg)
 	if err != nil {
-		return nil, errors.Errorf(
-			"failed to JSON unmarshal %T from main thread: %+v", msg, err)
+		reply([]byte(errors.Errorf("failed to JSON unmarshal %T from main "+
+			"thread: %+v", msg, err).Error()))
+		return
 	}
+
 	var messageID *message.ID
 	var timestamp *time.Time
 	var round *rounds.Round
@@ -261,20 +256,31 @@ func (m *manager) updateFromUUIDCB(data []byte) ([]byte, error) {
 	err = m.model.UpdateFromUUID(
 		msg.UUID, messageID, timestamp, round, pinned, hidden, status)
 	if err != nil {
-		return []byte(err.Error()), nil
+		reply([]byte(err.Error()))
 	}
 
-	return nil, nil
+	reply(nil)
 }
 
-// updateFromMessageIDCB is the callback for wasmModel.UpdateFromMessageID.
+// updateFromMessageIdCB is the callback for wasmModel.UpdateFromMessageID.
 // Always returns nil; meaning, no response is supplied (or expected).
-func (m *manager) updateFromMessageIDCB(data []byte) ([]byte, error) {
+func (m *manager) updateFromMessageIdCB(message []byte, reply func(message []byte)) {
+	var ue wChannels.UuidError
+	defer func() {
+		if replyMessage, err := json.Marshal(ue); err != nil {
+			exception.Throwf("[CH] Failed to JSON marshal %T for "+
+				"UpdateFromMessageID: %+v", ue, err)
+		} else {
+			reply(replyMessage)
+		}
+	}()
+
 	var msg wChannels.MessageUpdateInfo
-	err := json.Unmarshal(data, &msg)
+	err := json.Unmarshal(message, &msg)
 	if err != nil {
-		return nil, errors.Errorf(
-			"failed to JSON unmarshal %T from main thread: %+v", msg, err)
+		ue.Error = errors.Errorf(
+			"failed to JSON unmarshal %T from main thread: %+v", msg, err).Error()
+		return
 	}
 	var timestamp *time.Time
 	var round *rounds.Round
@@ -296,79 +302,72 @@ func (m *manager) updateFromMessageIDCB(data []byte) ([]byte, error) {
 		status = &msg.Status
 	}
 
-	var ue wChannels.UuidError
 	uuid, err := m.model.UpdateFromMessageID(
 		msg.MessageID, timestamp, round, pinned, hidden, status)
 	if err != nil {
-		ue.Error = []byte(err.Error())
+		ue.Error = err.Error()
 	} else {
 		ue.UUID = uuid
 	}
-
-	data, err = json.Marshal(ue)
-	if err != nil {
-		return nil, errors.Errorf("failed to JSON marshal %T: %+v", ue, err)
-	}
-
-	return data, nil
 }
 
 // getMessageCB is the callback for wasmModel.GetMessage. Returns JSON
 // marshalled channels.GetMessageMessage. If an error occurs, then Error will
 // be set with the error message. Otherwise, Message will be set. Only one field
 // will be set.
-func (m *manager) getMessageCB(data []byte) ([]byte, error) {
-	messageID, err := message.UnmarshalID(data)
+func (m *manager) getMessageCB(messageData []byte, reply func(message []byte)) {
+	var replyMsg wChannels.GetMessageMessage
+	defer func() {
+		if replyMessage, err := json.Marshal(replyMsg); err != nil {
+			exception.Throwf("[CH] Failed to JSON marshal %T for "+
+				"GetMessage: %+v", replyMsg, err)
+		} else {
+			reply(replyMessage)
+		}
+	}()
+
+	messageID, err := message.UnmarshalID(messageData)
 	if err != nil {
-		return nil, errors.Errorf(
-			"failed to JSON unmarshal %T from main thread: %+v", messageID, err)
+		replyMsg.Error = errors.Errorf("failed to JSON unmarshal %T from "+
+			"main thread: %+v", messageID, err).Error()
+		return
 	}
 
-	reply := wChannels.GetMessageMessage{}
-
 	msg, err := m.model.GetMessage(messageID)
 	if err != nil {
-		reply.Error = err.Error()
+		replyMsg.Error = err.Error()
 	} else {
-		reply.Message = msg
+		replyMsg.Message = msg
 	}
-
-	messageData, err := json.Marshal(reply)
-	if err != nil {
-		return nil, errors.Errorf("failed to JSON marshal %T from main thread "+
-			"for GetMessage reply: %+v", reply, err)
-	}
-	return messageData, nil
 }
 
 // deleteMessageCB is the callback for wasmModel.DeleteMessage. Always returns
 // nil; meaning, no response is supplied (or expected).
-func (m *manager) deleteMessageCB(data []byte) ([]byte, error) {
-	messageID, err := message.UnmarshalID(data)
+func (m *manager) deleteMessageCB(messageData []byte, reply func(message []byte)) {
+	messageID, err := message.UnmarshalID(messageData)
 	if err != nil {
-		return nil, errors.Errorf(
-			"failed to JSON unmarshal %T from main thread: %+v", messageID, err)
+		reply([]byte(errors.Errorf("failed to JSON unmarshal %T from main "+
+			"thread: %+v", messageID, err).Error()))
+		return
 	}
 
 	err = m.model.DeleteMessage(messageID)
 	if err != nil {
-		return []byte(err.Error()), nil
+		reply([]byte(err.Error()))
 	}
 
-	return nil, nil
+	reply(nil)
 }
 
 // muteUserCB is the callback for wasmModel.MuteUser. Always returns nil;
 // meaning, no response is supplied (or expected).
-func (m *manager) muteUserCB(data []byte) ([]byte, error) {
+func (m *manager) muteUserCB(message []byte, _ func([]byte)) {
 	var msg wChannels.MuteUserMessage
-	err := json.Unmarshal(data, &msg)
+	err := json.Unmarshal(message, &msg)
 	if err != nil {
-		return nil, errors.Errorf(
-			"failed to JSON unmarshal %T from main thread: %+v", msg, err)
+		jww.ERROR.Printf("[CH] Could not JSON unmarshal %T for MuteUser from "+
+			"main thread: %+v", msg, err)
+		return
 	}
-
 	m.model.MuteUser(msg.ChannelID, msg.PubKey, msg.Unmute)
-
-	return nil, nil
 }
diff --git a/indexedDb/impl/channels/channelsIndexedDbWorker.js b/indexedDb/impl/channels/channelsIndexedDbWorker.js
index c109cba89735425f23c0d4d436532a3e3eb9a852..4ca8fa6c5d5910ed80b200efa00f2f2fa7d3073f 100644
--- a/indexedDb/impl/channels/channelsIndexedDbWorker.js
+++ b/indexedDb/impl/channels/channelsIndexedDbWorker.js
@@ -12,6 +12,10 @@ const isReady = new Promise((resolve) => {
 });
 
 const go = new Go();
+go.argv = [
+    '--logLevel=2',
+    '--threadLogLevel=2',
+]
 const binPath = 'xxdk-channelsIndexedDkWorker.wasm'
 WebAssembly.instantiateStreaming(fetch(binPath), go.importObject).then(async (result) => {
     go.run(result.instance);
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 0976d263b00011b7ec4a6a5d6bfce38c2780beb0..8980a4bbd337b0993bd4b9963f6d89cb326b10f6 100644
--- a/indexedDb/impl/channels/implementation.go
+++ b/indexedDb/impl/channels/implementation.go
@@ -12,6 +12,7 @@ package main
 import (
 	"crypto/ed25519"
 	"encoding/json"
+	"gitlab.com/elixxir/client/v4/bindings"
 	"strconv"
 	"strings"
 	"syscall/js"
@@ -24,25 +25,20 @@ import (
 	"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"
+	idbCrypto "gitlab.com/elixxir/crypto/indexedDb"
 	"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        idbCrypto.Cipher
+	eventCallback eventUpdate
 }
 
 // JoinChannel is called whenever a channel is joined locally.
@@ -149,21 +145,22 @@ func (w *wasmModel) ReceiveMessage(channelID *id.ID, messageID message.ID,
 	nickname, text string, pubKey ed25519.PublicKey, dmToken uint32,
 	codeset uint8, timestamp time.Time, lease time.Duration, round rounds.Round,
 	mType channels.MessageType, status channels.SentStatus, hidden bool) uint64 {
-	textBytes := []byte(text)
 	var err error
 
 	// Handle encryption, if it is present
 	if w.cipher != nil {
-		textBytes, err = w.cipher.Encrypt([]byte(text))
+		text, err = w.cipher.Encrypt([]byte(text))
 		if err != nil {
 			jww.ERROR.Printf("Failed to encrypt Message: %+v", err)
 			return 0
 		}
 	}
 
+	channelIDBytes := channelID.Marshal()
+
 	msgToInsert := buildMessage(
-		channelID.Marshal(), messageID.Bytes(), nil, nickname,
-		textBytes, pubKey, dmToken, codeset, timestamp, lease, round.ID, mType,
+		channelIDBytes, messageID.Bytes(), nil, nickname,
+		text, pubKey, dmToken, codeset, timestamp, lease, round.ID, mType,
 		false, hidden, status)
 
 	uuid, err := w.upsertMessage(msgToInsert)
@@ -172,7 +169,11 @@ func (w *wasmModel) ReceiveMessage(channelID *id.ID, messageID message.ID,
 		return 0
 	}
 
-	go w.receivedMessageCB(uuid, channelID, false)
+	go w.eventCallback(bindings.MessageReceived, bindings.MessageReceivedJSON{
+		UUID:      int64(uuid),
+		ChannelID: channelID,
+		Update:    false,
+	})
 	return uuid
 }
 
@@ -187,20 +188,21 @@ func (w *wasmModel) ReceiveReply(channelID *id.ID, messageID,
 	dmToken uint32, codeset uint8, timestamp time.Time, lease time.Duration,
 	round rounds.Round, mType channels.MessageType, status channels.SentStatus,
 	hidden bool) uint64 {
-	textBytes := []byte(text)
 	var err error
 
 	// Handle encryption, if it is present
 	if w.cipher != nil {
-		textBytes, err = w.cipher.Encrypt([]byte(text))
+		text, err = w.cipher.Encrypt([]byte(text))
 		if err != nil {
 			jww.ERROR.Printf("Failed to encrypt Message: %+v", err)
 			return 0
 		}
 	}
 
-	msgToInsert := buildMessage(channelID.Marshal(), messageID.Bytes(),
-		replyTo.Bytes(), nickname, textBytes, pubKey, dmToken, codeset,
+	channelIDBytes := channelID.Marshal()
+
+	msgToInsert := buildMessage(channelIDBytes, messageID.Bytes(),
+		replyTo.Bytes(), nickname, text, pubKey, dmToken, codeset,
 		timestamp, lease, round.ID, mType, hidden, false, status)
 
 	uuid, err := w.upsertMessage(msgToInsert)
@@ -208,7 +210,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.eventCallback(bindings.MessageReceived, bindings.MessageReceivedJSON{
+		UUID:      int64(uuid),
+		ChannelID: channelID,
+		Update:    false,
+	})
 	return uuid
 }
 
@@ -223,21 +230,21 @@ func (w *wasmModel) ReceiveReaction(channelID *id.ID, messageID,
 	dmToken uint32, codeset uint8, timestamp time.Time, lease time.Duration,
 	round rounds.Round, mType channels.MessageType, status channels.SentStatus,
 	hidden bool) uint64 {
-	textBytes := []byte(reaction)
 	var err error
 
 	// Handle encryption, if it is present
 	if w.cipher != nil {
-		textBytes, err = w.cipher.Encrypt([]byte(reaction))
+		reaction, err = w.cipher.Encrypt([]byte(reaction))
 		if err != nil {
 			jww.ERROR.Printf("Failed to encrypt Message: %+v", err)
 			return 0
 		}
 	}
 
+	channelIDBytes := channelID.Marshal()
 	msgToInsert := buildMessage(
-		channelID.Marshal(), messageID.Bytes(), reactionTo.Bytes(), nickname,
-		textBytes, pubKey, dmToken, codeset, timestamp, lease, round.ID, mType,
+		channelIDBytes, messageID.Bytes(), reactionTo.Bytes(), nickname,
+		reaction, pubKey, dmToken, codeset, timestamp, lease, round.ID, mType,
 		false, hidden, status)
 
 	uuid, err := w.upsertMessage(msgToInsert)
@@ -245,7 +252,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.eventCallback(bindings.MessageReceived, bindings.MessageReceivedJSON{
+		UUID:      int64(uuid),
+		ChannelID: channelID,
+		Update:    false,
+	})
 	return uuid
 }
 
@@ -334,8 +346,8 @@ func (w *wasmModel) UpdateFromMessageID(messageID message.ID,
 // NOTE: ID is not set inside this function because we want to use the
 // autoincrement key by default. If you are trying to overwrite an existing
 // message, then you need to set it manually yourself.
-func buildMessage(channelID, messageID, parentID []byte, nickname string,
-	text []byte, pubKey ed25519.PublicKey, dmToken uint32, codeset uint8,
+func buildMessage(channelID, messageID, parentID []byte, nickname,
+	text string, pubKey ed25519.PublicKey, dmToken uint32, codeset uint8,
 	timestamp time.Time, lease time.Duration, round id.Round,
 	mType channels.MessageType, pinned, hidden bool,
 	status channels.SentStatus) *Message {
@@ -392,9 +404,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.eventCallback(bindings.MessageReceived, bindings.MessageReceivedJSON{
+		UUID:      int64(uuid),
+		ChannelID: channelID,
+		Update:    true,
+	})
 
 	return uuid, nil
 }
@@ -415,6 +435,26 @@ func (w *wasmModel) upsertMessage(msg *Message) (uint64, error) {
 	// Store message to database
 	msgIdObj, err := impl.Put(w.db, messageStoreName, messageObj)
 	if err != nil {
+		// Do not error out when this message already exists inside
+		// the DB. Instead, set the ID and re-attempt as an update.
+		if msg.ID == 0 { // always error out when not an insert attempt
+			msgID, inErr := message.UnmarshalID(msg.MessageID)
+			if inErr == nil {
+				jww.WARN.Printf("upsertMessage duplicate: %+v",
+					err)
+				rnd := &rounds.Round{ID: id.Round(msg.Round)}
+				status := (*channels.SentStatus)(&msg.Status)
+				return w.UpdateFromMessageID(msgID,
+					&msg.Timestamp,
+					rnd,
+					&msg.Pinned,
+					&msg.Hidden,
+					status)
+			}
+			// Add this to the main putMessage error
+			err = errors.Wrapf(err, "bad msg ID: %+v",
+				inErr)
+		}
 		return 0, errors.Errorf("Unable to put Message: %+v\n%s",
 			err, newMessageJson)
 	}
@@ -476,7 +516,7 @@ func (w *wasmModel) GetMessage(
 		Status:          channels.SentStatus(lookupResult.Status),
 		Hidden:          lookupResult.Hidden,
 		Pinned:          lookupResult.Pinned,
-		Content:         lookupResult.Text,
+		Content:         []byte(lookupResult.Text),
 		Type:            channels.MessageType(lookupResult.Type),
 		Round:           id.Round(lookupResult.Round),
 		PubKey:          lookupResult.Pubkey,
@@ -492,7 +532,9 @@ func (w *wasmModel) DeleteMessage(messageID message.ID) error {
 		return err
 	}
 
-	go w.deletedMessageCB(messageID)
+	go w.eventCallback(bindings.MessageDeleted, bindings.MessageDeletedJSON{
+		MessageID: messageID,
+	})
 
 	return nil
 }
@@ -500,7 +542,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.eventCallback(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..4f52e0ff707de1a611384c41e816613ebd39638a 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"
+	idbCrypto "gitlab.com/elixxir/crypto/indexedDb"
 	"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,12 @@ 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) {}
+var dummyEU = func(int64, any) {}
 
 // 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, dummyEU)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -121,12 +118,12 @@ func TestWasmModel_ReceiveFile(t *testing.T) {
 
 // Happy path, insert message and look it up
 func TestWasmModel_GetMessage(t *testing.T) {
-	cipher, err := cryptoChannel.NewCipher(
+	cipher, err := idbCrypto.NewCipher(
 		[]byte("testPass"), []byte("testSalt"), 128, csprng.NewSystemRNG())
 	if err != nil {
 		t.Fatalf("Failed to create cipher")
 	}
-	for _, c := range []cryptoChannel.Cipher{nil, cipher} {
+	for _, c := range []idbCrypto.Cipher{nil, cipher} {
 		cs := ""
 		if c != nil {
 			cs = "_withCipher"
@@ -136,14 +133,13 @@ func TestWasmModel_GetMessage(t *testing.T) {
 			storage.GetLocalStorage().Clear()
 			testMsgId := message.DeriveChannelMessageID(&id.ID{1}, 0, []byte(testString))
 
-			eventModel, err := newWASMModel(testString, c,
-				dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB)
+			eventModel, err := newWASMModel(testString, c, dummyEU)
 			if err != nil {
 				t.Fatal(err)
 			}
 
 			testMsg := buildMessage(id.NewIdFromBytes([]byte(testString), t).Marshal(),
-				testMsgId.Bytes(), nil, testString, []byte(testString),
+				testMsgId.Bytes(), nil, testString, testString,
 				[]byte{8, 6, 7, 5}, 0, 0, netTime.Now(),
 				time.Second, 0, 0, false, false, channels.Sent)
 			_, err = eventModel.upsertMessage(testMsg)
@@ -167,15 +163,14 @@ 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, dummyEU)
 	if err != nil {
 		t.Fatal(err)
 	}
 
 	// Insert a message
 	testMsg := buildMessage([]byte(testString), testMsgId.Bytes(), nil,
-		testString, []byte(testString), []byte{8, 6, 7, 5}, 0, 0, netTime.Now(),
+		testString, testString, []byte{8, 6, 7, 5}, 0, 0, netTime.Now(),
 		time.Second, 0, 0, false, false, channels.Sent)
 	_, err = eventModel.upsertMessage(testMsg)
 	if err != nil {
@@ -209,12 +204,12 @@ func TestWasmModel_DeleteMessage(t *testing.T) {
 
 // Test wasmModel.UpdateSentStatus happy path and ensure fields don't change.
 func Test_wasmModel_UpdateSentStatus(t *testing.T) {
-	cipher, err := cryptoChannel.NewCipher(
+	cipher, err := idbCrypto.NewCipher(
 		[]byte("testPass"), []byte("testSalt"), 128, csprng.NewSystemRNG())
 	if err != nil {
 		t.Fatalf("Failed to create cipher")
 	}
-	for _, c := range []cryptoChannel.Cipher{nil, cipher} {
+	for _, c := range []idbCrypto.Cipher{nil, cipher} {
 		cs := ""
 		if c != nil {
 			cs = "_withCipher"
@@ -224,15 +219,18 @@ func Test_wasmModel_UpdateSentStatus(t *testing.T) {
 			testString := "Test_wasmModel_UpdateSentStatus" + cs
 			testMsgId := message.DeriveChannelMessageID(
 				&id.ID{1}, 0, []byte(testString))
-			eventModel, err2 := newWASMModel(testString, c,
-				dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB)
+			eventModel, err2 := newWASMModel(testString, c, dummyEU)
 			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,
-				testString, []byte(testString), []byte{8, 6, 7, 5}, 0, 0,
+			testMsg := buildMessage(cid.Bytes(), testMsgId.Bytes(), nil,
+				testString, testString, []byte{8, 6, 7, 5}, 0, 0,
 				netTime.Now(), time.Second, 0, 0, false, false, channels.Sent)
 			uuid, err2 := eventModel.upsertMessage(testMsg)
 			if err2 != nil {
@@ -280,20 +278,19 @@ func Test_wasmModel_UpdateSentStatus(t *testing.T) {
 
 // Smoke test wasmModel.JoinChannel/wasmModel.LeaveChannel happy paths.
 func Test_wasmModel_JoinChannel_LeaveChannel(t *testing.T) {
-	cipher, err := cryptoChannel.NewCipher(
+	cipher, err := idbCrypto.NewCipher(
 		[]byte("testPass"), []byte("testSalt"), 128, csprng.NewSystemRNG())
 	if err != nil {
 		t.Fatalf("Failed to create cipher")
 	}
-	for _, c := range []cryptoChannel.Cipher{nil, cipher} {
+	for _, c := range []idbCrypto.Cipher{nil, cipher} {
 		cs := ""
 		if c != nil {
 			cs = "_withCipher"
 		}
 		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, dummyEU)
 			if err2 != nil {
 				t.Fatal(err2)
 			}
@@ -333,12 +330,12 @@ func Test_wasmModel_JoinChannel_LeaveChannel(t *testing.T) {
 
 // Test UUID gets returned when different messages are added.
 func Test_wasmModel_UUIDTest(t *testing.T) {
-	cipher, err := cryptoChannel.NewCipher(
+	cipher, err := idbCrypto.NewCipher(
 		[]byte("testPass"), []byte("testSalt"), 128, csprng.NewSystemRNG())
 	if err != nil {
 		t.Fatalf("Failed to create cipher")
 	}
-	for _, c := range []cryptoChannel.Cipher{nil, cipher} {
+	for _, c := range []idbCrypto.Cipher{nil, cipher} {
 		cs := ""
 		if c != nil {
 			cs = "_withCipher"
@@ -346,8 +343,7 @@ func Test_wasmModel_UUIDTest(t *testing.T) {
 		t.Run("Test_wasmModel_UUIDTest"+cs, func(t *testing.T) {
 			storage.GetLocalStorage().Clear()
 			testString := "testHello" + cs
-			eventModel, err2 := newWASMModel(testString, c,
-				dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB)
+			eventModel, err2 := newWASMModel(testString, c, dummyEU)
 			if err2 != nil {
 				t.Fatal(err2)
 			}
@@ -380,12 +376,12 @@ func Test_wasmModel_UUIDTest(t *testing.T) {
 
 // Tests if the same message ID being sent always returns the same UUID.
 func Test_wasmModel_DuplicateReceives(t *testing.T) {
-	cipher, err := cryptoChannel.NewCipher(
+	cipher, err := idbCrypto.NewCipher(
 		[]byte("testPass"), []byte("testSalt"), 128, csprng.NewSystemRNG())
 	if err != nil {
 		t.Fatalf("Failed to create cipher")
 	}
-	for _, c := range []cryptoChannel.Cipher{nil, cipher} {
+	for _, c := range []idbCrypto.Cipher{nil, cipher} {
 		cs := ""
 		if c != nil {
 			cs = "_withCipher"
@@ -393,8 +389,7 @@ func Test_wasmModel_DuplicateReceives(t *testing.T) {
 		testString := "Test_wasmModel_DuplicateReceives" + cs
 		t.Run(testString, func(t *testing.T) {
 			storage.GetLocalStorage().Clear()
-			eventModel, err := newWASMModel(testString, c,
-				dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB)
+			eventModel, err := newWASMModel(testString, c, dummyEU)
 			if err != nil {
 				t.Fatal(err)
 			}
@@ -412,13 +407,16 @@ func Test_wasmModel_DuplicateReceives(t *testing.T) {
 			}
 
 			// Store duplicate messages with same messageID
+			referenceID := uint64(0)
 			for i := 0; i < 10; i++ {
 				uuid = eventModel.ReceiveMessage(channelID, msgID, "test",
 					testString+fmt.Sprintf("%d", i), []byte{8, 6, 7, 5}, 0, 0,
 					netTime.Now(), time.Hour, rnd, 0, channels.Sent, false)
-				if uuid != 0 {
-					t.Fatalf("Expected UUID to be zero for duplicate receives")
+				if referenceID == 0 {
+					referenceID = uuid
 				}
+				require.Equal(t, referenceID, uuid,
+					"UUID must be identical for duplicate receives")
 			}
 		})
 	}
@@ -427,12 +425,12 @@ func Test_wasmModel_DuplicateReceives(t *testing.T) {
 // Happy path: Inserts many messages, deletes some, and checks that the final
 // result is as expected.
 func Test_wasmModel_deleteMsgByChannel(t *testing.T) {
-	cipher, err := cryptoChannel.NewCipher(
+	cipher, err := idbCrypto.NewCipher(
 		[]byte("testPass"), []byte("testSalt"), 128, csprng.NewSystemRNG())
 	if err != nil {
 		t.Fatalf("Failed to create cipher")
 	}
-	for _, c := range []cryptoChannel.Cipher{nil, cipher} {
+	for _, c := range []idbCrypto.Cipher{nil, cipher} {
 		cs := ""
 		if c != nil {
 			cs = "_withCipher"
@@ -442,8 +440,7 @@ func Test_wasmModel_deleteMsgByChannel(t *testing.T) {
 			storage.GetLocalStorage().Clear()
 			totalMessages := 10
 			expectedMessages := 5
-			eventModel, err := newWASMModel(testString, c,
-				dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB)
+			eventModel, err := newWASMModel(testString, c, dummyEU)
 			if err != nil {
 				t.Fatal(err)
 			}
@@ -500,12 +497,12 @@ func Test_wasmModel_deleteMsgByChannel(t *testing.T) {
 // This test is designed to prove the behavior of unique indexes.
 // Inserts will not fail, they simply will not happen.
 func TestWasmModel_receiveHelper_UniqueIndex(t *testing.T) {
-	cipher, err := cryptoChannel.NewCipher(
+	cipher, err := idbCrypto.NewCipher(
 		[]byte("testPass"), []byte("testSalt"), 128, csprng.NewSystemRNG())
 	if err != nil {
 		t.Fatalf("Failed to create cipher")
 	}
-	for i, c := range []cryptoChannel.Cipher{nil, cipher} {
+	for i, c := range []idbCrypto.Cipher{nil, cipher} {
 		cs := ""
 		if c != nil {
 			cs = "_withCipher"
@@ -513,8 +510,7 @@ func TestWasmModel_receiveHelper_UniqueIndex(t *testing.T) {
 		t.Run("TestWasmModel_receiveHelper_UniqueIndex"+cs, func(t *testing.T) {
 			storage.GetLocalStorage().Clear()
 			testString := fmt.Sprintf("test_receiveHelper_UniqueIndex_%d", i)
-			eventModel, err := newWASMModel(testString, c,
-				dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB)
+			eventModel, err := newWASMModel(testString, c, dummyEU)
 			if err != nil {
 				t.Fatal(err)
 			}
@@ -541,12 +537,12 @@ func TestWasmModel_receiveHelper_UniqueIndex(t *testing.T) {
 
 			testMsgId := message.DeriveChannelMessageID(&id.ID{1}, 0, []byte(testString))
 			testMsg := buildMessage([]byte(testString), testMsgId.Bytes(), nil,
-				testString, []byte(testString), []byte{8, 6, 7, 5}, 0, 0,
+				testString, testString, []byte{8, 6, 7, 5}, 0, 0,
 				netTime.Now(), time.Second, 0, 0, false, false, channels.Sent)
 
 			testMsgId2 := message.DeriveChannelMessageID(&id.ID{2}, 0, []byte(testString))
 			testMsg2 := buildMessage([]byte(testString), testMsgId2.Bytes(), nil,
-				testString, []byte(testString), []byte{8, 6, 7, 5}, 0, 0,
+				testString, testString, []byte{8, 6, 7, 5}, 0, 0,
 				netTime.Now(), time.Second, 0, 0, false, false, channels.Sent)
 
 			// First message insert should succeed
@@ -586,3 +582,67 @@ func TestWasmModel_receiveHelper_UniqueIndex(t *testing.T) {
 		})
 	}
 }
+
+func TestUpsertMessageDuplicates(t *testing.T) {
+	msgJSON := []byte(`{"pubkey":"4gFD38jRhf/aNDRzaguLJHHgTncs0pwvNiRvjTBOaXk=",
+                "dm_token":3173028031,
+		"nickname":"",
+		"message_id":"BTi+V2anHhrRAEcCMm6g2Bs9oKn7PhQzCzvM7x8Ml7A=",
+		"channel_id":"Gt1T8t1GcnQXijiOjX5yjiLt3F8tnPB9S70ntBx86h8D",
+		"parent_message_id":null,
+		"pinned":false,
+		"hidden":false,
+		"round":151369004,
+		"codeset_version":0,
+		"timestamp": "2023-06-06T19:39:27.281204113Z",
+		"lease_v2":"30000000000",
+		"status":2,
+		"type":1,
+		"text":"KzT25s76K9AEIv0HadhjULkU37dkCUp/8N9+5UkkA8pLEBK71P7KFMr2xii/enYr4jpQcayNm82WD4wisimkK2wtaKb0hJIELAxJHxHilF2qVcDYn7zmx2ZiYkerZGTlf7DkHPZrA+EJJIu9gZ4ApR8TNVf15GxPSDGKHV6iv7vEMrrdoi4JKOFTpNsUjjbDykc91ADmmRlh5LUR/lqyt7bEasyVY2Zo+UiZpJrUJII0fhhn0HLbfs6ekg1tkq+vbPap3vNoebLZag3nEnMk+JqNjVW93BFICSMFv8cYX9q0sSK6sgHtz0HX9b7xTEFVHlCb2Zv/ErOX2LiedsgM4aGpOvjGYQM9ludCutVJArfM/5ejupMpvVPimY69QdevqxMm6v7c8H3abLQf4iKNVoaTaRygYFYArTWWoKep7BDZhjhCd4XnILGLjq7oeBfb3enMUSmeYabIuQpjAfRXU6cZh4cVomhEbkF5kj/rAc3GLzWtN1XcE5PC8bTL3m6YVN6hMCd1S7dsnDQ4FJQoDoqYzmTLD7jtyQujS86JsPkkt7kAIi14VxHTNEAM07cN6rH4VifWWMgQo4djPJ+nnsW9zUnylT9KS1GGyeaUhGC7GmrJGd7DmfHrhdWb9iMboKyH7VTm5C6X71JW1AhmdFfjtIHmWfhbAFsrP2gwkb4e+F86urUmK5LTT6qhGklUsXsmMFjwiEUCBWLSOZLKfITkNOBGIuGGcMi8Jtvwo1f4DHQ6sm/Y8qaJFmesyVKpA3MAVNawqlZJSJXflUA/mpJprFxLm/J3mdnxkLrqQSeSCAxHgpEdH3jiSPNhmn6QaJOVT+wotm+8lAq7qWgBWfMmcVSgWdAfVwujeP71Uo1y24/Z4+jXulFz1hPfbfmHk6/Qz4JlSgHZIqgOS1FTsmXFvBUe4moHR/QFDEXnz2LroRIavt9gWZPE3z1FIcjwSmtUzvpE8gZud+dKVVrjm8YPseq+IJ/AwRRlkUR5D0vf1TwEm0yQ7E+EotFvibQ="
+	}`)
+
+	// Initial call
+	msg1 := &Message{}
+	err := json.Unmarshal(msgJSON, msg1)
+	require.NoError(t, err)
+	msg1.Status = 0
+
+	// Second call
+	msg2 := &Message{}
+	err = json.Unmarshal(msgJSON, msg2)
+	require.NoError(t, err)
+	msg2.Status = 1
+
+	// Final call
+	msg3 := &Message{}
+	err = json.Unmarshal(msgJSON, msg3)
+	require.NoError(t, err)
+	msg3.Status = 2
+
+	cipher, err := idbCrypto.NewCipher(
+		[]byte("testPass"), []byte("testSalt"), 128,
+		csprng.NewSystemRNG())
+	require.NoError(t, err)
+	testString := "test_duplicateUpsertMessage"
+	eventModel, err := newWASMModel(testString, cipher, dummyEU)
+	require.NoError(t, err)
+
+	uuid, err := eventModel.upsertMessage(msg1)
+	require.NoError(t, err)
+	require.NotEqual(t, uuid, 0)
+
+	uuid2, err := eventModel.upsertMessage(msg2)
+	require.NoError(t, err)
+	require.Equal(t, uuid, uuid2)
+
+	uuid3, err := eventModel.upsertMessage(msg3)
+	require.NoError(t, err)
+	require.Equal(t, uuid, uuid3)
+
+	msgID, err := message.UnmarshalID(msg1.MessageID)
+	require.NoError(t, err)
+
+	modelMsg, err := eventModel.GetMessage(msgID)
+	require.NoError(t, err)
+	require.Equal(t, channels.Delivered, modelMsg.Status)
+}
diff --git a/indexedDb/impl/channels/init.go b/indexedDb/impl/channels/init.go
index ecc16da447b0db684ca55031a26e6a3d19206320..7a55c6d09823124e26360558aa7d3555bdbdb462 100644
--- a/indexedDb/impl/channels/init.go
+++ b/indexedDb/impl/channels/init.go
@@ -16,43 +16,42 @@ import (
 	jww "github.com/spf13/jwalterweatherman"
 
 	"gitlab.com/elixxir/client/v4/channels"
-	cryptoChannel "gitlab.com/elixxir/crypto/channel"
+	idbCrypto "gitlab.com/elixxir/crypto/indexedDb"
 	"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
+
+// eventUpdate takes an event type and JSON object from
+// bindings/channelsCallbacks.go.
+type eventUpdate func(eventType int64, jsonMarshallable any)
 
 // 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)
+func NewWASMEventModel(databaseName string, encryption idbCrypto.Cipher,
+	eventCallback eventUpdate) (channels.EventModel, error) {
+	return newWASMModel(databaseName, encryption, eventCallback)
 }
 
 // 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) {
+func newWASMModel(databaseName string, encryption idbCrypto.Cipher,
+	eventCallback eventUpdate) (*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 +61,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
 		})
@@ -86,11 +77,9 @@ func newWASMModel(databaseName string, encryption cryptoChannel.Cipher,
 	}
 
 	wrapper := &wasmModel{
-		db:                db,
-		cipher:            encryption,
-		receivedMessageCB: messageReceivedCB,
-		deletedMessageCB:  deletedMessageCB,
-		mutedUserCB:       mutedUserCB,
+		db:            db,
+		cipher:        encryption,
+		eventCallback: eventCallback,
 	}
 	return wrapper, nil
 }
@@ -150,15 +139,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 b84f13dc205ecd8bfe2466ea9ecb827770f31754..7213825175820be161e1b6e8d0d29efde49c545f 100644
--- a/indexedDb/impl/channels/main.go
+++ b/indexedDb/impl/channels/main.go
@@ -17,6 +17,7 @@ import (
 	"github.com/spf13/cobra"
 	jww "github.com/spf13/jwalterweatherman"
 
+	"gitlab.com/elixxir/wasm-utils/exception"
 	"gitlab.com/elixxir/xxdk-wasm/logging"
 	"gitlab.com/elixxir/xxdk-wasm/worker"
 )
@@ -51,10 +52,27 @@ var channelsCmd = &cobra.Command{
 		jww.INFO.Printf("xxDK channels web worker version: v%s", SEMVER)
 
 		jww.INFO.Print("[WW] Starting xxDK WebAssembly Channels Database Worker.")
-		m := &manager{
-			wtm: worker.NewThreadManager("ChannelsIndexedDbWorker", true),
+		tm, err := worker.NewThreadManager("ChannelsIndexedDbWorker", true)
+		if err != nil {
+			exception.ThrowTrace(err)
 		}
+		m := &manager{wtm: tm}
 		m.registerCallbacks()
+
+		m.wtm.RegisterMessageChannelCallback(worker.LoggerTag,
+			func(port js.Value, channelName string) {
+				p := worker.DefaultParams()
+				p.MessageLogging = false
+				err = logging.EnableThreadLogging(
+					logLevel, threadLogLevel, 0, channelName, port)
+				if err != nil {
+					fmt.Printf("Failed to intialize logging: %+v", err)
+					os.Exit(1)
+				}
+
+				jww.INFO.Print("TEST channel")
+			})
+
 		m.wtm.SignalReady()
 
 		// Indicate to the Javascript caller that the WASM is ready by resolving
@@ -68,13 +86,20 @@ var channelsCmd = &cobra.Command{
 }
 
 var (
-	logLevel jww.Threshold
+	logLevel       jww.Threshold
+	threadLogLevel jww.Threshold
 )
 
 func init() {
 	// Initialize all startup flags
-	channelsCmd.Flags().IntVarP((*int)(&logLevel), "logLevel", "l", 2,
+	channelsCmd.Flags().IntVarP((*int)(&logLevel),
+		"logLevel", "l", int(jww.LevelDebug),
 		"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.")
+	channelsCmd.Flags().IntVarP((*int)(&threadLogLevel),
+		"threadLogLevel", "m", int(jww.LevelDebug),
+		"The log level when outputting to the worker file buffer. "+
+			"0 = TRACE, 1 = DEBUG, 2 = INFO, 3 = WARN, 4 = ERROR, "+
+			"5 = CRITICAL, 6 = FATAL, -1 = disabled.")
 }
diff --git a/indexedDb/impl/channels/model.go b/indexedDb/impl/channels/model.go
index e5d3e00aa5209985de60a77f5330a53208b1af88..702d489b04d4eb6bfac510a5b9e77f753f411cee 100644
--- a/indexedDb/impl/channels/model.go
+++ b/indexedDb/impl/channels/model.go
@@ -56,7 +56,7 @@ type Message struct {
 	Status          uint8     `json:"status"`
 	Hidden          bool      `json:"hidden"`
 	Pinned          bool      `json:"pinned"` // Index
-	Text            []byte    `json:"text"`
+	Text            string    `json:"text"`
 	Type            uint16    `json:"type"`
 	Round           uint64    `json:"round"`
 
diff --git a/indexedDb/impl/dm/callbacks.go b/indexedDb/impl/dm/callbacks.go
index 380f04d9953fdda65c4b0d80a66e7b40a7edc2b1..a081a83dca9adf61a78e48101239edfd6a22c0b1 100644
--- a/indexedDb/impl/dm/callbacks.go
+++ b/indexedDb/impl/dm/callbacks.go
@@ -10,15 +10,15 @@
 package main
 
 import (
-	"crypto/ed25519"
 	"encoding/json"
 
 	"github.com/pkg/errors"
 	jww "github.com/spf13/jwalterweatherman"
 
 	"gitlab.com/elixxir/client/v4/dm"
-	cryptoChannel "gitlab.com/elixxir/crypto/channel"
 	"gitlab.com/elixxir/crypto/fastRNG"
+	idbCrypto "gitlab.com/elixxir/crypto/indexedDb"
+	"gitlab.com/elixxir/wasm-utils/exception"
 	wDm "gitlab.com/elixxir/xxdk-wasm/indexedDb/worker/dm"
 	"gitlab.com/elixxir/xxdk-wasm/worker"
 	"gitlab.com/xx_network/crypto/csprng"
@@ -42,192 +42,226 @@ func (m *manager) registerCallbacks() {
 	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.DeleteMessageTag, m.deleteMessageCB)
 	m.wtm.RegisterCallback(wDm.GetConversationTag, m.getConversationCB)
 	m.wtm.RegisterCallback(wDm.GetConversationsTag, m.getConversationsCB)
 }
 
 // newWASMEventModelCB is the callback for NewWASMEventModel. Returns an empty
 // slice on success or an error message on failure.
-func (m *manager) newWASMEventModelCB(data []byte) ([]byte, error) {
+func (m *manager) newWASMEventModelCB(message []byte, reply func(message []byte)) {
 	var msg wDm.NewWASMEventModelMessage
-	err := json.Unmarshal(data, &msg)
+	err := json.Unmarshal(message, &msg)
 	if err != nil {
-		return []byte{}, errors.Errorf(
-			"failed to JSON unmarshal %T from main thread: %+v", msg, err)
+		reply([]byte(errors.Wrapf(err,
+			"failed to JSON unmarshal %T from main thread", msg).Error()))
+		return
 	}
 
 	// Create new encryption cipher
 	rng := fastRNG.NewStreamGenerator(12, 1024, csprng.NewSystemRNG)
-	encryption, err := cryptoChannel.NewCipherFromJSON(
+	encryption, err := idbCrypto.NewCipherFromJSON(
 		[]byte(msg.EncryptionJSON), rng.GetStream())
 	if err != nil {
-		return []byte{}, errors.Errorf("failed to JSON unmarshal channel "+
-			"cipher from main thread: %+v", err)
+		reply([]byte(errors.Wrap(err,
+			"failed to JSON unmarshal Cipher from main thread").Error()))
+		return
 	}
 
 	m.model, err = NewWASMEventModel(
-		msg.DatabaseName, encryption, m.messageReceivedCallback)
+		msg.DatabaseName, encryption, m.eventUpdateCallback)
 	if err != nil {
-		return []byte(err.Error()), nil
+		reply([]byte(err.Error()))
+		return
 	}
 
-	return []byte{}, nil
+	reply(nil)
 }
 
-// messageReceivedCallback sends calls to the MessageReceivedCallback in the
-// main thread.
-//
-// messageReceivedCallback adhere to the MessageReceivedCallback type.
-func (m *manager) messageReceivedCallback(uuid uint64, pubKey ed25519.PublicKey,
-	messageUpdate, conversationUpdate bool) {
+// eventUpdateCallback JSON marshals the interface and sends it to the main
+// thread the with the event type to be sent on the EventUpdate callback.
+func (m *manager) eventUpdateCallback(eventType int64, jsonMarshallable any) {
+	jsonData, err := json.Marshal(jsonMarshallable)
+	if err != nil {
+		jww.FATAL.Panicf("[DM] Failed to JSON marshal %T for EventUpdate "+
+			"callback: %+v", jsonMarshallable, err)
+	}
+
 	// Package parameters for sending
-	msg := &wDm.MessageReceivedCallbackMessage{
-		UUID:               uuid,
-		PubKey:             pubKey,
-		MessageUpdate:      messageUpdate,
-		ConversationUpdate: conversationUpdate,
+	msg := wDm.EventUpdateCallbackMessage{
+		EventType: eventType,
+		JsonData:  jsonData,
 	}
 	data, err := json.Marshal(msg)
 	if err != nil {
-		jww.ERROR.Printf(
-			"Could not JSON marshal MessageReceivedCallbackMessage: %+v", err)
-		return
+		exception.Throwf("[DM] Could not JSON marshal %T for EventUpdate "+
+			"callback: %+v", msg, err)
 	}
 
 	// Send it to the main thread
-	m.wtm.SendMessage(wDm.MessageReceivedCallbackTag, data)
+	err = m.wtm.SendNoResponse(wDm.EventUpdateCallbackTag, data)
+	if err != nil {
+		exception.Throwf(
+			"[DM] Could not send message for EventUpdate callback: %+v", err)
+	}
 }
 
 // receiveCB is the callback for wasmModel.Receive. Returns a UUID of 0 on error
 // or the JSON marshalled UUID (uint64) on success.
-func (m *manager) receiveCB(data []byte) ([]byte, error) {
+func (m *manager) receiveCB(message []byte, reply func(message []byte)) {
 	var msg wDm.TransferMessage
-	err := json.Unmarshal(data, &msg)
+	err := json.Unmarshal(message, &msg)
 	if err != nil {
-		return zeroUUID, errors.Errorf(
-			"failed to JSON unmarshal %T from main thread: %+v", msg, err)
+		jww.ERROR.Printf("[DM] Could not JSON unmarshal payload for Receive "+
+			"from main thread: %+v", err)
+		reply(zeroUUID)
+		return
 	}
 
 	uuid := m.model.Receive(
-		msg.MessageID, msg.Nickname, msg.Text, msg.PartnerKey, msg.SenderKey, msg.DmToken,
-		msg.Codeset, msg.Timestamp, msg.Round, msg.MType, msg.Status)
+		msg.MessageID, msg.Nickname, msg.Text, msg.PartnerKey, msg.SenderKey,
+		msg.DmToken, msg.Codeset, msg.Timestamp, msg.Round, msg.MType,
+		msg.Status)
 
-	uuidData, err := json.Marshal(uuid)
+	replyMsg, err := json.Marshal(uuid)
 	if err != nil {
-		return zeroUUID, errors.Errorf("failed to JSON marshal UUID: %+v", err)
+		exception.Throwf(
+			"[DM] Could not JSON marshal UUID for Receive: %+v", err)
 	}
-	return uuidData, nil
+
+	reply(replyMsg)
 }
 
 // receiveTextCB is the callback for wasmModel.ReceiveText. Returns a UUID of 0
 // on error or the JSON marshalled UUID (uint64) on success.
-func (m *manager) receiveTextCB(data []byte) ([]byte, error) {
+func (m *manager) receiveTextCB(message []byte, reply func(message []byte)) {
 	var msg wDm.TransferMessage
-	err := json.Unmarshal(data, &msg)
+	err := json.Unmarshal(message, &msg)
 	if err != nil {
-		return []byte{}, errors.Errorf(
-			"failed to JSON unmarshal %T from main thread: %+v", msg, err)
+		jww.ERROR.Printf("[DM] Could not JSON unmarshal payload for "+
+			"ReceiveText from main thread: %+v", err)
+		reply(zeroUUID)
+		return
 	}
 
 	uuid := m.model.ReceiveText(
-		msg.MessageID, msg.Nickname, string(msg.Text), msg.PartnerKey, msg.SenderKey, msg.DmToken,
-		msg.Codeset, msg.Timestamp, msg.Round, msg.Status)
+		msg.MessageID, msg.Nickname, string(msg.Text), msg.PartnerKey,
+		msg.SenderKey, msg.DmToken, msg.Codeset, msg.Timestamp, msg.Round,
+		msg.Status)
 
-	uuidData, err := json.Marshal(uuid)
+	replyMsg, err := json.Marshal(uuid)
 	if err != nil {
-		return []byte{}, errors.Errorf("failed to JSON marshal UUID: %+v", err)
+		exception.Throwf(
+			"[DM] Could not JSON marshal UUID for ReceiveText: %+v", err)
 	}
 
-	return uuidData, nil
+	reply(replyMsg)
 }
 
 // receiveReplyCB is the callback for wasmModel.ReceiveReply. Returns a UUID of
 // 0 on error or the JSON marshalled UUID (uint64) on success.
-func (m *manager) receiveReplyCB(data []byte) ([]byte, error) {
+func (m *manager) receiveReplyCB(message []byte, reply func(message []byte)) {
 	var msg wDm.TransferMessage
-	err := json.Unmarshal(data, &msg)
+	err := json.Unmarshal(message, &msg)
 	if err != nil {
-		return zeroUUID, errors.Errorf(
-			"failed to JSON unmarshal %T from main thread: %+v", msg, err)
+		jww.ERROR.Printf("[DM] Could not JSON unmarshal payload for "+
+			"ReceiveReply from main thread: %+v", err)
+		reply(zeroUUID)
+		return
 	}
 
 	uuid := m.model.ReceiveReply(msg.MessageID, msg.ReactionTo, msg.Nickname,
 		string(msg.Text), msg.PartnerKey, msg.SenderKey, msg.DmToken, msg.Codeset, msg.Timestamp,
 		msg.Round, msg.Status)
 
-	uuidData, err := json.Marshal(uuid)
+	replyMsg, err := json.Marshal(uuid)
 	if err != nil {
-		return zeroUUID, errors.Errorf("failed to JSON marshal UUID: %+v", err)
+		exception.Throwf(
+			"[DM] Could not JSON marshal UUID for ReceiveReply: %+v", err)
 	}
 
-	return uuidData, nil
+	reply(replyMsg)
 }
 
 // receiveReactionCB is the callback for wasmModel.ReceiveReaction. Returns a
 // UUID of 0 on error or the JSON marshalled UUID (uint64) on success.
-func (m *manager) receiveReactionCB(data []byte) ([]byte, error) {
+func (m *manager) receiveReactionCB(message []byte, reply func(message []byte)) {
 	var msg wDm.TransferMessage
-	err := json.Unmarshal(data, &msg)
+	err := json.Unmarshal(message, &msg)
 	if err != nil {
-		return zeroUUID, errors.Errorf(
-			"failed to JSON unmarshal %T from main thread: %+v", msg, err)
+		jww.ERROR.Printf("[DM] Could not JSON unmarshal payload for "+
+			"ReceiveReaction from main thread: %+v", err)
+		reply(zeroUUID)
+		return
 	}
 
 	uuid := m.model.ReceiveReaction(msg.MessageID, msg.ReactionTo, msg.Nickname,
 		string(msg.Text), msg.PartnerKey, msg.SenderKey, msg.DmToken, msg.Codeset, msg.Timestamp,
 		msg.Round, msg.Status)
 
-	uuidData, err := json.Marshal(uuid)
+	replyMsg, err := json.Marshal(uuid)
 	if err != nil {
-		return zeroUUID, errors.Errorf("failed to JSON marshal UUID: %+v", err)
+		exception.Throwf(
+			"[DM] Could not JSON marshal UUID for ReceiveReaction: %+v", err)
 	}
 
-	return uuidData, nil
+	reply(replyMsg)
 }
 
 // updateSentStatusCB is the callback for wasmModel.UpdateSentStatus. Always
 // returns nil; meaning, no response is supplied (or expected).
-func (m *manager) updateSentStatusCB(data []byte) ([]byte, error) {
+func (m *manager) updateSentStatusCB(message []byte, _ func([]byte)) {
 	var msg wDm.TransferMessage
-	err := json.Unmarshal(data, &msg)
+	err := json.Unmarshal(message, &msg)
 	if err != nil {
-		return nil, errors.Errorf(
-			"failed to JSON unmarshal %T from main thread: %+v", msg, err)
+		jww.ERROR.Printf("[DM] Could not JSON unmarshal %T for "+
+			"UpdateSentStatus from main thread: %+v", msg, err)
+		return
 	}
 
 	m.model.UpdateSentStatus(
 		msg.UUID, msg.MessageID, msg.Timestamp, msg.Round, msg.Status)
-
-	return nil, nil
 }
 
-// blockSenderCB is the callback for wasmModel.BlockSender. Always
-// returns nil; meaning, no response is supplied (or expected).
-func (m *manager) blockSenderCB(data []byte) ([]byte, error) {
-	m.model.BlockSender(data)
-	return nil, nil
-}
+// deleteMessageCB is the callback for wasmModel.DeleteMessage. Returns a JSON
+// marshalled bool.
+func (m *manager) deleteMessageCB(message []byte, reply func(message []byte)) {
+	var msg wDm.TransferMessage
+	err := json.Unmarshal(message, &msg)
+	if err != nil {
+		jww.ERROR.Printf("[DM] Could not JSON unmarshal %T for "+
+			"UpdateSentStatus from main thread: %+v", msg, err)
+		reply([]byte{0})
+		return
+	}
 
-// unblockSenderCB is the callback for wasmModel.UnblockSender. Always
-// returns nil; meaning, no response is supplied (or expected).
-func (m *manager) unblockSenderCB(data []byte) ([]byte, error) {
-	m.model.UnblockSender(data)
-	return nil, nil
+	if m.model.DeleteMessage(msg.MessageID, msg.SenderKey) {
+		reply([]byte{1})
+		return
+	}
+	reply([]byte{0})
 }
 
 // getConversationCB is the callback for wasmModel.GetConversation.
 // Returns nil on error or the JSON marshalled Conversation on success.
-func (m *manager) getConversationCB(data []byte) ([]byte, error) {
-	result := m.model.GetConversation(data)
-	return json.Marshal(result)
+func (m *manager) getConversationCB(message []byte, reply func(message []byte)) {
+	result := m.model.GetConversation(message)
+	replyMessage, err := json.Marshal(result)
+	if err != nil {
+		exception.Throwf("[DM] Could not JSON marshal %T for "+
+			"GetConversation: %+v", result, err)
+	}
+	reply(replyMessage)
 }
 
 // getConversationsCB is the callback for wasmModel.GetConversations.
 // Returns nil on error or the JSON marshalled list of Conversation on success.
-func (m *manager) getConversationsCB(_ []byte) ([]byte, error) {
+func (m *manager) getConversationsCB(_ []byte, reply func(message []byte)) {
 	result := m.model.GetConversations()
-	return json.Marshal(result)
+	replyMessage, err := json.Marshal(result)
+	if err != nil {
+		exception.Throwf("[DM] Could not JSON marshal %T for "+
+			"GetConversations: %+v", result, err)
+	}
+	reply(replyMessage)
 }
diff --git a/indexedDb/impl/dm/dmIndexedDbWorker.js b/indexedDb/impl/dm/dmIndexedDbWorker.js
index 8a5fdbf8ad9a02967b408985a0219647003eaf7e..41a5492fb799fe8c27db59252c18d6d8ad454c90 100644
--- a/indexedDb/impl/dm/dmIndexedDbWorker.js
+++ b/indexedDb/impl/dm/dmIndexedDbWorker.js
@@ -12,6 +12,10 @@ const isReady = new Promise((resolve) => {
 });
 
 const go = new Go();
+go.argv = [
+    '--logLevel=2',
+    '--threadLogLevel=2',
+]
 const binPath = 'xxdk-dmIndexedDkWorker.wasm'
 WebAssembly.instantiateStreaming(fetch(binPath), go.importObject).then(async (result) => {
     go.run(result.instance);
diff --git a/indexedDb/impl/dm/implementation.go b/indexedDb/impl/dm/implementation.go
index 1c8e18050d8c36c32ede354f9afaa9a6af1993db..e34bedb879c69daafbf0f5df1eb89c788b343b46 100644
--- a/indexedDb/impl/dm/implementation.go
+++ b/indexedDb/impl/dm/implementation.go
@@ -21,37 +21,39 @@ import (
 	"github.com/pkg/errors"
 	jww "github.com/spf13/jwalterweatherman"
 
+	"gitlab.com/elixxir/client/v4/bindings"
 	"gitlab.com/elixxir/client/v4/cmix/rounds"
 	"gitlab.com/elixxir/client/v4/dm"
-	cryptoChannel "gitlab.com/elixxir/crypto/channel"
+	idbCrypto "gitlab.com/elixxir/crypto/indexedDb"
 	"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"
+	"gitlab.com/xx_network/primitives/netTime"
 )
 
 // wasmModel implements dm.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 MessageReceivedCallback
+	db            *idb.Database
+	cipher        idbCrypto.Cipher
+	eventCallback eventUpdate
 }
 
 // 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
@@ -80,7 +82,7 @@ func (w *wasmModel) upsertConversation(nickname string,
 // NOTE: ID is not set inside this function because we want to use the
 // autoincrement key by default. If you are trying to overwrite an existing
 // message, then you need to set it manually yourself.
-func buildMessage(messageID, parentID, text []byte, partnerKey,
+func buildMessage(messageID, parentID []byte, text string, partnerKey []byte,
 	senderKey ed25519.PublicKey, timestamp time.Time, round id.Round,
 	mType dm.MessageType, codeset uint8, status dm.Status) *Message {
 	return &Message{
@@ -178,13 +180,7 @@ func (w *wasmModel) UpdateSentStatus(uuid uint64, messageID message.ID,
 	}
 
 	// Extract the existing Message and update the Status
-	newMessage := &Message{}
-	err = json.Unmarshal([]byte(utils.JsToJson(currentMsg)), newMessage)
-	if err != nil {
-		jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr,
-			"Could not JSON unmarshal message: %+v", err))
-		return
-	}
+	newMessage, err := valueToMessage(currentMsg)
 
 	newMessage.Status = uint8(status)
 	if !messageID.Equals(message.ID{}) {
@@ -208,8 +204,12 @@ func (w *wasmModel) UpdateSentStatus(uuid uint64, messageID message.ID,
 
 	jww.TRACE.Printf("[DM indexedDB] Calling ReceiveMessageCB(%v, %v, t, f)",
 		uuid, newMessage.ConversationPubKey)
-	go w.receivedMessageCB(uuid, newMessage.ConversationPubKey,
-		true, false)
+	go w.eventCallback(bindings.DmMessageReceived, bindings.DmMessageReceivedJSON{
+		UUID:               uuid,
+		PubKey:             newMessage.ConversationPubKey,
+		MessageUpdate:      true,
+		ConversationUpdate: false,
+	})
 }
 
 // receiveWrapper is a higher-level wrapper of upsertMessage.
@@ -231,11 +231,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,16 +268,15 @@ 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
 		}
 	}
 
 	// Handle encryption, if it is present
-	textBytes := []byte(data)
 	if w.cipher != nil {
-		textBytes, err = w.cipher.Encrypt(textBytes)
+		data, err = w.cipher.Encrypt([]byte(data))
 		if err != nil {
 			return 0, err
 		}
@@ -288,7 +287,7 @@ func (w *wasmModel) receiveWrapper(messageID message.ID, parentID *message.ID, n
 		parentIdBytes = parentID.Marshal()
 	}
 
-	msgToInsert := buildMessage(messageID.Bytes(), parentIdBytes, textBytes,
+	msgToInsert := buildMessage(messageID.Bytes(), parentIdBytes, data,
 		partnerKey, senderKey, timestamp, round.ID, mType, codeset, status)
 
 	uuid, err := w.upsertMessage(msgToInsert)
@@ -298,7 +297,12 @@ func (w *wasmModel) receiveWrapper(messageID message.ID, parentID *message.ID, n
 
 	jww.TRACE.Printf("[DM indexedDB] Calling ReceiveMessageCB(%v, %v, f, %t)",
 		uuid, partnerKey, conversationUpdated)
-	go w.receivedMessageCB(uuid, partnerKey, false, conversationUpdated)
+	go w.eventCallback(bindings.DmMessageReceived, bindings.DmMessageReceivedJSON{
+		UUID:               uuid,
+		PubKey:             partnerKey,
+		MessageUpdate:      false,
+		ConversationUpdate: conversationUpdated,
+	})
 	return uuid, nil
 }
 
@@ -349,14 +353,63 @@ 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)
+}
+
+// DeleteMessage deletes the message with the given message.ID belonging to
+// the sender. If the message exists and belongs to the sender, then it is
+// deleted and DeleteMessage returns true. If it does not exist, it returns
+// false.
+func (w *wasmModel) DeleteMessage(messageID message.ID, senderPubKey ed25519.PublicKey) bool {
+	parentErr := "failed to DeleteMessage"
+	msgId := impl.EncodeBytes(messageID.Marshal())
+
+	// Use the key to get the existing Message
+	currentMsg, err := impl.GetIndex(w.db, messageStoreName,
+		messageStoreMessageIndex, msgId)
+	if err != nil {
+		jww.ERROR.Printf("%s: %+v", parentErr, err)
+		return false
+	}
+
+	// Convert the js.Value to a proper object
+	msgObj, err := valueToMessage(currentMsg)
+	if err != nil {
+		jww.ERROR.Printf("%s: %+v", parentErr, err)
+		return false
+	}
+
+	// Ensure the public keys match
+	if !bytes.Equal(msgObj.SenderPubKey, senderPubKey) {
+		jww.ERROR.Printf("%s: %s", parentErr, "Public keys do not match")
+		return false
+	}
+
+	// Perform the delete
+	err = impl.DeleteIndex(w.db, messageStoreName, messageStoreMessageIndex,
+		msgPkeyName, msgId)
+	if err != nil {
+		jww.ERROR.Printf("%s: %+v", parentErr, err)
+		return false
+	}
+
+	go w.eventCallback(bindings.DmMessageReceived, bindings.DmMessageDeletedJSON{
+		MessageID: messageID,
+	})
+	return true
 }
 
 // GetConversation returns the conversation held by the model (receiver).
@@ -369,11 +422,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,
 	}
 }
 
@@ -411,12 +464,18 @@ 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
 }
+
+// valueToMessage is a helper for converting js.Value to Message.
+func valueToMessage(msgObj js.Value) (*Message, error) {
+	resultMsg := &Message{}
+	return resultMsg, json.Unmarshal([]byte(utils.JsToJson(msgObj)), resultMsg)
+}
diff --git a/indexedDb/impl/dm/implementation_test.go b/indexedDb/impl/dm/implementation_test.go
index 8e05e6af5fff85af79ea0f15be9dc1e2f5a77c12..3ccadb2d8202f2b435a5331a52c7a569cd995ffa 100644
--- a/indexedDb/impl/dm/implementation_test.go
+++ b/indexedDb/impl/dm/implementation_test.go
@@ -14,11 +14,12 @@ import (
 	"crypto/ed25519"
 	"encoding/json"
 	"fmt"
+	"github.com/stretchr/testify/require"
 	"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"
@@ -28,7 +29,7 @@ import (
 	jww "github.com/spf13/jwalterweatherman"
 )
 
-func dummyReceivedMessageCB(uint64, ed25519.PublicKey, bool, bool) {}
+var dummyEU = func(int64, any) {}
 
 func TestMain(m *testing.M) {
 	jww.SetStdoutThreshold(jww.LevelDebug)
@@ -37,8 +38,7 @@ func TestMain(m *testing.M) {
 
 // Test simple receive of a new message for a new conversation.
 func TestImpl_Receive(t *testing.T) {
-	m, err := newWASMModel("TestImpl_Receive", nil,
-		dummyReceivedMessageCB)
+	m, err := newWASMModel("TestImpl_Receive", nil, dummyEU)
 	if err != nil {
 		t.Fatal(err.Error())
 	}
@@ -90,8 +90,7 @@ func TestImpl_Receive(t *testing.T) {
 
 // Test happy path. Insert some conversations and check they exist.
 func TestImpl_GetConversations(t *testing.T) {
-	m, err := newWASMModel("TestImpl_GetConversations", nil,
-		dummyReceivedMessageCB)
+	m, err := newWASMModel("TestImpl_GetConversations", nil, dummyEU)
 	if err != nil {
 		t.Fatal(err.Error())
 	}
@@ -102,7 +101,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())
 		}
@@ -126,35 +125,68 @@ func TestImpl_GetConversations(t *testing.T) {
 
 // Test happy path toggling between blocked/unblocked in a Conversation.
 func TestWasmModel_BlockSender(t *testing.T) {
-	m, err := newWASMModel("TestWasmModel_BlockSender", nil, dummyReceivedMessageCB)
+	m, err := newWASMModel("TestWasmModel_BlockSender", nil, dummyEU)
 	if err != nil {
 		t.Fatal(err.Error())
 	}
 
 	// 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")
 	}
 }
+
+// Test failed and successful deletes
+func TestWasmModel_DeleteMessage(t *testing.T) {
+	m, err := newWASMModel("TestWasmModel_DeleteMessage", nil, dummyEU)
+	if err != nil {
+		t.Fatal(err.Error())
+	}
+
+	// Insert test message
+	testBytes := []byte("test")
+	testBadBytes := []byte("uwu")
+	testMsgId := message.DeriveChannelMessageID(&id.ID{1}, 0, testBytes)
+	testMsg := &Message{
+		MessageID:          testMsgId.Marshal(),
+		ConversationPubKey: testBytes,
+		ParentMessageID:    nil,
+		Timestamp:          time.Now(),
+		SenderPubKey:       testBytes,
+		CodesetVersion:     5,
+		Status:             5,
+		Text:               "",
+		Type:               5,
+		Round:              5,
+	}
+	_, err = m.upsertMessage(testMsg)
+	require.NoError(t, err)
+
+	// Non-matching pub key, should fail to delete
+	require.False(t, m.DeleteMessage(testMsgId, testBadBytes))
+
+	// Correct pub key, should have deleted
+	require.True(t, m.DeleteMessage(testMsgId, testBytes))
+}
diff --git a/indexedDb/impl/dm/init.go b/indexedDb/impl/dm/init.go
index f99f4ef3e2d7f46f9a5672c8ea998c4afdbe1a77..0c33f614fc7cf2b9e2bbc041059e2e8b3bf0b90e 100644
--- a/indexedDb/impl/dm/init.go
+++ b/indexedDb/impl/dm/init.go
@@ -10,14 +10,13 @@
 package main
 
 import (
-	"crypto/ed25519"
 	"syscall/js"
 
 	"github.com/hack-pad/go-indexeddb/idb"
 	jww "github.com/spf13/jwalterweatherman"
 
 	"gitlab.com/elixxir/client/v4/dm"
-	cryptoChannel "gitlab.com/elixxir/crypto/channel"
+	idbCrypto "gitlab.com/elixxir/crypto/indexedDb"
 	"gitlab.com/elixxir/xxdk-wasm/indexedDb/impl"
 )
 
@@ -25,36 +24,33 @@ import (
 // migration purposes.
 const currentVersion uint = 1
 
-// MessageReceivedCallback is called any time a message is received or updated.
-//
-// messageUpdate is true if the Message already exists and was edited.
-// conversationUpdate is true if the Conversation was created or modified.
-type MessageReceivedCallback func(
-	uuid uint64, pubKey ed25519.PublicKey, messageUpdate, conversationUpdate bool)
+// eventUpdate takes an event type and JSON object from bindings/dm.go.
+type eventUpdate func(eventType int64, jsonMarshallable any)
 
 // 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,
-	cb MessageReceivedCallback) (dm.EventModel, error) {
-	return newWASMModel(databaseName, encryption, cb)
+func NewWASMEventModel(databaseName string, encryption idbCrypto.Cipher,
+	eventCallback eventUpdate) (dm.EventModel, error) {
+	return newWASMModel(databaseName, encryption, eventCallback)
 }
 
 // newWASMModel creates the given [idb.Database] and returns a wasmModel.
-func newWASMModel(databaseName string, encryption cryptoChannel.Cipher,
-	cb MessageReceivedCallback) (*wasmModel, error) {
+func newWASMModel(databaseName string, encryption idbCrypto.Cipher,
+	eventCallback eventUpdate) (*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)
@@ -79,7 +75,11 @@ func newWASMModel(databaseName string, encryption cryptoChannel.Cipher,
 		return nil, ctx.Err()
 	}
 
-	wrapper := &wasmModel{db: db, receivedMessageCB: cb, cipher: encryption}
+	wrapper := &wasmModel{
+		db:            db,
+		cipher:        encryption,
+		eventCallback: eventCallback,
+	}
 	return wrapper, nil
 }
 
diff --git a/indexedDb/impl/dm/main.go b/indexedDb/impl/dm/main.go
index 96fae8e6fbdbce3767739891d4d7ea466e08149c..0034e9265d86a79c76a2b604ae725e846688f09f 100644
--- a/indexedDb/impl/dm/main.go
+++ b/indexedDb/impl/dm/main.go
@@ -17,6 +17,7 @@ import (
 	"github.com/spf13/cobra"
 	jww "github.com/spf13/jwalterweatherman"
 
+	"gitlab.com/elixxir/wasm-utils/exception"
 	"gitlab.com/elixxir/xxdk-wasm/logging"
 	"gitlab.com/elixxir/xxdk-wasm/worker"
 )
@@ -52,10 +53,27 @@ var dmCmd = &cobra.Command{
 		jww.INFO.Printf("xxDK DM web worker version: v%s", SEMVER)
 
 		jww.INFO.Print("[WW] Starting xxDK WebAssembly DM Database Worker.")
-		m := &manager{
-			wtm: worker.NewThreadManager("DmIndexedDbWorker", true),
+		tm, err := worker.NewThreadManager("DmIndexedDbWorker", true)
+		if err != nil {
+			exception.ThrowTrace(err)
 		}
+		m := &manager{wtm: tm}
 		m.registerCallbacks()
+
+		m.wtm.RegisterMessageChannelCallback(worker.LoggerTag,
+			func(port js.Value, channelName string) {
+				p := worker.DefaultParams()
+				p.MessageLogging = false
+				err = logging.EnableThreadLogging(
+					logLevel, threadLogLevel, 0, channelName, port)
+				if err != nil {
+					fmt.Printf("Failed to intialize logging: %+v", err)
+					os.Exit(1)
+				}
+
+				jww.INFO.Print("TEST channel")
+			})
+
 		m.wtm.SignalReady()
 
 		// Indicate to the Javascript caller that the WASM is ready by resolving
@@ -69,13 +87,20 @@ var dmCmd = &cobra.Command{
 }
 
 var (
-	logLevel jww.Threshold
+	logLevel       jww.Threshold
+	threadLogLevel jww.Threshold
 )
 
 func init() {
 	// Initialize all startup flags
-	dmCmd.Flags().IntVarP((*int)(&logLevel), "logLevel", "l", 2,
+	dmCmd.Flags().IntVarP((*int)(&logLevel),
+		"logLevel", "l", int(jww.LevelDebug),
 		"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.")
+	dmCmd.Flags().IntVarP((*int)(&threadLogLevel),
+		"threadLogLevel", "m", int(jww.LevelDebug),
+		"The log level when outputting to the worker file buffer. "+
+			"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..6893fc3a6a7fffd95eb31e389e8352040e797b9f 100644
--- a/indexedDb/impl/dm/model.go
+++ b/indexedDb/impl/dm/model.go
@@ -46,7 +46,7 @@ type Message struct {
 	SenderPubKey       []byte    `json:"sender_pub_key"` // Index
 	CodesetVersion     uint8     `json:"codeset_version"`
 	Status             uint8     `json:"status"`
-	Text               []byte    `json:"text"`
+	Text               string    `json:"text"`
 	Type               uint16    `json:"type"`
 	Round              uint64    `json:"round"`
 }
@@ -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..5bbfcc7d89a3dd8675c47824725c3add02b38f8e
--- /dev/null
+++ b/indexedDb/impl/state/callbacks.go
@@ -0,0 +1,94 @@
+////////////////////////////////////////////////////////////////////////////////
+// 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/wasm-utils/exception"
+	"gitlab.com/elixxir/xxdk-wasm/indexedDb/impl"
+	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 impl.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(message []byte, reply func(message []byte)) {
+	var msg stateWorker.NewStateMessage
+	err := json.Unmarshal(message, &msg)
+	if err != nil {
+		reply([]byte(errors.Wrapf(err,
+			"failed to JSON unmarshal %T from main thread", msg).Error()))
+		return
+	}
+
+	m.model, err = NewState(msg.DatabaseName)
+	if err != nil {
+		reply([]byte(err.Error()))
+		return
+	}
+
+	reply(nil)
+}
+
+// setCB is the callback for stateModel.Set.
+// Returns nil on error or the resulting byte data on success.
+func (m *manager) setCB(message []byte, reply func(message []byte)) {
+	var msg stateWorker.TransferMessage
+	err := json.Unmarshal(message, &msg)
+	if err != nil {
+		reply([]byte(errors.Wrapf(err,
+			"failed to JSON unmarshal %T from main thread", msg).Error()))
+		return
+	}
+
+	err = m.model.Set(msg.Key, msg.Value)
+	if err != nil {
+		reply([]byte(err.Error()))
+		return
+	}
+
+	reply(nil)
+}
+
+// getCB is the callback for stateModel.Get.
+// Returns nil on error or the resulting byte data on success.
+func (m *manager) getCB(message []byte, reply func(message []byte)) {
+	key := string(message)
+	result, err := m.model.Get(key)
+	msg := stateWorker.TransferMessage{
+		Key:   key,
+		Value: result,
+		Error: err.Error(),
+	}
+
+	replyMessage, err := json.Marshal(msg)
+	if err != nil {
+		exception.Throwf("Could not JSON marshal %T for Get: %+v", msg, err)
+	}
+
+	reply(replyMessage)
+}
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..14aab3a896ea8dc248f8d7f8b9f7c2bf6b42b87d
--- /dev/null
+++ b/indexedDb/impl/state/init.go
@@ -0,0 +1,83 @@
+////////////////////////////////////////////////////////////////////////////////
+// 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/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) (impl.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..2344486a2c3b1c6ab55db13110d4d4305b16ed0f
--- /dev/null
+++ b/indexedDb/impl/state/main.go
@@ -0,0 +1,105 @@
+////////////////////////////////////////////////////////////////////////////////
+// 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/wasm-utils/exception"
+	"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.
+	stateCmd.SetArgs(os.Args)
+
+	err := stateCmd.Execute()
+	if err != nil {
+		fmt.Println(err)
+		os.Exit(1)
+	}
+}
+
+var stateCmd = &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.")
+		tm, err := worker.NewThreadManager("DmIndexedDbWorker", true)
+		if err != nil {
+			exception.ThrowTrace(err)
+		}
+		m := &manager{wtm: tm}
+		m.registerCallbacks()
+
+		m.wtm.RegisterMessageChannelCallback(worker.LoggerTag,
+			func(port js.Value, channelName string) {
+				p := worker.DefaultParams()
+				p.MessageLogging = false
+				err = logging.EnableThreadLogging(
+					logLevel, threadLogLevel, 0, channelName, port)
+				if err != nil {
+					fmt.Printf("Failed to intialize logging: %+v", err)
+					os.Exit(1)
+				}
+
+				jww.INFO.Print("TEST channel")
+			})
+
+		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
+	threadLogLevel jww.Threshold
+)
+
+func init() {
+	// Initialize all startup flags
+	stateCmd.Flags().IntVarP((*int)(&logLevel),
+		"logLevel", "l", int(jww.LevelDebug),
+		"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.")
+	stateCmd.Flags().IntVarP((*int)(&threadLogLevel),
+		"threadLogLevel", "m", int(jww.LevelDebug),
+		"The log level when outputting to the worker file buffer. "+
+			"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..cff3e34af865755070e6ff634465b1b82b12ced3
--- /dev/null
+++ b/indexedDb/impl/state/stateIndexedDbWorker.js
@@ -0,0 +1,25 @@
+////////////////////////////////////////////////////////////////////////////////
+// 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();
+go.argv = [
+    '--logLevel=2',
+    '--threadLogLevel=2',
+]
+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 b657240b7e7018a031365ffc8514c3fe54141471..d9c2fa8ed1eeb32ee7a71a879e5ae2e405f01e69 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"
 )
@@ -32,6 +32,13 @@ const (
 	ErrDoesNotExist = "result is undefined"
 )
 
+// WebState defines an interface for setting persistent state in a KV format
+// specifically for web-based implementations.
+type WebState interface {
+	Get(key string) ([]byte, error)
+	Set(key string, value []byte) error
+}
+
 // NewContext builds a context for indexedDb operations.
 func NewContext() (context.Context, context.CancelFunc) {
 	return context.WithTimeout(context.Background(), dbTimeout)
diff --git a/indexedDb/worker/channels/implementation.go b/indexedDb/worker/channels/implementation.go
index 9639bc185095e74cbf4b3e63256fead39bbcef82..d6495de4fae213db99481992e38306a49f2bcd4d 100644
--- a/indexedDb/worker/channels/implementation.go
+++ b/indexedDb/worker/channels/implementation.go
@@ -41,12 +41,17 @@ func (w *wasmModel) JoinChannel(channel *cryptoBroadcast.Channel) {
 		return
 	}
 
-	w.wm.SendMessage(JoinChannelTag, data, nil)
+	if err = w.wm.SendNoResponse(JoinChannelTag, data); err != nil {
+		jww.FATAL.Panicf("[CH] Failed to send to %q: %+v", JoinChannelTag, err)
+	}
 }
 
 // LeaveChannel is called whenever a channel is left locally.
 func (w *wasmModel) LeaveChannel(channelID *id.ID) {
-	w.wm.SendMessage(LeaveChannelTag, channelID.Marshal(), nil)
+	err := w.wm.SendNoResponse(LeaveChannelTag, channelID.Marshal())
+	if err != nil {
+		jww.FATAL.Panicf("[CH] Failed to send to %q: %+v", LeaveChannelTag, err)
+	}
 }
 
 // ReceiveMessage is called whenever a message is received on a given channel.
@@ -81,27 +86,20 @@ func (w *wasmModel) ReceiveMessage(channelID *id.ID, messageID message.ID,
 		return 0
 	}
 
-	uuidChan := make(chan uint64)
-	w.wm.SendMessage(ReceiveMessageTag, data, func(data []byte) {
-		var uuid uint64
-		err = json.Unmarshal(data, &uuid)
-		if err != nil {
-			jww.ERROR.Printf("[CH] Could not JSON unmarshal response to "+
-				"ReceiveMessage: %+v", err)
-			uuidChan <- 0
-		}
-		uuidChan <- uuid
-	})
+	response, err := w.wm.SendMessage(ReceiveMessageTag, data)
+	if err != nil {
+		jww.FATAL.Panicf(
+			"[CH] Failed to send to %q: %+v", ReceiveMessageTag, err)
+	}
 
-	select {
-	case uuid := <-uuidChan:
-		return uuid
-	case <-time.After(worker.ResponseTimeout):
-		jww.ERROR.Printf("[CH] Timed out after %s waiting for response from "+
-			"the worker about ReceiveMessage", worker.ResponseTimeout)
+	var uuid uint64
+	if err = json.Unmarshal(response, &uuid); err != nil {
+		jww.ERROR.Printf("[CH] Failed to JSON unmarshal UUID from worker for "+
+			"%q: %+v", ReceiveMessageTag, err)
+		return 0
 	}
 
-	return 0
+	return uuid
 }
 
 // ReceiveReplyMessage is JSON marshalled and sent to the worker for
@@ -149,27 +147,20 @@ func (w *wasmModel) ReceiveReply(channelID *id.ID, messageID,
 		return 0
 	}
 
-	uuidChan := make(chan uint64)
-	w.wm.SendMessage(ReceiveReplyTag, data, func(data []byte) {
-		var uuid uint64
-		err = json.Unmarshal(data, &uuid)
-		if err != nil {
-			jww.ERROR.Printf("[CH] Could not JSON unmarshal response to "+
-				"ReceiveReply: %+v", err)
-			uuidChan <- 0
-		}
-		uuidChan <- uuid
-	})
+	response, err := w.wm.SendMessage(ReceiveReplyTag, data)
+	if err != nil {
+		jww.FATAL.Panicf(
+			"[CH] Failed to send to %q: %+v", ReceiveReplyTag, err)
+	}
 
-	select {
-	case uuid := <-uuidChan:
-		return uuid
-	case <-time.After(worker.ResponseTimeout):
-		jww.ERROR.Printf("[CH] Timed out after %s waiting for response from "+
-			"the worker about ReceiveReply", worker.ResponseTimeout)
+	var uuid uint64
+	if err = json.Unmarshal(response, &uuid); err != nil {
+		jww.ERROR.Printf("[CH] Failed to JSON unmarshal UUID from worker for "+
+			"%q: %+v", ReceiveReplyTag, err)
+		return 0
 	}
 
-	return 0
+	return uuid
 }
 
 // ReceiveReaction is called whenever a reaction to a message is received on a
@@ -211,27 +202,20 @@ func (w *wasmModel) ReceiveReaction(channelID *id.ID, messageID,
 		return 0
 	}
 
-	uuidChan := make(chan uint64)
-	w.wm.SendMessage(ReceiveReactionTag, data, func(data []byte) {
-		var uuid uint64
-		err = json.Unmarshal(data, &uuid)
-		if err != nil {
-			jww.ERROR.Printf("[CH] Could not JSON unmarshal response to "+
-				"ReceiveReaction: %+v", err)
-			uuidChan <- 0
-		}
-		uuidChan <- uuid
-	})
+	response, err := w.wm.SendMessage(ReceiveReactionTag, data)
+	if err != nil {
+		jww.FATAL.Panicf(
+			"[CH] Failed to send to %q: %+v", ReceiveReactionTag, err)
+	}
 
-	select {
-	case uuid := <-uuidChan:
-		return uuid
-	case <-time.After(worker.ResponseTimeout):
-		jww.ERROR.Printf("[CH] Timed out after %s waiting for response from "+
-			"the worker about ReceiveReply", worker.ResponseTimeout)
+	var uuid uint64
+	if err = json.Unmarshal(response, &uuid); err != nil {
+		jww.ERROR.Printf("[CH] Failed to JSON unmarshal UUID from worker for "+
+			"%q: %+v", ReceiveReactionTag, err)
+		return 0
 	}
 
-	return 0
+	return uuid
 }
 
 // MessageUpdateInfo is JSON marshalled and sent to the worker for
@@ -301,29 +285,22 @@ func (w *wasmModel) UpdateFromUUID(uuid uint64, messageID *message.ID,
 			"could not JSON marshal payload for UpdateFromUUID: %+v", err)
 	}
 
-	errChan := make(chan error)
-	w.wm.SendMessage(UpdateFromUUIDTag, data, func(data []byte) {
-		if data != nil {
-			errChan <- errors.New(string(data))
-		} else {
-			errChan <- nil
-		}
-	})
-
-	select {
-	case err = <-errChan:
-		return err
-	case <-time.After(worker.ResponseTimeout):
-		return errors.Errorf("timed out after %s waiting for response from "+
-			"the worker about UpdateFromUUID", worker.ResponseTimeout)
+	response, err := w.wm.SendMessage(UpdateFromUUIDTag, data)
+	if err != nil {
+		jww.FATAL.Panicf(
+			"[CH] Failed to send to %q: %+v", UpdateFromUUIDTag, err)
+	} else if len(response) > 0 {
+		return errors.New(string(response))
 	}
+
+	return nil
 }
 
 // UuidError is JSON marshalled and sent to the worker for
 // [wasmModel.UpdateFromMessageID].
 type UuidError struct {
 	UUID  uint64 `json:"uuid"`
-	Error []byte `json:"error"`
+	Error string `json:"error"`
 }
 
 // UpdateFromMessageID is called whenever a message with the message ID is
@@ -367,29 +344,20 @@ func (w *wasmModel) UpdateFromMessageID(messageID message.ID,
 			"UpdateFromMessageID: %+v", err)
 	}
 
-	uuidChan := make(chan uint64)
-	errChan := make(chan error)
-	w.wm.SendMessage(UpdateFromMessageIDTag, data,
-		func(data []byte) {
-			var ue UuidError
-			if err = json.Unmarshal(data, &ue); err != nil {
-				errChan <- errors.Errorf("could not JSON unmarshal response "+
-					"to UpdateFromMessageID: %+v", err)
-			} else if ue.Error != nil {
-				errChan <- errors.New(string(ue.Error))
-			} else {
-				uuidChan <- ue.UUID
-			}
-		})
-
-	select {
-	case uuid := <-uuidChan:
-		return uuid, nil
-	case err = <-errChan:
-		return 0, err
-	case <-time.After(worker.ResponseTimeout):
-		return 0, errors.Errorf("timed out after %s waiting for response from "+
-			"the worker about UpdateFromMessageID", worker.ResponseTimeout)
+	response, err := w.wm.SendMessage(UpdateFromMessageIDTag, data)
+	if err != nil {
+		jww.FATAL.Panicf(
+			"[CH] Failed to send to %q: %+v", UpdateFromMessageIDTag, err)
+	}
+
+	var ue UuidError
+	if err = json.Unmarshal(response, &ue); err != nil {
+		return 0, errors.Errorf("could not JSON unmarshal response to %q: %+v",
+			UpdateFromMessageIDTag, err)
+	} else if len(ue.Error) > 0 {
+		return 0, errors.New(ue.Error)
+	} else {
+		return ue.UUID, nil
 	}
 }
 
@@ -403,50 +371,37 @@ type GetMessageMessage struct {
 // GetMessage returns the message with the given [channel.MessageID].
 func (w *wasmModel) GetMessage(
 	messageID message.ID) (channels.ModelMessage, error) {
-	msgChan := make(chan GetMessageMessage)
-	w.wm.SendMessage(GetMessageTag, messageID.Marshal(),
-		func(data []byte) {
-			var msg GetMessageMessage
-			err := json.Unmarshal(data, &msg)
-			if err != nil {
-				jww.ERROR.Printf("[CH] Could not JSON unmarshal response to "+
-					"GetMessage: %+v", err)
-			}
-			msgChan <- msg
-		})
-
-	select {
-	case msg := <-msgChan:
-		if msg.Error != "" {
-			return channels.ModelMessage{}, errors.New(msg.Error)
-		}
-		return msg.Message, nil
-	case <-time.After(worker.ResponseTimeout):
-		return channels.ModelMessage{}, errors.Errorf("timed out after %s "+
-			"waiting for response from the worker about GetMessage",
-			worker.ResponseTimeout)
+
+	response, err := w.wm.SendMessage(GetMessageTag, messageID.Marshal())
+	if err != nil {
+		jww.FATAL.Panicf(
+			"[CH] Failed to send to %q: %+v", GetMessageTag, err)
+	}
+
+	var msg GetMessageMessage
+	if err = json.Unmarshal(response, &msg); err != nil {
+		return channels.ModelMessage{}, errors.Wrapf(err,
+			"[CH] Could not JSON unmarshal response to %q", GetMessageTag)
 	}
+
+	if msg.Error != "" {
+		return channels.ModelMessage{}, errors.New(msg.Error)
+	}
+
+	return msg.Message, nil
 }
 
 // DeleteMessage removes a message with the given messageID from storage.
 func (w *wasmModel) DeleteMessage(messageID message.ID) error {
-	errChan := make(chan error)
-	w.wm.SendMessage(DeleteMessageTag, messageID.Marshal(),
-		func(data []byte) {
-			if data != nil {
-				errChan <- errors.New(string(data))
-			} else {
-				errChan <- nil
-			}
-		})
-
-	select {
-	case err := <-errChan:
-		return err
-	case <-time.After(worker.ResponseTimeout):
-		return errors.Errorf("timed out after %s waiting for response from "+
-			"the worker about DeleteMessage", worker.ResponseTimeout)
+	response, err := w.wm.SendMessage(DeleteMessageTag, messageID.Marshal())
+	if err != nil {
+		jww.FATAL.Panicf(
+			"[CH] Failed to send to %q: %+v", DeleteMessageTag, err)
+	} else if len(response) > 0 {
+		return errors.New(string(response))
 	}
+
+	return nil
 }
 
 // MuteUserMessage is JSON marshalled and sent to the worker for
@@ -472,5 +427,8 @@ func (w *wasmModel) MuteUser(
 		return
 	}
 
-	w.wm.SendMessage(MuteUserTag, data, nil)
+	err = w.wm.SendNoResponse(MuteUserTag, data)
+	if err != nil {
+		jww.FATAL.Panicf("[CH] Failed to send to %q: %+v", MuteUserTag, err)
+	}
 }
diff --git a/indexedDb/worker/channels/init.go b/indexedDb/worker/channels/init.go
index 2ee630caf43177460a19f14d2b953d3a03b9c687..7dbc56e6b77499e6c92dfb9903a72e06af77383a 100644
--- a/indexedDb/worker/channels/init.go
+++ b/indexedDb/worker/channels/init.go
@@ -10,47 +10,30 @@
 package channels
 
 import (
-	"crypto/ed25519"
 	"encoding/json"
+
 	"github.com/pkg/errors"
-	"time"
 
 	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"
+	idbCrypto "gitlab.com/elixxir/crypto/indexedDb"
+	"gitlab.com/elixxir/xxdk-wasm/logging"
 	"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)
-
 // 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 {
+func NewWASMEventModelBuilder(wasmJsPath string, encryption idbCrypto.Cipher,
+	channelCbs bindings.ChannelUICallbacks) channels.EventModelBuilder {
 	fn := func(path string) (channels.EventModel, error) {
 		return NewWASMEventModel(path, wasmJsPath, encryption,
-			messageReceivedCB, deletedMessageCB, mutedUserCB)
+			channelCbs)
 	}
 	return fn
 }
@@ -64,10 +47,8 @@ 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) (
-	channels.EventModel, error) {
+func NewWASMEventModel(path, wasmJsPath string, encryption idbCrypto.Cipher,
+	cbs bindings.ChannelUICallbacks) (channels.EventModel, error) {
 	databaseName := path + databaseSuffix
 
 	wm, err := worker.NewManager(wasmJsPath, "channelsIndexedDb", true)
@@ -75,17 +56,17 @@ 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 EventUpdate
+	wm.RegisterCallback(EventUpdateCallbackTag, eventUpdateCallbackHandler(cbs))
 
-	// Register handler to manage messages for the MutedUserCallback
-	wm.RegisterCallback(MutedUserCallbackTag,
-		mutedUserCallbackHandler(mutedUserCB))
+	// Create MessageChannel between worker and logger so that the worker logs
+	// are saved
+	err = worker.CreateMessageChannel(logging.GetLogger().Worker(), wm,
+		"channelsIndexedDbLogger", worker.LoggerTag)
+	if err != nil {
+		return nil, errors.Wrap(err, "Failed to create message channel "+
+			"between channel indexedDb worker and logger")
+	}
 
 	// Store the database name
 	err = storage.StoreIndexedDb(databaseName)
@@ -115,74 +96,37 @@ func NewWASMEventModel(path, wasmJsPath string, encryption cryptoChannel.Cipher,
 		return nil, err
 	}
 
-	dataChan := make(chan []byte)
-	wm.SendMessage(NewWASMEventModelTag, 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)
+	response, err := wm.SendMessage(NewWASMEventModelTag, payload)
+	if err != nil {
+		return nil, errors.Wrapf(err,
+			"failed to send message %q", NewWASMEventModelTag)
+	} else if len(response) > 0 {
+		return nil, errors.New(string(response))
 	}
 
 	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"`
-}
-
-// 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)
-	}
+// EventUpdateCallbackMessage is JSON marshalled and received from the worker
+// for the EventUpdate callback.
+type EventUpdateCallbackMessage struct {
+	EventType int64  `json:"eventType"`
+	JsonData  []byte `json:"jsonData"`
 }
 
-// mutedUserCallbackHandler returns a handler to manage messages for the
-// MutedUserCallback.
-func mutedUserCallbackHandler(cb MutedUserCallback) func(data []byte) {
-	return func(data []byte) {
-		var msg MuteUserMessage
-		err := json.Unmarshal(data, &msg)
-		if err != nil {
+// eventUpdateCallbackHandler returns a handler to manage messages for the
+// [bindings.ChannelUICallbacks.EventUpdate] callback.
+func eventUpdateCallbackHandler(
+	cbs bindings.ChannelUICallbacks) worker.ReceiverCallback {
+	return func(message []byte, _ func([]byte)) {
+		var msg EventUpdateCallbackMessage
+		if err := json.Unmarshal(message, &msg); err != nil {
 			jww.ERROR.Printf(
 				"Failed to JSON unmarshal %T from worker: %+v", msg, err)
 			return
 		}
 
-		cb(msg.ChannelID, msg.PubKey, msg.Unmute)
+		cbs.EventUpdate(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/dm/implementation.go b/indexedDb/worker/dm/implementation.go
index 657f8a488705b38e55e9d75b825330c07b583ce0..3104eb48ae12a78afb0ae654230f1c2ac85584a1 100644
--- a/indexedDb/worker/dm/implementation.go
+++ b/indexedDb/worker/dm/implementation.go
@@ -29,19 +29,19 @@ type wasmModel struct {
 
 // TransferMessage is JSON marshalled and sent to the worker.
 type TransferMessage struct {
-	UUID       uint64            `json:"uuid"`
-	MessageID  message.ID        `json:"messageID"`
-	ReactionTo message.ID        `json:"reactionTo"`
-	Nickname   string            `json:"nickname"`
-	Text       []byte            `json:"text"`
-	PartnerKey ed25519.PublicKey `json:"partnerKey"`
-	SenderKey  ed25519.PublicKey `json:"senderKey"`
-	DmToken    uint32            `json:"dmToken"`
-	Codeset    uint8             `json:"codeset"`
+	UUID       uint64            `json:"uuid,omitempty"`
+	MessageID  message.ID        `json:"messageID,omitempty"`
+	ReactionTo message.ID        `json:"reactionTo,omitempty"`
+	Nickname   string            `json:"nickname,omitempty"`
+	Text       []byte            `json:"text,omitempty"`
+	PartnerKey ed25519.PublicKey `json:"partnerKey,omitempty"`
+	SenderKey  ed25519.PublicKey `json:"senderKey,omitempty"`
+	DmToken    uint32            `json:"dmToken,omitempty"`
+	Codeset    uint8             `json:"codeset,omitempty"`
 	Timestamp  time.Time         `json:"timestamp"`
 	Round      rounds.Round      `json:"round"`
-	MType      dm.MessageType    `json:"mType"`
-	Status     dm.Status         `json:"status"`
+	MType      dm.MessageType    `json:"mType,omitempty"`
+	Status     dm.Status         `json:"status,omitempty"`
 }
 
 func (w *wasmModel) Receive(messageID message.ID, nickname string, text []byte,
@@ -64,31 +64,23 @@ func (w *wasmModel) Receive(messageID message.ID, nickname string, text []byte,
 	data, err := json.Marshal(msg)
 	if err != nil {
 		jww.ERROR.Printf(
-			"Could not JSON marshal payload for TransferMessage: %+v", err)
+			"[DM] Could not JSON marshal payload for Receive: %+v", err)
 		return 0
 	}
 
-	uuidChan := make(chan uint64)
-	w.wh.SendMessage(ReceiveTag, data, func(data []byte) {
-		var uuid uint64
-		err = json.Unmarshal(data, &uuid)
-		if err != nil {
-			jww.ERROR.Printf(
-				"Could not JSON unmarshal response to Receive: %+v", err)
-			uuidChan <- 0
-		}
-		uuidChan <- uuid
-	})
-
-	select {
-	case uuid := <-uuidChan:
-		return uuid
-	case <-time.After(worker.ResponseTimeout):
-		jww.ERROR.Printf("Timed out after %s waiting for response from the "+
-			"worker about Receive", worker.ResponseTimeout)
+	response, err := w.wh.SendMessage(ReceiveTag, data)
+	if err != nil {
+		jww.FATAL.Panicf("[DM] Failed to send to %q: %+v", ReceiveTag, err)
+	}
+
+	var uuid uint64
+	if err = json.Unmarshal(response, &uuid); err != nil {
+		jww.ERROR.Printf("[DM] Failed to JSON unmarshal UUID from worker for "+
+			"%q: %+v", ReceiveTag, err)
+		return 0
 	}
 
-	return 0
+	return uuid
 }
 
 func (w *wasmModel) ReceiveText(messageID message.ID, nickname, text string,
@@ -114,27 +106,19 @@ func (w *wasmModel) ReceiveText(messageID message.ID, nickname, text string,
 		return 0
 	}
 
-	uuidChan := make(chan uint64)
-	w.wh.SendMessage(ReceiveTextTag, data, func(data []byte) {
-		var uuid uint64
-		err = json.Unmarshal(data, &uuid)
-		if err != nil {
-			jww.ERROR.Printf(
-				"Could not JSON unmarshal response to ReceiveText: %+v", err)
-			uuidChan <- 0
-		}
-		uuidChan <- uuid
-	})
-
-	select {
-	case uuid := <-uuidChan:
-		return uuid
-	case <-time.After(worker.ResponseTimeout):
-		jww.ERROR.Printf("Timed out after %s waiting for response from the "+
-			"worker about ReceiveText", worker.ResponseTimeout)
+	response, err := w.wh.SendMessage(ReceiveTextTag, data)
+	if err != nil {
+		jww.FATAL.Panicf("[DM] Failed to send to %q: %+v", ReceiveTextTag, err)
 	}
 
-	return 0
+	var uuid uint64
+	if err = json.Unmarshal(response, &uuid); err != nil {
+		jww.ERROR.Printf("[DM] Failed to JSON unmarshal UUID from worker for "+
+			"%q: %+v", ReceiveTextTag, err)
+		return 0
+	}
+
+	return uuid
 }
 
 func (w *wasmModel) ReceiveReply(messageID, reactionTo message.ID, nickname,
@@ -161,27 +145,19 @@ func (w *wasmModel) ReceiveReply(messageID, reactionTo message.ID, nickname,
 		return 0
 	}
 
-	uuidChan := make(chan uint64)
-	w.wh.SendMessage(ReceiveReplyTag, data, func(data []byte) {
-		var uuid uint64
-		err = json.Unmarshal(data, &uuid)
-		if err != nil {
-			jww.ERROR.Printf(
-				"Could not JSON unmarshal response to ReceiveReply: %+v", err)
-			uuidChan <- 0
-		}
-		uuidChan <- uuid
-	})
-
-	select {
-	case uuid := <-uuidChan:
-		return uuid
-	case <-time.After(worker.ResponseTimeout):
-		jww.ERROR.Printf("Timed out after %s waiting for response from the "+
-			"worker about ReceiveReply", worker.ResponseTimeout)
+	response, err := w.wh.SendMessage(ReceiveReplyTag, data)
+	if err != nil {
+		jww.FATAL.Panicf("[DM] Failed to send to %q: %+v", ReceiveReplyTag, err)
+	}
+
+	var uuid uint64
+	if err = json.Unmarshal(response, &uuid); err != nil {
+		jww.ERROR.Printf("[DM] Failed to JSON unmarshal UUID from worker for "+
+			"%q: %+v", ReceiveReplyTag, err)
+		return 0
 	}
 
-	return 0
+	return uuid
 }
 
 func (w *wasmModel) ReceiveReaction(messageID, reactionTo message.ID, nickname,
@@ -208,27 +184,19 @@ func (w *wasmModel) ReceiveReaction(messageID, reactionTo message.ID, nickname,
 		return 0
 	}
 
-	uuidChan := make(chan uint64)
-	w.wh.SendMessage(ReceiveReactionTag, data, func(data []byte) {
-		var uuid uint64
-		err = json.Unmarshal(data, &uuid)
-		if err != nil {
-			jww.ERROR.Printf(
-				"Could not JSON unmarshal response to ReceiveReaction: %+v", err)
-			uuidChan <- 0
-		}
-		uuidChan <- uuid
-	})
-
-	select {
-	case uuid := <-uuidChan:
-		return uuid
-	case <-time.After(worker.ResponseTimeout):
-		jww.ERROR.Printf("Timed out after %s waiting for response from the "+
-			"worker about ReceiveReaction", worker.ResponseTimeout)
+	response, err := w.wh.SendMessage(ReceiveReactionTag, data)
+	if err != nil {
+		jww.FATAL.Panicf("[DM] Failed to send to %q: %+v", ReceiveReactionTag, err)
+	}
+
+	var uuid uint64
+	if err = json.Unmarshal(response, &uuid); err != nil {
+		jww.ERROR.Printf("[DM] Failed to JSON unmarshal UUID from worker for "+
+			"%q: %+v", ReceiveReactionTag, err)
+		return 0
 	}
 
-	return 0
+	return uuid
 }
 
 func (w *wasmModel) UpdateSentStatus(uuid uint64, messageID message.ID,
@@ -247,59 +215,63 @@ func (w *wasmModel) UpdateSentStatus(uuid uint64, messageID message.ID,
 			"Could not JSON marshal payload for TransferMessage: %+v", err)
 	}
 
-	w.wh.SendMessage(UpdateSentStatusTag, data, nil)
+	if err = w.wh.SendNoResponse(UpdateSentStatusTag, data); err != nil {
+		jww.FATAL.Panicf("[DM] Failed to send to %q: %+v", UpdateSentStatusTag, err)
+	}
 }
 
-func (w *wasmModel) BlockSender(senderPubKey ed25519.PublicKey) {
-	w.wh.SendMessage(BlockSenderTag, senderPubKey, nil)
-}
+func (w *wasmModel) DeleteMessage(
+	messageID message.ID, senderPubKey ed25519.PublicKey) bool {
+	msg := TransferMessage{
+		MessageID: messageID,
+		SenderKey: senderPubKey,
+	}
+
+	data, err := json.Marshal(msg)
+	if err != nil {
+		jww.ERROR.Printf(
+			"Could not JSON marshal payload for TransferMessage: %+v", err)
+	}
 
-func (w *wasmModel) UnblockSender(senderPubKey ed25519.PublicKey) {
-	w.wh.SendMessage(UnblockSenderTag, senderPubKey, nil)
+	response, err := w.wh.SendMessage(DeleteMessageTag, data)
+	if err != nil {
+		jww.FATAL.Panicf("[DM] Failed to send to %q: %+v", DeleteMessageTag, err)
+	} else if len(response) == 0 {
+		jww.FATAL.Panicf(
+			"[DM] Received empty response from %q", DeleteMessageTag)
+	}
+
+	return response[0] == 1
 }
 
 func (w *wasmModel) GetConversation(senderPubKey ed25519.PublicKey) *dm.ModelConversation {
-	resultChan := make(chan *dm.ModelConversation)
-	w.wh.SendMessage(GetConversationTag, senderPubKey,
-		func(data []byte) {
-			var result *dm.ModelConversation
-			err := json.Unmarshal(data, &result)
-			if err != nil {
-				jww.ERROR.Printf("Could not JSON unmarshal response to "+
-					"GetConversation: %+v", err)
-			}
-			resultChan <- result
-		})
-
-	select {
-	case result := <-resultChan:
-		return result
-	case <-time.After(worker.ResponseTimeout):
-		jww.ERROR.Printf("Timed out after %s waiting for response from the "+
-			"worker about GetConversation", worker.ResponseTimeout)
+	response, err := w.wh.SendMessage(GetConversationTag, senderPubKey)
+	if err != nil {
+		jww.FATAL.Panicf("[DM] Failed to send to %q: %+v", GetConversationTag, err)
+	}
+
+	var result dm.ModelConversation
+	if err = json.Unmarshal(response, &result); err != nil {
+		jww.ERROR.Printf("[DM] Failed to JSON unmarshal %T from worker for "+
+			"%q: %+v", result, GetConversationTag, err)
 		return nil
 	}
+
+	return &result
 }
 
 func (w *wasmModel) GetConversations() []dm.ModelConversation {
-	resultChan := make(chan []dm.ModelConversation)
-	w.wh.SendMessage(GetConversationTag, nil,
-		func(data []byte) {
-			var result []dm.ModelConversation
-			err := json.Unmarshal(data, &result)
-			if err != nil {
-				jww.ERROR.Printf("Could not JSON unmarshal response to "+
-					"GetConversations: %+v", err)
-			}
-			resultChan <- result
-		})
-
-	select {
-	case result := <-resultChan:
-		return result
-	case <-time.After(worker.ResponseTimeout):
-		jww.ERROR.Printf("Timed out after %s waiting for response from the "+
-			"worker about GetConversations", worker.ResponseTimeout)
+	response, err := w.wh.SendMessage(GetConversationsTag, nil)
+	if err != nil {
+		jww.FATAL.Panicf("[DM] Failed to send to %q: %+v", GetConversationsTag, err)
+	}
+
+	var result []dm.ModelConversation
+	if err = json.Unmarshal(response, &result); err != nil {
+		jww.ERROR.Printf("[DM] Failed to JSON unmarshal %T from worker for "+
+			"%q: %+v", result, GetConversationsTag, err)
 		return nil
 	}
+
+	return result
 }
diff --git a/indexedDb/worker/dm/init.go b/indexedDb/worker/dm/init.go
index 3fd1cd13897bdc8684e2efc13750e1fed20ff000..4e084aa29f92f71d296ae85ee39187f38aacc3b8 100644
--- a/indexedDb/worker/dm/init.go
+++ b/indexedDb/worker/dm/init.go
@@ -12,13 +12,14 @@ package dm
 import (
 	"crypto/ed25519"
 	"encoding/json"
-	"time"
 
 	"github.com/pkg/errors"
 	jww "github.com/spf13/jwalterweatherman"
 
+	"gitlab.com/elixxir/client/v4/bindings"
 	"gitlab.com/elixxir/client/v4/dm"
-	cryptoChannel "gitlab.com/elixxir/crypto/channel"
+	idbCrypto "gitlab.com/elixxir/crypto/indexedDb"
+	"gitlab.com/elixxir/xxdk-wasm/logging"
 	"gitlab.com/elixxir/xxdk-wasm/storage"
 	"gitlab.com/elixxir/xxdk-wasm/worker"
 )
@@ -42,8 +43,8 @@ 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,
-	cb MessageReceivedCallback) (dm.EventModel, error) {
+func NewWASMEventModel(path, wasmJsPath string, encryption idbCrypto.Cipher,
+	cbs bindings.DmCallbacks) (dm.EventModel, error) {
 	databaseName := path + databaseSuffix
 
 	wh, err := worker.NewManager(wasmJsPath, "dmIndexedDb", true)
@@ -52,8 +53,16 @@ func NewWASMEventModel(path, wasmJsPath string, encryption cryptoChannel.Cipher,
 	}
 
 	// Register handler to manage messages for the MessageReceivedCallback
-	wh.RegisterCallback(
-		MessageReceivedCallbackTag, messageReceivedCallbackHandler(cb))
+	wh.RegisterCallback(EventUpdateCallbackTag, eventUpdateCallbackHandler(cbs))
+
+	// Create MessageChannel between worker and logger so that the worker logs
+	// are saved
+	err = worker.CreateMessageChannel(logging.GetLogger().Worker(), wh,
+		"dmIndexedDbLogger", worker.LoggerTag)
+	if err != nil {
+		return nil, errors.Wrap(err, "Failed to create message channel "+
+			"between DM indexedDb worker and logger")
+	}
 
 	// Store the database name
 	err = storage.StoreIndexedDb(databaseName)
@@ -83,44 +92,37 @@ func NewWASMEventModel(path, wasmJsPath string, encryption cryptoChannel.Cipher,
 		return nil, err
 	}
 
-	dataChan := make(chan []byte)
-	wh.SendMessage(NewWASMEventModelTag, 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)
+	response, err := wh.SendMessage(NewWASMEventModelTag, payload)
+	if err != nil {
+		return nil, errors.Wrapf(err,
+			"failed to send message %q", NewWASMEventModelTag)
+	} else if len(response) > 0 {
+		return nil, errors.New(string(response))
 	}
 
 	return &wasmModel{wh}, nil
 }
 
-// MessageReceivedCallbackMessage is JSON marshalled and received from the
-// worker for the [MessageReceivedCallback] callback.
-type MessageReceivedCallbackMessage struct {
-	UUID               uint64            `json:"uuid"`
-	PubKey             ed25519.PublicKey `json:"pubKey"`
-	MessageUpdate      bool              `json:"message_update"`
-	ConversationUpdate bool              `json:"conversation_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 "+
-				"MessageReceivedCallback message from worker: %+v", err)
+// eventUpdateCallbackHandler returns a handler to manage messages for the
+// [bindings.DmCallbacks.EventUpdate] callback.
+func eventUpdateCallbackHandler(
+	cbs bindings.DmCallbacks) worker.ReceiverCallback {
+	return func(message []byte, _ func([]byte)) {
+		var msg EventUpdateCallbackMessage
+		if err := json.Unmarshal(message, &msg); err != nil {
+			jww.ERROR.Printf(
+				"Failed to JSON unmarshal %T from worker: %+v", msg, err)
 			return
 		}
-		cb(msg.UUID, msg.PubKey, msg.MessageUpdate, msg.ConversationUpdate)
+
+		cbs.EventUpdate(msg.EventType, msg.JsonData)
 	}
 }
 
diff --git a/indexedDb/worker/dm/tags.go b/indexedDb/worker/dm/tags.go
index b71762421a15003123071954312e290e5677a24e..6679c9be4bd72026a8baf432fb43347dccc7bb53 100644
--- a/indexedDb/worker/dm/tags.go
+++ b/indexedDb/worker/dm/tags.go
@@ -14,17 +14,16 @@ 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"
+	NewWASMEventModelTag   worker.Tag = "NewWASMEventModel"
+	EventUpdateCallbackTag worker.Tag = "EventUpdateCallback"
 
 	ReceiveReplyTag     worker.Tag = "ReceiveReply"
 	ReceiveReactionTag  worker.Tag = "ReceiveReaction"
 	ReceiveTag          worker.Tag = "Receive"
 	ReceiveTextTag      worker.Tag = "ReceiveText"
-	UpdateSentStatusTag worker.Tag = "UpdateSentStatusTag"
+	UpdateSentStatusTag worker.Tag = "UpdateSentStatus"
+	DeleteMessageTag    worker.Tag = "DeleteMessage"
 
-	BlockSenderTag      worker.Tag = "BlockSender"
-	UnblockSenderTag    worker.Tag = "UnblockSender"
 	GetConversationTag  worker.Tag = "GetConversation"
 	GetConversationsTag worker.Tag = "GetConversations"
 )
diff --git a/indexedDb/worker/state/implementation.go b/indexedDb/worker/state/implementation.go
new file mode 100644
index 0000000000000000000000000000000000000000..17eb1228a873a54131f6a48dedbcd2afc4c312a0
--- /dev/null
+++ b/indexedDb/worker/state/implementation.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"
+
+	"github.com/pkg/errors"
+	jww "github.com/spf13/jwalterweatherman"
+
+	"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)
+	}
+
+	response, err := w.wh.SendMessage(SetTag, data)
+	if err != nil {
+		jww.FATAL.Panicf("Failed to send message to %q: %+v", SetTag, err)
+	} else if len(response) > 0 {
+		return errors.New(string(response))
+	}
+
+	return nil
+}
+
+func (w *wasmModel) Get(key string) ([]byte, error) {
+
+	response, err := w.wh.SendMessage(GetTag, []byte(key))
+	if err != nil {
+		jww.FATAL.Panicf("Failed to send message to %q: %+v", GetTag, err)
+	}
+
+	var msg TransferMessage
+	if err = json.Unmarshal(response, &msg); err != nil {
+		return nil, errors.Errorf(
+			"failed to JSON unmarshal %T from worker: %+v", msg, err)
+	}
+
+	if len(msg.Error) > 0 {
+		return nil, errors.New(msg.Error)
+	}
+
+	return msg.Value, nil
+}
diff --git a/indexedDb/worker/state/init.go b/indexedDb/worker/state/init.go
new file mode 100644
index 0000000000000000000000000000000000000000..4477924c77f3a35e27be1178cb15cfa0957455d6
--- /dev/null
+++ b/indexedDb/worker/state/init.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"
+	jww "github.com/spf13/jwalterweatherman"
+
+	"gitlab.com/elixxir/xxdk-wasm/indexedDb/impl"
+	"gitlab.com/elixxir/xxdk-wasm/logging"
+	"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"`
+}
+
+// WebState defines an interface for setting persistent state in a KV format
+// specifically for web-based implementations.
+type WebState interface {
+	Get(key string) ([]byte, error)
+	Set(key string, value []byte) error
+}
+
+// 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) (impl.WebState, error) {
+	databaseName := path + databaseSuffix
+
+	wh, err := worker.NewManager(wasmJsPath, "stateIndexedDb", true)
+	if err != nil {
+		return nil, err
+	}
+
+	// Create MessageChannel between worker and logger so that the worker logs
+	// are saved
+	err = worker.CreateMessageChannel(logging.GetLogger().Worker(), wh,
+		"stateIndexedDbLogger", worker.LoggerTag)
+	if err != nil {
+		return nil, errors.Wrap(err, "Failed to create message channel "+
+			"between state indexedDb worker and logger")
+	}
+
+	// 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
+	}
+
+	response, err := wh.SendMessage(NewStateTag, payload)
+	if err != nil {
+		jww.FATAL.Panicf("Failed to send message to %q: %+v", NewStateTag, err)
+	} else if len(response) > 0 {
+		return nil, errors.New(string(response))
+	}
+
+	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/console.go b/logging/console.go
index 8c2edd821210d338f73ff18bfabedfc2f319d7d9..bfc249d1f3284ef856c2692060284c5493a0d900 100644
--- a/logging/console.go
+++ b/logging/console.go
@@ -10,9 +10,10 @@
 package logging
 
 import (
-	jww "github.com/spf13/jwalterweatherman"
 	"io"
 	"syscall/js"
+
+	jww "github.com/spf13/jwalterweatherman"
 )
 
 var consoleObj = js.Global().Get("console")
diff --git a/logging/fileLogger_test.go b/logging/fileLogger_test.go
index 317f69544a927280827aa4f3739c7f1e39d30ac4..c28e89cc7a799d0750548e9b84353d21fc551ff4 100644
--- a/logging/fileLogger_test.go
+++ b/logging/fileLogger_test.go
@@ -11,11 +11,12 @@ package logging
 
 import (
 	"bytes"
-	"github.com/armon/circbuf"
-	jww "github.com/spf13/jwalterweatherman"
 	"math/rand"
 	"reflect"
 	"testing"
+
+	"github.com/armon/circbuf"
+	jww "github.com/spf13/jwalterweatherman"
 )
 
 func Test_newFileLogger(t *testing.T) {
diff --git a/logging/jwwListeners.go b/logging/jwwListeners.go
deleted file mode 100644
index 7d83f3d5bcc69190c505dfd0d0676fbe6104d1d0..0000000000000000000000000000000000000000
--- a/logging/jwwListeners.go
+++ /dev/null
@@ -1,78 +0,0 @@
-////////////////////////////////////////////////////////////////////////////////
-// Copyright © 2022 xx foundation                                             //
-//                                                                            //
-// Use of this source code is governed by a license that can be found in the  //
-// LICENSE file.                                                              //
-////////////////////////////////////////////////////////////////////////////////
-
-package logging
-
-import (
-	jww "github.com/spf13/jwalterweatherman"
-	"sync"
-)
-
-// logListeners contains a map of all registered log listeners keyed on a unique
-// ID that can be used to remove the listener once it has been added. This
-// global keeps track of all listeners that are registered to jwalterweatherman
-// logging.
-var logListeners = newLogListenerList()
-
-type logListenerList struct {
-	listeners map[uint64]jww.LogListener
-	currentID uint64
-	sync.Mutex
-}
-
-func newLogListenerList() logListenerList {
-	return logListenerList{
-		listeners: make(map[uint64]jww.LogListener),
-		currentID: 0,
-	}
-}
-
-// AddLogListener registers the log listener with jwalterweatherman. Returns a
-// unique ID that can be used to remove the listener.
-func AddLogListener(ll jww.LogListener) uint64 {
-	logListeners.Lock()
-	defer logListeners.Unlock()
-
-	id := logListeners.addLogListener(ll)
-	jww.SetLogListeners(logListeners.mapToSlice()...)
-	return id
-}
-
-// RemoveLogListener unregisters the log listener with the ID from
-// jwalterweatherman.
-func RemoveLogListener(id uint64) {
-	logListeners.Lock()
-	defer logListeners.Unlock()
-
-	logListeners.removeLogListener(id)
-	jww.SetLogListeners(logListeners.mapToSlice()...)
-
-}
-
-// addLogListener adds the listener to the list and returns its unique ID.
-func (lll *logListenerList) addLogListener(ll jww.LogListener) uint64 {
-	id := lll.currentID
-	lll.currentID++
-	lll.listeners[id] = ll
-
-	return id
-}
-
-// removeLogListener removes the listener with the specified ID from the list.
-func (lll *logListenerList) removeLogListener(id uint64) {
-	delete(lll.listeners, id)
-}
-
-// mapToSlice converts the map of listeners to a slice of listeners so that it
-// can be registered with jwalterweatherman.SetLogListeners.
-func (lll *logListenerList) mapToSlice() []jww.LogListener {
-	listeners := make([]jww.LogListener, 0, len(lll.listeners))
-	for _, l := range lll.listeners {
-		listeners = append(listeners, l)
-	}
-	return listeners
-}
diff --git a/logging/logger.go b/logging/logger.go
index 3064a00d551a464a486c8f8a23925cf74cea7998..81197d22f98ec09c640f9d1b5fbdd42b9181f926 100644
--- a/logging/logger.go
+++ b/logging/logger.go
@@ -15,7 +15,7 @@ import (
 
 	jww "github.com/spf13/jwalterweatherman"
 
-	"gitlab.com/elixxir/xxdk-wasm/utils"
+	"gitlab.com/elixxir/wasm-utils/utils"
 	"gitlab.com/elixxir/xxdk-wasm/worker"
 )
 
@@ -26,6 +26,7 @@ const (
 	WriteLogTag   worker.Tag = "WriteLog"
 	GetFileTag    worker.Tag = "GetFile"
 	GetFileExtTag worker.Tag = "GetFileExt"
+	MaxSizeTag    worker.Tag = "MaxSize"
 	SizeTag       worker.Tag = "Size"
 )
 
@@ -38,6 +39,7 @@ func GetLogger() Logger {
 	return logger
 }
 
+// Logger controls and accesses the log file for this binary.
 type Logger interface {
 	// StopLogging stops log message writes. Once logging is stopped, it cannot
 	// be resumed and the log file cannot be recovered.
@@ -65,6 +67,21 @@ type Logger interface {
 // worker file buffer. This must be called only once at initialisation.
 func EnableLogging(logLevel, fileLogLevel jww.Threshold, maxLogFileSizeMB int,
 	workerScriptURL, workerName string) error {
+	return enableLogging(logLevel, fileLogLevel, maxLogFileSizeMB,
+		workerScriptURL, workerName, js.Undefined())
+}
+
+// EnableThreadLogging enables logging to the Javascript console and to
+// a local or remote thread file buffer. This must be called only once at
+// initialisation.
+func EnableThreadLogging(logLevel, fileLogLevel jww.Threshold,
+	maxLogFileSizeMB int, workerName string, messagePort js.Value) error {
+	return enableLogging(
+		logLevel, fileLogLevel, maxLogFileSizeMB, "", workerName, messagePort)
+}
+
+func enableLogging(logLevel, fileLogLevel jww.Threshold, maxLogFileSizeMB int,
+	workerScriptURL, workerName string, messagePort js.Value) error {
 
 	var listeners []jww.LogListener
 	if logLevel > -1 {
@@ -80,13 +97,7 @@ func EnableLogging(logLevel, fileLogLevel jww.Threshold, maxLogFileSizeMB int,
 
 	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 {
+		if workerScriptURL != "" {
 			wl, err := newWorkerLogger(
 				fileLogLevel, maxLogFileSize, workerScriptURL, workerName)
 			if err != nil {
@@ -94,6 +105,18 @@ func EnableLogging(logLevel, fileLogLevel jww.Threshold, maxLogFileSizeMB int,
 			}
 
 			listeners = append(listeners, wl.Listen)
+		} else if !messagePort.IsUndefined() {
+			tl, err := newThreadLogger(fileLogLevel, workerName, messagePort)
+			if err != nil {
+				return errors.Wrap(err, "could not initialize logging on message port")
+			}
+			listeners = append(listeners, tl.Listen)
+		} else {
+			fl, err := newFileLogger(fileLogLevel, maxLogFileSize)
+			if err != nil {
+				return errors.Wrap(err, "could not initialize logging to file")
+			}
+			listeners = append(listeners, fl.Listen)
 		}
 
 		js.Global().Set("GetLogger", js.FuncOf(GetLoggerJS))
@@ -121,6 +144,7 @@ func GetLoggerJS(js.Value, []js.Value) any {
 	return newLoggerJS(LoggerJS{GetLogger()})
 }
 
+// LoggerJS is the Javascript wrapper for the Logger.
 type LoggerJS struct {
 	api Logger
 }
diff --git a/logging/workerLogger.go b/logging/workerLogger.go
index bfac4703943c94956a8dc6501b722e7d0e5cad62..41f95f7490a292fa459ecb4e6476592280bdc376 100644
--- a/logging/workerLogger.go
+++ b/logging/workerLogger.go
@@ -14,7 +14,6 @@ import (
 	"encoding/json"
 	"io"
 	"math"
-	"time"
 
 	"github.com/pkg/errors"
 	jww "github.com/spf13/jwalterweatherman"
@@ -22,8 +21,6 @@ import (
 	"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 {
@@ -52,7 +49,7 @@ func newWorkerLogger(threshold jww.Threshold, maxLogFileSize int,
 
 	// 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) {
+	wl.wm.RegisterCallback(GetFileExtTag, func([]byte, func([]byte)) {
 		jww.DEBUG.Print("[LOG] Received file requested from external " +
 			"Javascript. Ignoring file.")
 	})
@@ -63,24 +60,12 @@ func newWorkerLogger(threshold jww.Threshold, maxLogFileSize int,
 	}
 
 	// 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)
+	response, err := wl.wm.SendMessage(NewLogFileTag, data)
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to initialize the log file listener")
+	} else if response != nil {
+		return nil, errors.Wrap(errors.New(string(response)),
+			"failed to initialize the log file listener")
 	}
 
 	jww.FEEDBACK.Printf("[LOG] Outputting log to file of max size %d at level "+
@@ -91,11 +76,9 @@ func newWorkerLogger(threshold jww.Threshold, maxLogFileSize int,
 }
 
 // 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.
+// worker to be added to the file buffer. Always returns the length of p.
 func (wl *workerLogger) Write(p []byte) (n int, err error) {
-	wl.wm.SendMessage(WriteLogTag, p, nil)
-	return len(p), nil
+	return len(p), wl.wm.SendNoResponse(WriteLogTag, p)
 }
 
 // Listen adheres to the [jwalterweatherman.LogListener] type and returns the
@@ -112,23 +95,22 @@ func (wl *workerLogger) Listen(threshold jww.Threshold) io.Writer {
 func (wl *workerLogger) StopLogging() {
 	wl.threshold = math.MaxInt
 
-	wl.wm.Stop()
-	jww.DEBUG.Printf("[LOG] Terminated log worker.")
+	err := wl.wm.Stop()
+	if err != nil {
+		jww.ERROR.Printf("[LOG] Failed to terminate log worker: %+v", err)
+	} else {
+		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
+	response, err := wl.wm.SendMessage(GetFileTag, nil)
+	if err != nil {
+		jww.FATAL.Panicf("[LOG] Failed to get log file from worker: %+v", err)
 	}
+
+	return response
 }
 
 // Threshold returns the log level threshold used in the file.
@@ -143,17 +125,12 @@ func (wl *workerLogger) MaxSize() int {
 
 // 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
+	response, err := wl.wm.SendMessage(SizeTag, nil)
+	if err != nil {
+		jww.FATAL.Panicf("[LOG] Failed to get log size from worker: %+v", err)
 	}
+
+	return int(binary.LittleEndian.Uint64(response))
 }
 
 // Worker returns the manager for the Javascript Worker object.
diff --git a/logging/workerThread/main.go b/logging/workerThread/main.go
index 1a9a31a0ca39ee684b4427eef03fc89f4a407b8c..0c969963651354d5776c98b48ce1149814595953 100644
--- a/logging/workerThread/main.go
+++ b/logging/workerThread/main.go
@@ -17,7 +17,7 @@ import (
 	"syscall/js"
 
 	"github.com/armon/circbuf"
-	"github.com/pkg/errors"
+	"github.com/hack-pad/safejs"
 	"github.com/spf13/cobra"
 	jww "github.com/spf13/jwalterweatherman"
 
@@ -31,8 +31,8 @@ const SEMVER = "0.1.0"
 // workerLogFile manages communication with the main thread and writing incoming
 // logging messages to the log file.
 type workerLogFile struct {
-	wtm *worker.ThreadManager
-	b   *circbuf.Buffer
+	tm *worker.ThreadManager
+	b  *circbuf.Buffer
 }
 
 func main() {
@@ -64,11 +64,15 @@ var LoggerCmd = &cobra.Command{
 
 		jww.INFO.Print("[LOG] Starting xxDK WebAssembly Logger Worker.")
 
-		wlf := workerLogFile{wtm: worker.NewThreadManager("Logger", false)}
+		tm, err := worker.NewThreadManager("Logger", true)
+		if err != nil {
+			jww.FATAL.Panicf("Failed to get new thread manager: %+v", err)
+		}
+		wlf := workerLogFile{tm: tm}
 
 		wlf.registerCallbacks()
 
-		wlf.wtm.SignalReady()
+		wlf.tm.SignalReady()
 
 		// Indicate to the Javascript caller that the WASM is ready by resolving
 		// a promise created by the caller.
@@ -96,54 +100,96 @@ func init() {
 // to get the file and file metadata.
 func (wlf *workerLogFile) registerCallbacks() {
 	// Callback for logging.LogToFileWorker
-	wlf.wtm.RegisterCallback(logging.NewLogFileTag,
-		func(data []byte) ([]byte, error) {
+	wlf.tm.RegisterCallback(logging.NewLogFileTag,
+		func(message []byte, reply func([]byte)) {
+
 			var maxLogFileSize int64
-			err := json.Unmarshal(data, &maxLogFileSize)
+			err := json.Unmarshal(message, &maxLogFileSize)
 			if err != nil {
-				return []byte(err.Error()), err
+				reply([]byte(err.Error()))
+				return
 			}
 
 			wlf.b, err = circbuf.NewBuffer(maxLogFileSize)
 			if err != nil {
-				return []byte(err.Error()), err
+				reply([]byte(err.Error()))
+				return
 			}
 
 			jww.DEBUG.Printf("[LOG] Created new worker log file of size %d",
 				maxLogFileSize)
 
-			return []byte{}, nil
+			reply(nil)
 		})
 
 	// Callback for Logging.GetFile
-	wlf.wtm.RegisterCallback(logging.WriteLogTag,
-		func(data []byte) ([]byte, error) {
-			n, err := wlf.b.Write(data)
+	wlf.tm.RegisterCallback(logging.WriteLogTag,
+		func(message []byte, _ func([]byte)) {
+			n, err := wlf.b.Write(message)
 			if err != nil {
-				return nil, err
-			} else if n != len(data) {
-				return nil, errors.Errorf(
-					"wrote %d bytes; expected %d bytes", n, len(data))
+				jww.ERROR.Printf("[LOG] Failed to write to log: %+v", err)
+			} else if n != len(message) {
+				jww.ERROR.Printf("[LOG] Failed to write to log: wrote %d "+
+					"bytes; expected %d bytes", n, len(message))
 			}
-
-			return nil, nil
 		},
 	)
 
 	// Callback for Logging.GetFile
-	wlf.wtm.RegisterCallback(logging.GetFileTag, func([]byte) ([]byte, error) {
-		return wlf.b.Bytes(), nil
+	wlf.tm.RegisterCallback(logging.GetFileTag,
+		func(_ []byte, reply func([]byte)) { reply(wlf.b.Bytes()) })
+
+	// Callback for Logging.GetFile
+	wlf.tm.RegisterCallback(logging.GetFileExtTag,
+		func(_ []byte, reply func([]byte)) { reply(wlf.b.Bytes()) })
+
+	// Callback for Logging.Size
+	wlf.tm.RegisterCallback(logging.SizeTag, func(_ []byte, reply func([]byte)) {
+		b := make([]byte, 8)
+		binary.LittleEndian.PutUint64(b, uint64(wlf.b.TotalWritten()))
+		reply(b)
 	})
 
+	wlf.tm.RegisterMessageChannelCallback(worker.LoggerTag, wlf.registerLogWorker)
+}
+
+func (wlf *workerLogFile) registerLogWorker(port js.Value, channelName string) {
+	p := worker.DefaultParams()
+	p.MessageLogging = false
+	mm, err := worker.NewMessageManager(
+		safejs.Safe(port), channelName+"-logger", p)
+	if err != nil {
+		jww.FATAL.Panic(err)
+	}
+
+	mm.RegisterCallback(logging.WriteLogTag,
+		func(message []byte, _ func([]byte)) {
+			n, err := wlf.b.Write(message)
+			if err != nil {
+				jww.ERROR.Printf("[LOG] Failed to write to log: %+v", err)
+			} else if n != len(message) {
+				jww.ERROR.Printf("[LOG] Failed to write to log: wrote %d "+
+					"bytes; expected %d bytes", n, len(message))
+			}
+		},
+	)
+
 	// Callback for Logging.GetFile
-	wlf.wtm.RegisterCallback(logging.GetFileExtTag, func([]byte) ([]byte, error) {
-		return wlf.b.Bytes(), nil
+	mm.RegisterCallback(logging.GetFileTag, func(_ []byte, reply func([]byte)) {
+		reply(wlf.b.Bytes())
+	})
+
+	// Callback for Logging.MaxSize
+	mm.RegisterCallback(logging.GetFileTag, func(_ []byte, reply func([]byte)) {
+		b := make([]byte, 8)
+		binary.LittleEndian.PutUint64(b, uint64(wlf.b.Size()))
+		reply(b)
 	})
 
 	// Callback for Logging.Size
-	wlf.wtm.RegisterCallback(logging.SizeTag, func([]byte) ([]byte, error) {
+	mm.RegisterCallback(logging.SizeTag, func(_ []byte, reply func([]byte)) {
 		b := make([]byte, 8)
 		binary.LittleEndian.PutUint64(b, uint64(wlf.b.TotalWritten()))
-		return b, nil
+		reply(b)
 	})
 }
diff --git a/logging/workerThreadLogger.go b/logging/workerThreadLogger.go
new file mode 100644
index 0000000000000000000000000000000000000000..900e82de69e4c3a3397c485c197cb9b093824d69
--- /dev/null
+++ b/logging/workerThreadLogger.go
@@ -0,0 +1,117 @@
+////////////////////////////////////////////////////////////////////////////////
+// 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"
+	"io"
+	"math"
+	"syscall/js"
+
+	"github.com/hack-pad/safejs"
+	jww "github.com/spf13/jwalterweatherman"
+
+	"gitlab.com/elixxir/xxdk-wasm/worker"
+)
+
+// threadLogger manages the recording of jwalterweatherman logs from a worker to
+// the in-memory file buffer in a remote Worker thread.
+type threadLogger struct {
+	threshold jww.Threshold
+	mm        *worker.MessageManager
+}
+
+// newThreadLogger starts logging to an in-memory log file in a remote Worker
+// at the specified threshold. Returns a [threadLogger] that can be used to get
+// the log file.
+func newThreadLogger(threshold jww.Threshold, channelName string,
+	messagePort js.Value) (*threadLogger, error) {
+	p := worker.DefaultParams()
+	p.MessageLogging = false
+	mm, err := worker.NewMessageManager(safejs.Safe(messagePort),
+		channelName+"-worker", p)
+	if err != nil {
+		return nil, err
+	}
+
+	tl := &threadLogger{
+		threshold: threshold,
+		mm:        mm,
+	}
+
+	jww.FEEDBACK.Printf("[LOG] Worker outputting log to file at level "+
+		"%s using web worker", tl.threshold)
+
+	logger = tl
+	return tl, 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 (tl *threadLogger) Write(p []byte) (n int, err error) {
+	return len(p), tl.mm.SendNoResponse(WriteLogTag, p)
+}
+
+// Listen adheres to the [jwalterweatherman.LogListener] type and returns the
+// log writer when the threshold is within the set threshold limit.
+func (tl *threadLogger) Listen(threshold jww.Threshold) io.Writer {
+	if threshold < tl.threshold {
+		return nil
+	}
+	return tl
+}
+
+// StopLogging stops sending log messages to the logging worker. Once logging is
+// stopped, it cannot be resumed and the log file cannot be recovered. This does
+// not stop the logging worker.
+func (tl *threadLogger) StopLogging() {
+	tl.threshold = math.MaxInt
+}
+
+// GetFile returns the entire log file.
+func (tl *threadLogger) GetFile() []byte {
+	response, err := tl.mm.Send(GetFileTag, nil)
+	if err != nil {
+		jww.FATAL.Panicf("[LOG] Failed to get log file from worker: %+v", err)
+	}
+
+	return response
+}
+
+// Threshold returns the log level threshold of logs sent to the worker.
+func (tl *threadLogger) Threshold() jww.Threshold {
+	return tl.threshold
+}
+
+// MaxSize returns the max size, in bytes, that the log file is allowed to be.
+func (tl *threadLogger) MaxSize() int {
+	response, err := tl.mm.Send(MaxSizeTag, nil)
+	if err != nil {
+		jww.FATAL.Panicf("[LOG] Failed to max file size from worker: %+v", err)
+	}
+
+	return int(binary.LittleEndian.Uint64(response))
+}
+
+// Size returns the number of bytes written to the log file.
+func (tl *threadLogger) Size() int {
+	response, err := tl.mm.Send(SizeTag, nil)
+	if err != nil {
+		jww.FATAL.Panicf("[LOG] Failed to file size from worker: %+v", err)
+	}
+
+	return int(binary.LittleEndian.Uint64(response))
+}
+
+// Worker always returns nil
+func (tl *threadLogger) Worker() *worker.Manager {
+	return nil
+}
diff --git a/main.go b/main.go
index 46cdc133897c1225c9956ea44c5cc2d3018b70aa..470740cfec9e85dda4eedb5de36b32854b26e27f 100644
--- a/main.go
+++ b/main.go
@@ -11,15 +11,16 @@ package main
 
 import (
 	"fmt"
-	"github.com/spf13/cobra"
 	"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"
 )
 
@@ -99,6 +100,11 @@ func setGlobals() {
 	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))
@@ -121,17 +127,24 @@ func setGlobals() {
 		js.FuncOf(wasm.NewChannelsManagerWithIndexedDbUnsafe))
 	js.Global().Set("DecodePublicURL", js.FuncOf(wasm.DecodePublicURL))
 	js.Global().Set("DecodePrivateURL", js.FuncOf(wasm.DecodePrivateURL))
+	js.Global().Set("DecodeInviteURL", js.FuncOf(wasm.DecodeInviteURL))
 	js.Global().Set("GetChannelJSON", js.FuncOf(wasm.GetChannelJSON))
 	js.Global().Set("GetChannelInfo", js.FuncOf(wasm.GetChannelInfo))
 	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("GetChannelNotificationReportsForMe",
+		js.FuncOf(wasm.GetChannelNotificationReportsForMe))
 	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.GetChannelNotificationReportsForMe))
 
-	// wasm/dm.go
+	// wasm/cipher.go
+	js.Global().Set("NewDatabaseCipher",
+		js.FuncOf(wasm.NewDatabaseCipher))
+
+	// wasm/channelsFileTransfer.go
 	js.Global().Set("InitChannelsFileTransfer",
 		js.FuncOf(wasm.InitChannelsFileTransfer))
 
@@ -141,12 +154,18 @@ func setGlobals() {
 		js.FuncOf(wasm.NewDMClientWithIndexedDb))
 	js.Global().Set("NewDMClientWithIndexedDbUnsafe",
 		js.FuncOf(wasm.NewDMClientWithIndexedDbUnsafe))
-	js.Global().Set("NewDMsDatabaseCipher",
-		js.FuncOf(wasm.NewDMsDatabaseCipher))
+	js.Global().Set("NewDMsDatabaseCipher", js.FuncOf(wasm.NewDatabaseCipher))
+	js.Global().Set("DecodeDMShareURL", js.FuncOf(wasm.DecodeDMShareURL))
+	js.Global().Set("GetDmNotificationReportsForMe",
+		js.FuncOf(wasm.GetDmNotificationReportsForMe))
 
 	// wasm/cmix.go
 	js.Global().Set("NewCmix", js.FuncOf(wasm.NewCmix))
+	js.Global().Set("NewSynchronizedCmix",
+		js.FuncOf(wasm.NewSynchronizedCmix))
 	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))
@@ -230,6 +249,8 @@ func setGlobals() {
 	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))
@@ -252,7 +273,7 @@ func setGlobals() {
 
 var (
 	logLevel, fileLogLevel      jww.Threshold
-	maxLogFileSizeMB              int
+	maxLogFileSizeMB            int
 	workerScriptURL, workerName string
 )
 
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..ecacf52e5d3f77c1d1aa16847b2f778c5fd88d5e 100644
--- a/storage/password.go
+++ b/storage/password.go
@@ -12,16 +12,22 @@ 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/crypto/hash"
+	"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.
@@ -35,6 +41,8 @@ const (
 	// saltLen is the length of the salt. Recommended to be 16 bytes here:
 	// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-argon2-04#section-3.1
 	saltLen = 16
+
+	internalPasswordConstant = "XXInternalPassword"
 )
 
 // Storage keys.
@@ -53,7 +61,7 @@ const (
 // Error messages.
 const (
 	// initInternalPassword
-	readInternalPasswordErr     = "could not generate internal password: %+v"
+	readInternalPasswordErr     = "could not generate"
 	internalPasswordNumBytesErr = "expected %d bytes for internal password, found %d bytes"
 
 	// getInternalPassword
@@ -85,17 +93,21 @@ const (
 // Parameters:
 //   - args[0] - The user supplied password (string).
 //
-// Returns:
+// Returns a promise:
 //   - Internal password (Uint8Array).
 //   - Throws TypeError on failure.
 func GetOrInitPassword(_ js.Value, args []js.Value) any {
-	internalPassword, err := getOrInit(args[0].String())
-	if err != nil {
-		utils.Throw(utils.TypeError, err)
-		return nil
+	externalPassword := args[0].String()
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		internalPassword, err := getOrInit(externalPassword)
+		if err != nil {
+			reject(exception.NewTrace(err))
+		} else {
+			resolve(utils.CopyBytesToJS(internalPassword))
+		}
 	}
 
-	return utils.CopyBytesToJS(internalPassword)
+	return utils.CreatePromise(promiseFn)
 }
 
 // ChangeExternalPassword allows a user to change their external password.
@@ -109,7 +121,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 +142,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 +160,10 @@ 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()
+	// NOTE: the following no longer works in synchronized environments, so
+	// disabled in produciton.
+	jww.FATAL.Panicf("cannot change password, unimplemented")
+	localStorage := storage.GetLocalStorage()
 	internalPassword, err := getInternalPassword(
 		oldExternalPassword, localStorage)
 	if err != nil {
@@ -159,13 +174,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,44 +192,61 @@ 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)
 
+	// FIXME: The internal password is now just an expansion of
+	// the users password text. We couldn't preserve the following
+	// when doing cross-device sync.
+	h := hash.CMixHash.New()
+	h.Write([]byte(externalPassword))
+	h.Write(internalPassword)
+	copy(internalPassword, h.Sum(nil)[:internalPasswordLen])
+
 	// Generate internal password
-	n, err := csprng.Read(internalPassword)
-	if err != nil {
-		return nil, errors.Errorf(readInternalPasswordErr, err)
-	} else if n != internalPasswordLen {
-		return nil, errors.Errorf(
-			internalPasswordNumBytesErr, internalPasswordLen, n)
-	}
+	// n, err := csprng.Read(internalPassword)
+	// if err != nil {
+	// 	return nil, errors.Errorf(readInternalPasswordErr, err)
+	// } else if n != internalPasswordLen {
+	// 	return nil, errors.Errorf(
+	// 		internalPasswordNumBytesErr, internalPasswordLen, n)
+	// }
 
 	// Generate and store salt
 	salt, err := makeSalt(csprng)
 	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 +254,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..fecd785c51c7e919ab511e2d8d5821dd6f4b37f5 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
@@ -42,42 +44,42 @@ func Test_getOrInit(t *testing.T) {
 
 // Tests that changeExternalPassword correctly changes the password and updates
 // the encryption.
-func Test_changeExternalPassword(t *testing.T) {
-	oldExternalPassword := "myPassword"
-	newExternalPassword := "hunter2"
-	oldInternalPassword, err := getOrInit(oldExternalPassword)
-	if err != nil {
-		t.Errorf("%+v", err)
-	}
-
-	err = changeExternalPassword(oldExternalPassword, newExternalPassword)
-	if err != nil {
-		t.Errorf("%+v", err)
-	}
-
-	newInternalPassword, err := getOrInit(newExternalPassword)
-	if err != nil {
-		t.Errorf("%+v", err)
-	}
-
-	if !bytes.Equal(oldInternalPassword, newInternalPassword) {
-		t.Errorf("Internal password was not changed in storage. Old and new "+
-			"should be different.\nold: %+v\nnew: %+v",
-			oldInternalPassword, newInternalPassword)
-	}
-
-	_, err = getOrInit(oldExternalPassword)
-	expectedErr := strings.Split(decryptWithPasswordErr, "%")[0]
-	if err == nil || !strings.Contains(err.Error(), expectedErr) {
-		t.Errorf("Unexpected error when trying to get internal password with "+
-			"old external password.\nexpected: %s\nreceived: %+v", expectedErr, err)
-	}
-}
+// func Test_changeExternalPassword(t *testing.T) {
+// 	oldExternalPassword := "myPassword"
+// 	newExternalPassword := "hunter2"
+// 	oldInternalPassword, err := getOrInit(oldExternalPassword)
+// 	if err != nil {
+// 		t.Errorf("%+v", err)
+// 	}
+
+// 	err = changeExternalPassword(oldExternalPassword, newExternalPassword)
+// 	if err != nil {
+// 		t.Errorf("%+v", err)
+// 	}
+
+// 	newInternalPassword, err := getOrInit(newExternalPassword)
+// 	if err != nil {
+// 		t.Errorf("%+v", err)
+// 	}
+
+// 	if !bytes.Equal(oldInternalPassword, newInternalPassword) {
+// 		t.Errorf("Internal password was not changed in storage. Old and new "+
+// 			"should be different.\nold: %+v\nnew: %+v",
+// 			oldInternalPassword, newInternalPassword)
+// 	}
+
+// 	_, err = getOrInit(oldExternalPassword)
+// 	expectedErr := strings.Split(decryptWithPasswordErr, "%")[0]
+// 	if err == nil || !strings.Contains(err.Error(), expectedErr) {
+// 		t.Errorf("Unexpected error when trying to get internal password with "+
+// 			"old external password.\nexpected: %s\nreceived: %+v", expectedErr, err)
+// 	}
+// }
 
 // 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]
@@ -152,26 +154,26 @@ func Test_initInternalPassword_CsprngReadError(t *testing.T) {
 
 // Tests that initInternalPassword returns  an error when the RNG does not
 // return enough bytes.
-func Test_initInternalPassword_CsprngReadNumBytesError(t *testing.T) {
-	externalPassword := "myPassword"
-	ls := GetLocalStorage()
-	b := bytes.NewBuffer(make([]byte, internalPasswordLen/2))
+// func Test_initInternalPassword_CsprngReadNumBytesError(t *testing.T) {
+// 	externalPassword := "myPassword"
+// 	ls := storage.GetLocalStorage()
+// 	b := bytes.NewBuffer(make([]byte, internalPasswordLen/2))
 
-	expectedErr := fmt.Sprintf(
-		internalPasswordNumBytesErr, internalPasswordLen, internalPasswordLen/2)
+// 	expectedErr := fmt.Sprintf(
+// 		internalPasswordNumBytesErr, internalPasswordLen, internalPasswordLen/2)
 
-	_, err := initInternalPassword(externalPassword, ls, b, defaultParams())
-	if err == nil || !strings.Contains(err.Error(), expectedErr) {
-		t.Errorf("Unexpected error when RNG does not return enough bytes."+
-			"\nexpected: %s\nreceived: %+v", expectedErr, err)
-	}
-}
+// 	_, err := initInternalPassword(externalPassword, ls, b, defaultParams())
+// 	if err == nil || !strings.Contains(err.Error(), expectedErr) {
+// 		t.Errorf("Unexpected error when RNG does not return enough bytes."+
+// 			"\nexpected: %s\nreceived: %+v", expectedErr, err)
+// 	}
+// }
 
 // Tests that getInternalPassword returns the internal password that is saved
 // 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..a7b7f64f6dca64d8ef69b2b84cbf976db1569675 100644
--- a/storage/purge.go
+++ b/storage/purge.go
@@ -10,13 +10,14 @@
 package storage
 
 import (
-	"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"
+
+	"github.com/hack-pad/go-indexeddb/idb"
+	jww "github.com/spf13/jwalterweatherman"
+
+	"gitlab.com/elixxir/wasm-utils/exception"
+	"gitlab.com/elixxir/wasm-utils/storage"
 )
 
 // numClientsRunning is an atomic that tracks the current number of Cmix
@@ -43,36 +44,32 @@ func DecrementNumClientsRunning() {
 // password is required.
 //
 // Parameters:
-//   - args[0] - Storage directory path (string). This is the same directory
-//     path passed into [wasm.NewCmix].
-//   - args[1] - The user-supplied password (string). This is the same password
+//   - args[0] - The user-supplied password (string). This is the same password
 //     passed into [wasm.NewCmix].
 //
 // Returns:
-//   - Throws a TypeError if the password is incorrect or if not all cMix
-//     followers have been stopped.
+//   - 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()
-	userPassword := args[1].String()
+	userPassword := args[0].String()
 
 	// 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,28 +79,18 @@ 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)
-	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)
-	jww.DEBUG.Printf("[PURGE] Cleared %d keys with the prefix %q (for NDF)",
-		n, utility.NdfStorageKeyNamePrefix)
-
 	return nil
 }
diff --git a/storage/version.go b/storage/version.go
index b0f192ab24f8ad80d1fb9a9a5c96177f67545440..3a35edfd31eda105afa5e1583dcf891b511b62f8 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 8f156761dd4919cfb4e92b93d91acef0253c6901..0000000000000000000000000000000000000000
--- a/utils/utils.go
+++ /dev/null
@@ -1,108 +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)
-	var handler js.Func
-	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)
-			go func() { handler.Release() }()
-		}(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 21ef36e6546871d714be2aba31ec8d223b5e7021..6b99feb119bffe5c79b4715adab08d14ccb7bd45 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,6 +58,8 @@ func newChannelsManagerJS(api *bindings.ChannelsManager) map[string]any {
 		"SendMessage":           js.FuncOf(cm.SendMessage),
 		"SendReply":             js.FuncOf(cm.SendReply),
 		"SendReaction":          js.FuncOf(cm.SendReaction),
+		"SendSilent":            js.FuncOf(cm.SendSilent),
+		"SendInvite":            js.FuncOf(cm.SendInvite),
 		"DeleteMessage":         js.FuncOf(cm.DeleteMessage),
 		"PinMessage":            js.FuncOf(cm.PinMessage),
 		"MuteUser":              js.FuncOf(cm.MuteUser),
@@ -79,6 +79,12 @@ func newChannelsManagerJS(api *bindings.ChannelsManager) map[string]any {
 
 		// Channel Receiving Logic and Callback Registration
 		"RegisterReceiveHandler": js.FuncOf(cm.RegisterReceiveHandler),
+
+		// Notifications
+		"GetNotificationLevel":  js.FuncOf(cm.GetNotificationLevel),
+		"GetNotificationStatus": js.FuncOf(cm.GetNotificationStatus),
+		"SetMobileNotificationsLevel": js.FuncOf(
+			cm.SetMobileNotificationsLevel),
 	}
 
 	return channelsManagerMap
@@ -105,11 +111,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
 	}
 
@@ -128,7 +134,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
@@ -144,7 +150,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
 	}
 
@@ -162,12 +168,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
 	}
 
@@ -184,14 +190,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
 	}
 
@@ -206,13 +212,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
 	}
 
@@ -229,14 +235,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
 	}
 
@@ -263,20 +269,28 @@ func GetPublicChannelIdentityFromPrivate(_ 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] - 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])
 	em := newEventModelBuilder(args[2])
 	extensionBuilderIDsJSON := utils.CopyBytesToGo(args[3])
+	notificationsID := args[4].Int()
+	cUI := newChannelUI(args[5])
 
 	cm, err := bindings.NewChannelsManager(
-		cmixId, privateIdentity, em, extensionBuilderIDsJSON)
+		cmixId, privateIdentity, em, extensionBuilderIDsJSON, notificationsID, cUI)
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -303,19 +317,27 @@ func NewChannelsManager(_ 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]`.
+//   - 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])
 	extensionBuilderIDsJSON := utils.CopyBytesToGo(args[3])
+	notificationsID := args[4].Int()
+	cUI := newChannelUI(args[5])
 	cm, err := bindings.LoadChannelsManager(
-		cmixID, storageTag, em, extensionBuilderIDsJSON)
+		cmixID, storageTag, em, extensionBuilderIDsJSON, notificationsID, cUI)
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -343,48 +365,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
-//     object with [NewChannelsDatabaseCipher] and get its id with
-//     [ChannelDbCipher.GetID].
+//   - 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 [DbCipher] object in tracker (int). Create this
+//     object with [NewDatabaseCipher] and get its id with
+//     [DbCipher.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)
+	cipher, err := dbCipherTrackerSingleton.get(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
@@ -409,22 +419,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.
@@ -436,40 +436,26 @@ 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 *DbCipher) any {
 
 	model := channelsDb.NewWASMEventModelBuilder(
-		wasmJsPath, cipher, messageReceived, deletedMessage, mutedUser)
+		wasmJsPath, cipher.api, 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))
 		}
@@ -492,46 +478,40 @@ 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[6] - ID of [ChannelDbCipher] object in tracker (int). Create this
-//     object with [NewChannelsDatabaseCipher] and get its id with
-//     [ChannelDbCipher.GetID].
+//   - 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 [DbCipher] object in tracker (int). Create this
+//     object with [NewDatabaseCipher] and get its id with
+//     [DbCipher.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 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)
+	cipher, err := dbCipherTrackerSingleton.get(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]
@@ -550,22 +530,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.
@@ -574,39 +548,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,
-	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)
-	}
+	extensionBuilderIDsJSON []byte, notificationsID int,
+	channelsCbs bindings.ChannelUICallbacks, cipher *DbCipher) any {
 
 	model := channelsDb.NewWASMEventModelBuilder(
-		wasmJsPath, cipher, messageReceived, deletedMessage, mutedUser)
+		wasmJsPath, cipher.api, 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))
 		}
@@ -632,7 +594,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
 	}
 
@@ -654,7 +616,33 @@ 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
+	}
+
+	return c
+}
+
+// DecodeInviteURL decodes the channel URL, using the password, into a channel
+// pretty print. This function can only be used for URLs from invitations.
+//
+// Parameters:
+//   - args[0] - The channel's share URL (string). Should be received from
+//     another user via invitation.
+//   - args[1] - The password needed to decrypt the secret data in the URL
+//     (string).
+//
+// Returns:
+//   - The channel pretty print (string)
+func DecodeInviteURL(_ js.Value, args []js.Value) any {
+	var (
+		url      = args[0].String()
+		password = args[1].String()
+	)
+
+	c, err := bindings.DecodeInviteURL(url, password)
+	if err != nil {
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -685,7 +673,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
 	}
 
@@ -704,11 +692,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
 	}
 
@@ -756,7 +744,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)
 		}
@@ -786,7 +774,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))
 		}
@@ -810,7 +798,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()
 		}
@@ -829,13 +817,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
 	}
 
@@ -846,7 +834,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:
 //
@@ -857,7 +845,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
 	}
 
@@ -871,12 +859,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
@@ -889,12 +877,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
@@ -907,12 +895,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
@@ -955,7 +943,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()
@@ -964,7 +952,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
 	}
 
@@ -980,7 +968,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:
 //
@@ -990,7 +978,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
 	}
 
@@ -1034,6 +1022,22 @@ 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 map of slices of [ed25519.PublicKey] of users that
+//     should receive mobile notifications for the message. Each slice keys on a
+//     [channels.PingType] that describes the type of notification it is
+//     (Uint8Array).
+//
+// Example map of slices of public keys:
+//
+//	{
+//	  "usrMention": [
+//	    "CLdKxbe8D2WVOpx1mT63TZ5CP/nesmxHLT5DUUalpe0=",
+//	    "S2c6NXjNqgR11SCOaiQUughWaLpWBKNufPt6cbTVHMA="
+//	  ],
+//	  "usrReply": [
+//	    "aaMzSeA6Cu2Aix2MlOwzrAI+NnpKshzvZRT02PZPVec="
+//	  ]
+//	}
 //
 // Returns a promise:
 //   - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array).
@@ -1045,12 +1049,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])
+	pingsMapJSON := utils.CopyBytesToGo(args[6])
 
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
 		sendReport, err := cm.api.SendGeneric(marshalledChanId, messageType,
-			msg, leaseTimeMS, tracked, cmixParamsJSON)
+			msg, leaseTimeMS, tracked, cmixParamsJSON, pingsMapJSON)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(utils.CopyBytesToJS(sendReport))
 		}
@@ -1078,6 +1083,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).
@@ -1087,12 +1102,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) {
 		sendReport, err := cm.api.SendMessage(
-			marshalledChanId, msg, leaseTimeMS, cmixParamsJSON)
+			marshalledChanId, msg, leaseTimeMS, cmixParamsJSON, pingsJSON)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(utils.CopyBytesToJS(sendReport))
 		}
@@ -1127,6 +1143,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).
@@ -1137,12 +1163,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) {
 		sendReport, err := cm.api.SendReply(marshalledChanId, msg,
-			messageToReactTo, leaseTimeMS, cmixParamsJSON)
+			messageToReactTo, leaseTimeMS, cmixParamsJSON, pingsJSON)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(utils.CopyBytesToJS(sendReport))
 		}
@@ -1190,7 +1217,104 @@ 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))
+		}
+	}
+
+	return utils.CreatePromise(promiseFn)
+}
+
+// SendInvite is used to send to a channel (invited) an invitation to another
+// channel (invitee).
+//
+// If the channel ID for the invitee channel is not recognized by the Manager,
+// then an error will be returned.
+//
+// Parameters:
+//   - args[0] - Marshalled bytes of the invited channel [id.ID] (Uint8Array).
+//   - args[1] - JSON of the invitee channel [id.ID].
+//     This can be retrieved from [GetChannelJSON]. (Uint8Array).
+//   - args[2] - The contents of the message (string).
+//   - args[3] - The URL to append the channel info to (string).
+//   - args[4] - 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[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).
+//   - Rejected with an error if sending fails.
+func (cm *ChannelsManager) SendInvite(_ js.Value, args []js.Value) any {
+	var (
+		marshalledChanId = utils.CopyBytesToGo(args[0])
+		inviteToJSON     = utils.CopyBytesToGo(args[1])
+		msg              = args[2].String()
+		host             = args[3].String()
+		leaseTimeMS      = int64(args[4].Int())
+		cmixParamsJSON   = utils.CopyBytesToGo(args[5])
+		pingsJSON        = utils.CopyBytesToGo(args[6])
+	)
+
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		sendReport, err := cm.api.SendInvite(marshalledChanId,
+			inviteToJSON, msg, host, leaseTimeMS,
+			cmixParamsJSON, pingsJSON)
+		if err != nil {
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(utils.CopyBytesToJS(sendReport))
 		}
@@ -1247,7 +1371,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))
 		}
@@ -1286,7 +1410,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))
 		}
@@ -1328,7 +1452,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))
 		}
@@ -1369,7 +1493,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))
 		}
@@ -1391,7 +1515,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
 	}
 
@@ -1410,7 +1534,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
 	}
 
@@ -1439,7 +1563,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
 	}
 
@@ -1457,7 +1581,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
 	}
 
@@ -1476,7 +1600,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
 	}
 
@@ -1498,7 +1622,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
@@ -1512,13 +1636,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
 	}
 
@@ -1535,7 +1659,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:
 //
@@ -1544,13 +1668,177 @@ 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
+}
+
+// GetNotificationStatus returns the notification status for the given channel.
+//
+// Parameters:
+//   - args[0] - The marshalled bytes of the channel's [id.ID] (Uint8Array).
+//
+// Returns:
+//   - The [notifications.NotificationState] for the channel (int).
+//   - Throws an error if the channel ID cannot be unmarshalled or the channel
+//     cannot be found.
+func (cm *ChannelsManager) GetNotificationStatus(_ js.Value, args []js.Value) any {
+	channelIDBytes := utils.CopyBytesToGo(args[0])
+
+	status, err := cm.api.GetNotificationStatus(channelIDBytes)
+	if err != nil {
+		exception.ThrowTrace(err)
+		return nil
+	}
+
+	return status
+}
+
+// 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 a promise and 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()
+
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		err := cm.api.SetMobileNotificationsLevel(channelIDBytes,
+			level, status)
+		if err != nil {
+			reject(exception.NewTrace(err))
+		} else {
+			resolve()
+		}
+	}
+
+	return utils.CreatePromise(promiseFn)
+}
+
+// GetChannelNotificationReportsForMe 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].
+//     It can optionally be the entire json return from
+//     [bindings.NotificationUpdateJson] instead of just the needed subsection
+//     (Uint8Array).
+//   - notificationDataCSV - CSV containing notification data (string).
+//
+// 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":{}}
+//		    }
+//		  }
+//		]
+//
+// Returns:
+//   - The JSON of a slice of [channels.NotificationReport] (Uint8Array).
+//   - Throws an error if getting the report fails.
+//
+// Example return:
+//
+//	[
+//	  {
+//	    "channel": "jOgZopfYj4zrE/AHtKmkf+QEWnfUKv9KfIy/+Bsg0PkD",
+//	    "type": 1,
+//	    "pingType": "usrMention"
+//	  },
+//	  {
+//	    "channel": "GKmfN/LKXQYM6++TC6DeZYqoxvSUPkh5UAHWODqh9zkD",
+//	    "type": 2,
+//	    "pingType": "usrReply"
+//	  },
+//	  {
+//	    "channel": "M+28xtj0coHrhDHfojGNcyb2c4maO7ZuheB6egS0Pc4D",
+//	    "type": 1,
+//	    "pingType": ""
+//	  }
+//	]
+func GetChannelNotificationReportsForMe(_ js.Value, args []js.Value) any {
+	notificationFilterJSON := utils.CopyBytesToGo(args[0])
+	notificationDataCSV := args[1].String()
+
+	report, err := bindings.GetChannelNotificationReportsForMe(
+		notificationFilterJSON, notificationDataCSV)
+	if err != nil {
+		exception.ThrowTrace(err)
+		return nil
+	}
+
+	return utils.CopyBytesToJS(report)
+}
+
 ////////////////////////////////////////////////////////////////////////////////
 // Admin Management                                                           //
 ////////////////////////////////////////////////////////////////////////////////
@@ -1563,11 +1851,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
 	}
 
@@ -1599,12 +1887,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)
@@ -1622,14 +1910,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])
@@ -1638,7 +1926,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
 	}
 
@@ -1656,13 +1944,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])
@@ -1671,7 +1959,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
 	}
 
@@ -1688,11 +1976,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
 	}
 
@@ -1722,7 +2010,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()
 }
@@ -1750,7 +2038,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{
@@ -1763,7 +2051,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
 	}
 
@@ -1795,7 +2083,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.
@@ -2144,141 +2432,22 @@ type MessageAndError struct {
 	Error string
 }
 
-////////////////////////////////////////////////////////////////////////////////
-// Channel Cipher                                                             //
-////////////////////////////////////////////////////////////////////////////////
-
-// ChannelDbCipher wraps the [bindings.ChannelDbCipher] object so its methods
-// can be wrapped to be Javascript compatible.
-type ChannelDbCipher struct {
-	api *bindings.ChannelDbCipher
-}
-
-// newChannelDbCipherJS creates a new Javascript compatible object
-// (map[string]any) that matches the [ChannelDbCipher] structure.
-func newChannelDbCipherJS(api *bindings.ChannelDbCipher) map[string]any {
-	c := ChannelDbCipher{api}
-	channelDbCipherMap := map[string]any{
-		"GetID":         js.FuncOf(c.GetID),
-		"Encrypt":       js.FuncOf(c.Encrypt),
-		"Decrypt":       js.FuncOf(c.Decrypt),
-		"MarshalJSON":   js.FuncOf(c.MarshalJSON),
-		"UnmarshalJSON": js.FuncOf(c.UnmarshalJSON),
-	}
-
-	return channelDbCipherMap
-}
-
-// NewChannelsDatabaseCipher constructs a [ChannelDbCipher] object.
-//
-// Parameters:
-//   - args[0] - The tracked [Cmix] object ID (int).
-//   - args[1] - The password for storage. This should be the same password
-//     passed into [NewCmix] (Uint8Array).
-//   - args[2] - The maximum size of a payload to be encrypted. A payload passed
-//     into [ChannelDbCipher.Encrypt] that is larger than this value will result
-//     in an error (int).
-//
-// Returns:
-//   - JavaScript representation of the [ChannelDbCipher] object.
-//   - Throws a TypeError if creating the cipher fails.
-func NewChannelsDatabaseCipher(_ js.Value, args []js.Value) any {
-	cmixId := args[0].Int()
-	password := utils.CopyBytesToGo(args[1])
-	plaintTextBlockSize := args[2].Int()
-
-	cipher, err := bindings.NewChannelsDatabaseCipher(
-		cmixId, password, plaintTextBlockSize)
-	if err != nil {
-		utils.Throw(utils.TypeError, err)
-		return nil
-	}
-
-	return newChannelDbCipherJS(cipher)
-}
-
-// GetID returns the ID for this [bindings.ChannelDbCipher] in the
-// channelDbCipherTracker.
-//
-// Returns:
-//   - Tracker ID (int).
-func (c *ChannelDbCipher) GetID(js.Value, []js.Value) any {
-	return c.api.GetID()
-}
-
-// Encrypt will encrypt the raw data. It will return a ciphertext. Padding is
-// done on the plaintext so all encrypted data looks uniform at rest.
-//
-// Parameters:
-//   - args[0] - The data to be encrypted (Uint8Array). This must be smaller
-//     than the block size passed into [NewChannelsDatabaseCipher]. If it is
-//     larger, this will return an error.
-//
-// Returns:
-//   - The ciphertext of the plaintext passed in (Uint8Array).
-//   - Throws a TypeError 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)
-		return nil
-	}
-
-	return utils.CopyBytesToJS(ciphertext)
-}
-
-// Decrypt will decrypt the passed in encrypted value. The plaintext will be
-// returned by this function. Any padding will be discarded within this
-// function.
-//
-// Parameters:
-//   - args[0] - the encrypted data returned by [ChannelDbCipher.Encrypt]
-//     (Uint8Array).
-//
-// Returns:
-//   - The plaintext of the ciphertext passed in (Uint8Array).
-//   - Throws a TypeError 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)
-		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"),
 	}
-
-	return utils.CopyBytesToJS(plaintext)
 }
 
-// MarshalJSON marshals the cipher into valid JSON.
-//
-// Returns:
-//   - JSON of the cipher (Uint8Array).
-//   - Throws a TypeError 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)
-		return nil
-	}
-
-	return utils.CopyBytesToJS(data)
+// eventModel wraps Javascript callbacks to adhere to the
+// [bindings.ChannelUICallbacks] interface.
+type channelUI struct {
+	eventUpdate func(args ...any) js.Value
 }
 
-// UnmarshalJSON unmarshalls JSON into the cipher.
-//
-// Note that this function does not transfer the internal RNG. Use
-// [channel.NewCipherFromJSON] to properly reconstruct a cipher from JSON.
-//
-// Parameters:
-//   - args[0] - JSON data to unmarshal (Uint8Array).
-//
-// Returns:
-//   - JSON of the cipher (Uint8Array).
-//   - Throws a TypeError 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)
-		return nil
-	}
-	return nil
+// 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..c96b547283a1b7d337666c17ef846370d4344825 100644
--- a/wasm/channelsFileTransfer.go
+++ b/wasm/channelsFileTransfer.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"
 )
 
 // ChannelsFileTransfer wraps the [bindings.ChannelsFileTransfer] object so its
@@ -67,7 +69,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 +166,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))
 		}
@@ -192,6 +194,8 @@ func (cft *ChannelsFileTransfer) Upload(_ js.Value, args []js.Value) any {
 //     the channel (int). For the maximum amount of time, use [ValidForever].
 //   - args[6] - JSON of [xxdk.CMIXParams] (Uint8Array). If left empty,
 //     [GetDefaultCMixParams] will be used internally.
+//   - args[7] - JSON of a slice of public keys of users that should receive
+//     mobile notifications for the message.
 //
 // Returns a promise:
 //   - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array).
@@ -205,13 +209,15 @@ func (cft *ChannelsFileTransfer) Send(_ js.Value, args []js.Value) any {
 		preview        = utils.CopyBytesToGo(args[4])
 		validUntilMS   = args[5].Int()
 		cmixParamsJSON = utils.CopyBytesToGo(args[6])
+		pingsJSON      = utils.CopyBytesToGo(args[7])
 	)
 
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
-		fileID, err := cft.api.Send(channelIdBytes, fileLinkJSON, fileName,
-			fileType, preview, validUntilMS, cmixParamsJSON)
+		fileID, err := cft.api.Send(channelIdBytes, fileLinkJSON,
+			fileName, fileType, preview, validUntilMS,
+			cmixParamsJSON, pingsJSON)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(utils.CopyBytesToJS(fileID))
 		}
@@ -264,7 +270,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 +311,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 +340,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 +392,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 +444,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 +496,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 +524,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..9ebbc691debd6d330a9cadc3a4aa4c9c62d342ad 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"
@@ -60,47 +60,6 @@ func Test_ChannelsManagerMethods(t *testing.T) {
 	}
 }
 
-// Tests that the map representing ChannelDbCipher returned by
-// newChannelDbCipherJS contains all of the methods on ChannelDbCipher.
-func Test_newChannelDbCipherJS(t *testing.T) {
-	cipherType := reflect.TypeOf(&ChannelDbCipher{})
-
-	cipher := newChannelDbCipherJS(&bindings.ChannelDbCipher{})
-	if len(cipher) != cipherType.NumMethod() {
-		t.Errorf("ChannelDbCipher JS object does not have all methods."+
-			"\nexpected: %d\nreceived: %d", cipherType.NumMethod(), len(cipher))
-	}
-
-	for i := 0; i < cipherType.NumMethod(); i++ {
-		method := cipherType.Method(i)
-
-		if _, exists := cipher[method.Name]; !exists {
-			t.Errorf("Method %s does not exist.", method.Name)
-		}
-	}
-}
-
-// Tests that ChannelDbCipher has all the methods that
-// [bindings.ChannelDbCipher] has.
-func Test_ChannelDbCipherMethods(t *testing.T) {
-	cipherType := reflect.TypeOf(&ChannelDbCipher{})
-	binCipherType := reflect.TypeOf(&bindings.ChannelDbCipher{})
-
-	if binCipherType.NumMethod() != cipherType.NumMethod() {
-		t.Errorf("WASM ChannelDbCipher object does not have all methods from "+
-			"bindings.\nexpected: %d\nreceived: %d",
-			binCipherType.NumMethod(), cipherType.NumMethod())
-	}
-
-	for i := 0; i < binCipherType.NumMethod(); i++ {
-		method := binCipherType.Method(i)
-
-		if _, exists := cipherType.MethodByName(method.Name); !exists {
-			t.Errorf("Method %s does not exist.", method.Name)
-		}
-	}
-}
-
 type jsIdentity struct {
 	pubKey  js.Value
 	codeset js.Value
diff --git a/wasm/cipher.go b/wasm/cipher.go
new file mode 100644
index 0000000000000000000000000000000000000000..5504d2f3a426c78640ff1611e025103e91fb876e
--- /dev/null
+++ b/wasm/cipher.go
@@ -0,0 +1,232 @@
+////////////////////////////////////////////////////////////////////////////////
+// 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 (
+	"github.com/pkg/errors"
+	"gitlab.com/elixxir/client/v4/bindings"
+	"gitlab.com/elixxir/client/v4/storage/utility"
+	"gitlab.com/elixxir/crypto/indexedDb"
+	"gitlab.com/elixxir/wasm-utils/exception"
+	"gitlab.com/elixxir/wasm-utils/utils"
+	"sync"
+	"syscall/js"
+)
+
+// dbCipherTrackerSingleton is used to track DbCipher objects
+// so that they can be referenced by ID back over the bindings.
+var dbCipherTrackerSingleton = &DbCipherTracker{
+	tracked: make(map[int]*DbCipher),
+	count:   0,
+}
+
+// DbCipherTracker is a singleton used to keep track of extant
+// DbCipher objects, preventing race conditions created by passing it
+// over the bindings.
+type DbCipherTracker struct {
+	tracked map[int]*DbCipher
+	count   int
+	mux     sync.RWMutex
+}
+
+// create creates a DbCipher from a [indexedDb.Cipher], assigns it a unique
+// ID, and adds it to the DbCipherTracker.
+func (ct *DbCipherTracker) create(c indexedDb.Cipher) *DbCipher {
+	ct.mux.Lock()
+	defer ct.mux.Unlock()
+
+	chID := ct.count
+	ct.count++
+
+	ct.tracked[chID] = &DbCipher{
+		api: c,
+		id:  chID,
+	}
+
+	return ct.tracked[chID]
+}
+
+// get an DbCipher from the DbCipherTracker given its ID.
+func (ct *DbCipherTracker) get(id int) (*DbCipher, error) {
+	ct.mux.RLock()
+	defer ct.mux.RUnlock()
+
+	c, exist := ct.tracked[id]
+	if !exist {
+		return nil, errors.Errorf(
+			"Cannot get DbCipher for ID %d, does not exist", id)
+	}
+
+	return c, nil
+}
+
+// delete removes a DbCipherTracker from the DbCipherTracker.
+func (ct *DbCipherTracker) delete(id int) {
+	ct.mux.Lock()
+	defer ct.mux.Unlock()
+
+	delete(ct.tracked, id)
+}
+
+// DbCipher wraps the [indexedDb.Cipher] object so its methods
+// can be wrapped to be Javascript compatible.
+type DbCipher struct {
+	api  indexedDb.Cipher
+	salt []byte
+	id   int
+}
+
+// newDbCipherJS creates a new Javascript compatible object
+// (map[string]any) that matches the [DbCipher] structure.
+func newDbCipherJS(c *DbCipher) map[string]any {
+	DbCipherMap := map[string]any{
+		"GetID":         js.FuncOf(c.GetID),
+		"Encrypt":       js.FuncOf(c.Encrypt),
+		"Decrypt":       js.FuncOf(c.Decrypt),
+		"MarshalJSON":   js.FuncOf(c.MarshalJSON),
+		"UnmarshalJSON": js.FuncOf(c.UnmarshalJSON),
+	}
+
+	return DbCipherMap
+}
+
+// NewDatabaseCipher constructs a [DbCipher] object.
+//
+// Parameters:
+//   - args[0] - The tracked [Cmix] object ID (int).
+//   - args[1] - The password for storage. This should be the same password
+//     passed into [NewCmix] (Uint8Array).
+//   - args[2] - The maximum size of a payload to be encrypted. A payload passed
+//     into [DbCipher.Encrypt] that is larger than this value will result
+//     in an error (int).
+//
+// Returns:
+//   - JavaScript representation of the [DbCipher] object.
+//   - Throws an error if creating the cipher fails.
+func NewDatabaseCipher(_ js.Value, args []js.Value) any {
+	cmixId := args[0].Int()
+	password := utils.CopyBytesToGo(args[1])
+	plaintTextBlockSize := args[2].Int()
+
+	// Get user from singleton
+	user, err := bindings.GetCMixInstance(cmixId)
+	if err != nil {
+		exception.ThrowTrace(err)
+		return nil
+	}
+
+	// Generate RNG
+	stream := user.GetRng().GetStream()
+
+	// Load or generate a salt
+	salt, err := utility.NewOrLoadSalt(
+		user.GetStorage().GetKV(), stream)
+	if err != nil {
+		exception.ThrowTrace(err)
+		return nil
+	}
+
+	// Construct a cipher
+	c, err := indexedDb.NewCipher(
+		password, salt, plaintTextBlockSize, stream)
+	if err != nil {
+		exception.ThrowTrace(err)
+		return nil
+	}
+
+	// Add to singleton and return
+	return newDbCipherJS(dbCipherTrackerSingleton.create(c))
+}
+
+// GetID returns the ID for this [DbCipher] in the
+// DbCipherTracker.
+//
+// Returns:
+//   - Tracker ID (int).
+func (c *DbCipher) GetID(js.Value, []js.Value) any {
+	return c.id
+}
+
+// Encrypt will encrypt the raw data. It will return a ciphertext. Padding is
+// done on the plaintext so all encrypted data looks uniform at rest.
+//
+// Parameters:
+//   - args[0] - The data to be encrypted (Uint8Array). This must be smaller
+//     than the block size passed into [NewDatabaseCipher]. If it is
+//     larger, this will return an error.
+//
+// Returns:
+//   - The ciphertext of the plaintext passed in (String).
+//   - Throws an error if it fails to encrypt the plaintext.
+func (c *DbCipher) Encrypt(_ js.Value, args []js.Value) any {
+	ciphertext, err := c.api.Encrypt(utils.CopyBytesToGo(args[0]))
+	if err != nil {
+		exception.ThrowTrace(err)
+		return nil
+	}
+
+	return ciphertext
+}
+
+// Decrypt will decrypt the passed in encrypted value. The plaintext will be
+// returned by this function. Any padding will be discarded within this
+// function.
+//
+// Parameters:
+//   - args[0] - the encrypted data returned by [DbCipher.Encrypt]
+//     (String).
+//
+// Returns:
+//   - The plaintext of the ciphertext passed in (Uint8Array).
+//   - Throws an error if it fails to encrypt the plaintext.
+func (c *DbCipher) Decrypt(_ js.Value, args []js.Value) any {
+	plaintext, err := c.api.Decrypt(args[0].String())
+	if err != nil {
+		exception.ThrowTrace(err)
+		return nil
+	}
+
+	return utils.CopyBytesToJS(plaintext)
+}
+
+// MarshalJSON marshals the cipher into valid JSON.
+//
+// Returns:
+//   - JSON of the cipher (Uint8Array).
+//   - Throws an error if marshalling fails.
+func (c *DbCipher) MarshalJSON(js.Value, []js.Value) any {
+	data, err := c.api.MarshalJSON()
+	if err != nil {
+		exception.ThrowTrace(err)
+		return nil
+	}
+
+	return utils.CopyBytesToJS(data)
+}
+
+// UnmarshalJSON unmarshalls JSON into the cipher.
+//
+// Note that this function does not transfer the internal RNG. Use
+// [indexedDb.NewCipherFromJSON] to properly reconstruct a cipher from JSON.
+//
+// Parameters:
+//   - args[0] - JSON data to unmarshal (Uint8Array).
+//
+// Returns:
+//   - JSON of the cipher (Uint8Array).
+//   - Throws an error if marshalling fails.
+func (c *DbCipher) UnmarshalJSON(_ js.Value, args []js.Value) any {
+	err := c.api.UnmarshalJSON(utils.CopyBytesToGo(args[0]))
+	if err != nil {
+		exception.ThrowTrace(err)
+		return nil
+	}
+	return nil
+}
diff --git a/wasm/cipher_test.go b/wasm/cipher_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..0f83461509bc0f70ff5afd7104de9ab5fb3e6e11
--- /dev/null
+++ b/wasm/cipher_test.go
@@ -0,0 +1,35 @@
+////////////////////////////////////////////////////////////////////////////////
+// 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"
+)
+
+// Tests that the map representing DbCipher returned by
+// newDbCipherJS contains all the methods on DbCipher.
+func Test_newChannelDbCipherJS(t *testing.T) {
+	cipherType := reflect.TypeOf(&DbCipher{})
+
+	cipher := newDbCipherJS(&DbCipher{})
+	if len(cipher) != cipherType.NumMethod() {
+		t.Errorf("DbCipher JS object does not have all methods."+
+			"\nexpected: %d\nreceived: %d", cipherType.NumMethod(), len(cipher))
+	}
+
+	for i := 0; i < cipherType.NumMethod(); i++ {
+		method := cipherType.Method(i)
+
+		if _, exists := cipher[method.Name]; !exists {
+			t.Errorf("Method %s does not exist.", method.Name)
+		}
+	}
+}
diff --git a/wasm/cmix.go b/wasm/cmix.go
index e3430f4cf4b489ad7480ab53522fac2631c7cbce..0231e368471d922a9b2b162950d4cf8d6e7290db 100644
--- a/wasm/cmix.go
+++ b/wasm/cmix.go
@@ -10,11 +10,19 @@
 package wasm
 
 import (
-	"gitlab.com/elixxir/client/v4/bindings"
-	"gitlab.com/elixxir/xxdk-wasm/utils"
+	"fmt"
+	"sync/atomic"
 	"syscall/js"
+
+	"gitlab.com/elixxir/client/v4/bindings"
+	"gitlab.com/elixxir/wasm-utils/exception"
+	"gitlab.com/elixxir/wasm-utils/utils"
 )
 
+// initializing prevents a synchronized Cmix object from being loaded while one
+// is being initialized.
+var initializing atomic.Bool
+
 // Cmix wraps the [bindings.Cmix] object so its methods can be wrapped to be
 // Javascript compatible.
 type Cmix struct {
@@ -29,6 +37,7 @@ func newCmixJS(api *bindings.Cmix) map[string]any {
 		// cmix.go
 		"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),
 
@@ -98,7 +107,49 @@ 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()
+		}
+	}
+
+	return utils.CreatePromise(promiseFn)
+}
+
+// NewSynchronizedCmix clones a cMix from remote storage.
+//
+// Users of this function should delete the storage directory on error.
+//
+// Parameters:
+//   - args[0] - NDF JSON ([ndf.NetworkDefinition]) (string).
+//   - args[1] - Storage directory path (string).
+//   - args[2] - The remote "directory" or path prefix used by the RemoteStore
+//     when reading/writing files (string).
+//   - args[3] - Password used for storage (Uint8Array).
+//   - args[4] - Javascript [RemoteStore] implementation.
+//
+// Returns a promise:
+//   - Resolves on success.
+//   - Rejected with an error if creating a new cMix client fails.
+func NewSynchronizedCmix(_ js.Value, args []js.Value) any {
+	ndfJSON := args[0].String()
+	storageDir := args[1].String()
+	remoteStoragePrefixPath := args[2].String()
+	password := utils.CopyBytesToGo(args[3])
+	rs := newRemoteStore(args[4])
+
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		// Block loading of synchronized Cmix during initialisation
+		initializing.Store(true)
+
+		err := bindings.NewSynchronizedCmix(ndfJSON, storageDir,
+			remoteStoragePrefixPath, password, rs)
+
+		// Unblock loading of synchronized Cmix during initialisation
+		initializing.Store(false)
+
+		if err != nil {
+			reject(exception.NewTrace(err))
 		} else {
 			resolve()
 		}
@@ -131,9 +182,49 @@ func LoadCmix(_ js.Value, args []js.Value) any {
 	cmixParamsJSON := utils.CopyBytesToGo(args[2])
 
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
-		net, err := bindings.LoadCmix(storageDir, password, cmixParamsJSON)
+		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] - The remote "directory" or path prefix used by the RemoteStore
+//     when reading/writing files (string).
+//   - args[2] - Password used for storage (Uint8Array).
+//   - args[3] - Javascript [RemoteStore] implementation.
+//   - args[4] - 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()
+	remoteStoragePrefixPath := args[1].String()
+	password := utils.CopyBytesToGo(args[2])
+	rs := newRemoteStore(args[3])
+	cmixParamsJSON := utils.CopyBytesToGo(args[4])
+
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		if initializing.Load() {
+			reject(exception.NewTrace(fmt.Errorf(
+				"cannot Load when New is running")))
+		}
+		net, err := bindings.LoadSynchronizedCmix(storageDir,
+			remoteStoragePrefixPath, password,
+			rs, cmixParamsJSON)
+		if err != nil {
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(newCmixJS(net))
 		}
@@ -158,6 +249,19 @@ 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, []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:
@@ -172,7 +276,7 @@ func (c *Cmix) EKVGet(_ js.Value, args []js.Value) any {
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
 		val, err := c.api.EKVGet(key)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(utils.CopyBytesToJS(val))
 		}
@@ -197,7 +301,7 @@ func (c *Cmix) EKVSet(_ js.Value, args []js.Value) any {
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
 		err := c.api.EKVSet(key, val)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(nil)
 		}
diff --git a/wasm/collective.go b/wasm/collective.go
new file mode 100644
index 0000000000000000000000000000000000000000..e7202220100ea61ef47a672bf605b385a588398d
--- /dev/null
+++ b/wasm/collective.go
@@ -0,0 +1,678 @@
+////////////////////////////////////////////////////////////////////////////////
+// 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),
+		"GetAllRemoteKeyListeners": js.FuncOf(
+			rkv.GetAllRemoteKeyListeners),
+		"GetRemoteKeyListeners": js.FuncOf(rkv.GetRemoteKeyListeners),
+		"DeleteRemoteKeyListener": js.FuncOf(
+			rkv.DeleteRemoteKeyListener),
+		"GetAllRemoteMapListeners": js.FuncOf(
+			rkv.GetAllRemoteMapListeners),
+		"GetRemoteMapListeners": js.FuncOf(rkv.GetRemoteMapListeners),
+		"DeleteRemoteMapListener": js.FuncOf(
+			rkv.DeleteRemoteMapListener),
+	}
+
+	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 promise:
+//   - Resolves to the prefix (string).
+func (r *RemoteKV) GetPrefix(js.Value, []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.
+//
+// Returns a promise:
+//   - Resolves to a new [RemoteKV].
+//   - Rejected with an error on failure.
+func (r *RemoteKV) Root(js.Value, []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.
+//
+// Returns a promise:
+//   - Resolves to a boolean.
+//   - Rejected with an error.
+func (r *RemoteKV) IsMemStore(js.Value, []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 ID of the callback or -1.
+// The version and "localEvents" flags are only respected on first call.
+//
+// Parameters:
+//   - args[0] - The key (string).
+//   - args[1] - The version (int).
+//   - args[2] - The [KeyChangedByRemoteCallback] Javascript callback.
+//   - args[3] - Toggle local events (optional) (boolean).
+//
+// Returns a promise:
+//   - Resolves to the callback ID (int).
+//   - Rejects with an error on failure.
+func (r *RemoteKV) ListenOnRemoteKey(_ js.Value, args []js.Value) any {
+	key := args[0].String()
+	version := int64(args[1].Int())
+	cb := newKeyChangedByRemoteCallback(args[2])
+
+	localEvents := true
+	if len(args) > 3 && !args[3].IsUndefined() {
+		localEvents = args[3].Bool()
+	}
+
+	id, err := r.api.ListenOnRemoteKey(key, version, cb,
+		localEvents)
+	if err != nil {
+		exception.ThrowTrace(err)
+	}
+	return id
+}
+
+// ListenOnRemoteMap allows the caller to receive updates when the map or map
+// elements are updated. It returns the ID of the callback or -1 and an error.
+// The version and "localEvents" flags are only respected on first call.
+//
+// Parameters:
+//   - args[0] - The key (string).
+//   - args[1] - The version (int).
+//   - args[2] - the [MapChangedByRemoteCallback] javascript callback
+//   - args[3] - Toggle local events (optional) (boolean).
+//
+// Returns a promise:
+//   - Resolves to the callback ID (int).
+//   - Rejects with an error on failure.
+func (r *RemoteKV) ListenOnRemoteMap(_ js.Value, args []js.Value) any {
+	mapName := args[0].String()
+	version := int64(args[1].Int())
+	cb := newMapChangedByRemoteCallback(args[2])
+
+	localEvents := true
+	if len(args) > 3 && !args[3].IsUndefined() {
+		localEvents = args[3].Bool()
+	}
+
+	id, err := r.api.ListenOnRemoteMap(mapName, version, cb,
+		localEvents)
+	if err != nil {
+		exception.ThrowTrace(err)
+	}
+	return id
+}
+
+// GetAllRemoteKeyListeners returns a JSON list of { key: [id, id, id, ...] },
+// where key is the key for the listener and the list is an list of integer ids
+// of each listener.
+func (r *RemoteKV) GetAllRemoteKeyListeners(_ js.Value, args []js.Value) any {
+	return r.api.GetAllRemoteKeyListeners()
+}
+
+// GeRemoteKeyListeners returns a JSON list of [id, id, id, ...],
+// where the list is an list of integer ids of each listener.
+//
+// Parameters:
+//   - args[0] - the key to look at
+func (r *RemoteKV) GetRemoteKeyListeners(_ js.Value, args []js.Value) any {
+	key := args[0].String()
+	return r.api.GetRemoteKeyListeners(key)
+}
+
+// DeleteRemoteKeyListener deletes a specific listener for a key.
+//
+// Parameters:
+//   - args[0] - the key to delete for
+//   - args[1] - the id of the listener
+func (r *RemoteKV) DeleteRemoteKeyListener(_ js.Value, args []js.Value) any {
+	key := args[0].String()
+	id := args[1].Int()
+
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		err := r.api.DeleteRemoteKeyListener(key, id)
+		if err != nil {
+			reject(exception.NewTrace(err))
+		} else {
+			resolve()
+		}
+	}
+	return utils.CreatePromise(promiseFn)
+}
+
+// GetAllRemoteMapListeners returns a JSON list of { key: [id, id, id, ...] },
+// where key is the key for the listener and the list is an list of integer ids
+// of each listener.
+func (r *RemoteKV) GetAllRemoteMapListeners(_ js.Value, args []js.Value) any {
+	return r.api.GetAllRemoteMapListeners()
+}
+
+// GeRemoteMapListeners returns a JSON list of [id, id, id, ...],
+// where the list is an list of integer ids of each listener.
+//
+// Parameters:
+//   - args[0] - the key to look at
+func (r *RemoteKV) GetRemoteMapListeners(_ js.Value, args []js.Value) any {
+	key := args[0].String()
+	return r.api.GetRemoteMapListeners(key)
+}
+
+// DeleteRemoteMapListener deletes a specific listener for a key.
+//
+// Parameters:
+//   - args[0] - the mapName to delete for
+//   - args[1] - the id of the listener
+func (r *RemoteKV) DeleteRemoteMapListener(_ js.Value, args []js.Value) any {
+	mapName := args[0].String()
+	id := args[1].Int()
+
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		err := r.api.DeleteRemoteMapListener(mapName, id)
+		if err != nil {
+			reject(exception.NewTrace(err))
+		} else {
+			resolve()
+		}
+	}
+	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) {
+	v, awaitErr := utils.Await(rsCB.read(path))
+	if awaitErr != nil {
+		return nil, js.Error{Value: awaitErr[0]}
+	}
+	return utils.CopyBytesToGo(v[0]), nil
+}
+
+// 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 {
+	_, awaitErr := utils.Await(rsCB.write(path, utils.CopyBytesToJS(data)))
+	if awaitErr != nil {
+		return js.Error{Value: awaitErr[0]}
+	}
+	return nil
+}
+
+// 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) (string, error) {
+	v, err := utils.Await(rsCB.getLastModified(path))
+	if err != nil {
+		return "", js.Error{Value: err[0]}
+	}
+	return v[0].String(), nil
+}
+
+// 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() (string, error) {
+	v, err := utils.Await(rsCB.getLastWrite())
+	if err != nil {
+		return "", js.Error{Value: err[0]}
+	}
+	return v[0].String(), nil
+}
+
+// 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) {
+	v, awaitErr := utils.Await(rsCB.readDir(path))
+	if awaitErr != nil {
+		return nil, js.Error{Value: awaitErr[0]}
+	}
+	return utils.CopyBytesToGo(v[0]), nil
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// 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 50aebb08a1ac9bdbce7ba5d7e53c2f0591a24577..249f59e95c8f68b4e0d457d97a560faf3442f6a1 100644
--- a/wasm/dm.go
+++ b/wasm/dm.go
@@ -10,7 +10,6 @@
 package wasm
 
 import (
-	"crypto/ed25519"
 	"encoding/base64"
 	"encoding/json"
 	"syscall/js"
@@ -20,8 +19,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"
 )
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -49,23 +49,49 @@ func newDMClientJS(api *bindings.DMClient) map[string]any {
 		"ExportPrivateIdentity": js.FuncOf(cm.ExportPrivateIdentity),
 		"GetNickname":           js.FuncOf(cm.GetNickname),
 		"SetNickname":           js.FuncOf(cm.SetNickname),
+		"BlockPartner":          js.FuncOf(cm.BlockPartner),
+		"UnblockPartner":        js.FuncOf(cm.UnblockPartner),
 		"IsBlocked":             js.FuncOf(cm.IsBlocked),
-		"GetBlockedSenders":     js.FuncOf(cm.GetBlockedSenders),
+		"GetBlockedPartners":    js.FuncOf(cm.GetBlockedPartners),
 		"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),
-		"Send":         js.FuncOf(cm.Send),
+		"SendText":      js.FuncOf(cm.SendText),
+		"SendReply":     js.FuncOf(cm.SendReply),
+		"SendReaction":  js.FuncOf(cm.SendReaction),
+		"SendSilent":    js.FuncOf(cm.SendSilent),
+		"SendInvite":    js.FuncOf(cm.SendInvite),
+		"DeleteMessage": js.FuncOf(cm.DeleteMessage),
+		"Send":          js.FuncOf(cm.Send),
+
+		// Notifications
+		"GetNotificationLevel": js.FuncOf(cm.GetNotificationLevel),
+		"SetMobileNotificationsLevel": js.FuncOf(
+			cm.SetMobileNotificationsLevel),
 	}
 
 	return dmClientMap
 }
 
+// dmCallbacks wraps Javascript callbacks to adhere to the
+// [bindings.DmCallbacks] interface.
+type dmCallbacks struct {
+	eventUpdate func(args ...any) js.Value
+}
+
+// newDmCallbacks adds the callbacks from the Javascript object.
+func newDmCallbacks(value js.Value) *dmCallbacks {
+	return &dmCallbacks{eventUpdate: utils.WrapCB(value, "EventUpdate")}
+}
+
+// EventUpdate implements [bindings.DmCallbacks.EventUpdate].
+func (dmCBS *dmCallbacks) EventUpdate(eventType int64, jsonData []byte) {
+	dmCBS.eventUpdate(eventType, utils.CopyBytesToJS(jsonData))
+}
+
 // NewDMClient creates a new [DMClient] from a private identity
 // ([codename.PrivateIdentity]), used for direct messaging.
 //
@@ -77,22 +103,32 @@ func newDMClientJS(api *bindings.DMClient) map[string]any {
 // Parameters:
 //   - args[0] - ID of [Cmix] object in tracker (int). This can be retrieved
 //     using [Cmix.GetID].
-//   - args[1] - Bytes of a private identity ([codename.PrivateIdentity]) that
+//   - args[1] - ID of [Notifications] object in tracker. This can be retrieved
+//     using [Notifications.GetID] (int).
+//   - args[2] - Bytes of a private identity ([codename.PrivateIdentity]) that
 //     is generated by [codename.GenerateIdentity] (Uint8Array).
-//   - args[2] - A function that initialises and returns a Javascript object
+//   - 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] - A Javascript object that implements the function on
+//     [bindings.DmCallbacks]. It is a callback that informs the UI about
+//     updates relating to DM conversations. The interface may be null, but if
+//     one is provided, each method must be implemented.
 //
 // 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])
+	cmixID := args[0].Int()
+	notificationsID := args[1].Int()
+	privateIdentity := utils.CopyBytesToGo(args[2])
+	em := newDMReceiverBuilder(args[3])
+	cbs := newDmCallbacks(args[4])
 
-	cm, err := bindings.NewDMClient(args[0].Int(), privateIdentity, em)
+	cm, err :=
+		bindings.NewDMClient(cmixID, notificationsID, privateIdentity, em, cbs)
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 		return nil
 	}
 
@@ -113,36 +149,37 @@ func NewDMClient(_ js.Value, args []js.Value) any {
 // Parameters:
 //   - args[0] - ID of [Cmix] object in tracker (int). This can be retrieved
 //     using [Cmix.GetID].
-//   - args[1] - Path to Javascript file that starts the worker (string).
-//   - args[2] - Bytes of a private identity ([codename.PrivateIdentity]) that
+//   - args[1] - ID of [Notifications] object in tracker. This can be retrieved
+//     using [Notifications.GetID] (int).
+//   - args[2] - ID of [DbCipher] object in tracker (int). Create this object
+//     with [NewDatabaseCipher] and get its id with [DbCipher.GetID].
+//   - args[3] - Path to Javascript file that starts the worker (string).
+//   - args[4] - Bytes of a private identity ([codename.PrivateIdentity]) that
 //     is generated by [codename.GenerateIdentity] (Uint8Array).
-//   - args[3] - The message receive callback. It is a function that takes in
-//     the same parameters as [dm.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.
-//     messageUpdate is true if the message already exists and was edited.
-//     conversationUpdate is true if the Conversation was created or modified.
-//   - args[4] - ID of [DMDbCipher] object in tracker (int). Create this object
-//     with [NewDMsDatabaseCipher] and get its id with [DMDbCipher.GetID].
+//   - args[5] - A Javascript object that implements the function on
+//     [bindings.DmCallbacks]. It is a callback that informs the UI about
+//     updates relating to DM conversations. The interface may be null, but if
+//     one is provided, each method must be implemented.
 //
 // 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()
-	privateIdentity := utils.CopyBytesToGo(args[2])
-	messageReceivedCB := args[3]
-	cipherID := args[4].Int()
+	notificationsID := args[1].Int()
+	cipherID := args[2].Int()
+	wasmJsPath := args[3].String()
+	privateIdentity := utils.CopyBytesToGo(args[4])
+	cbs := newDmCallbacks(args[5])
 
-	cipher, err := bindings.GetDMDbCipherTrackerFromID(cipherID)
+	cipher, err := dbCipherTrackerSingleton.get(cipherID)
 	if err != nil {
-		utils.Throw(utils.TypeError, err)
+		exception.ThrowTrace(err)
 	}
 
 	return newDMClientWithIndexedDb(
-		cmixID, wasmJsPath, privateIdentity, messageReceivedCB, cipher)
+		cmixID, notificationsID, wasmJsPath, privateIdentity, cipher, cbs)
 }
 
 // NewDMClientWithIndexedDbUnsafe creates a new [DMClient] from a private
@@ -160,55 +197,49 @@ func NewDMClientWithIndexedDb(_ js.Value, args []js.Value) any {
 // Parameters:
 //   - args[0] - ID of [Cmix] object in tracker (int). This can be retrieved
 //     using [Cmix.GetID].
-//   - args[1] - Path to Javascript file that starts the worker (string).
-//   - args[2] - Bytes of a private identity ([codename.PrivateIdentity]) that
+//   - args[1] - ID of [Notifications] object in tracker. This can be retrieved
+//     using [Notifications.GetID] (int).
+//   - args[2] - Path to Javascript file that starts the worker (string).
+//   - args[3] - Bytes of a private identity ([codename.PrivateIdentity]) that
 //     is generated by [codename.GenerateIdentity] (Uint8Array).
-//   - args[3] - The message receive callback. It is a function that takes in
-//     the same parameters as [dm.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.
-//     messageUpdate is true if the message already exists and was edited.
-//     conversationUpdate is true if the Conversation was created or modified.
+//   - args[4] - A Javascript object that implements the function on
+//     [bindings.DmCallbacks]. It is a callback that informs the UI about
+//     updates relating to DM conversations. The interface may be null, but if
+//     one is provided, each method must be implemented.
 //
 // Returns a promise:
 //   - Resolves to a Javascript representation of the [DMClient] object.
 //   - Rejected with an error if loading indexedDbWorker or the manager fails.
 func NewDMClientWithIndexedDbUnsafe(_ js.Value, args []js.Value) any {
 	cmixID := args[0].Int()
-	wasmJsPath := args[1].String()
-	privateIdentity := utils.CopyBytesToGo(args[2])
-	messageReceivedCB := args[3]
+	notificationsID := args[1].Int()
+	wasmJsPath := args[2].String()
+	privateIdentity := utils.CopyBytesToGo(args[3])
+	cbs := newDmCallbacks(args[4])
 
 	return newDMClientWithIndexedDb(
-		cmixID, wasmJsPath, privateIdentity, messageReceivedCB, nil)
+		cmixID, notificationsID, wasmJsPath, privateIdentity, nil, cbs)
 }
 
-func newDMClientWithIndexedDb(cmixID int, wasmJsPath string,
-	privateIdentity []byte, cb js.Value, cipher *bindings.DMDbCipher) any {
-
-	messageReceivedCB := func(uuid uint64, pubKey ed25519.PublicKey,
-		messageUpdate, conversationUpdate bool) {
-		cb.Invoke(uuid, utils.CopyBytesToJS(pubKey[:]),
-			messageUpdate, conversationUpdate)
-	}
+func newDMClientWithIndexedDb(cmixID, notificationsID int, wasmJsPath string,
+	privateIdentity []byte, cipher *DbCipher, cbs *dmCallbacks) any {
 
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
-
 		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)
+		model, err :=
+			indexDB.NewWASMEventModel(dmPath, wasmJsPath, cipher.api, cbs)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		}
 
 		cm, err := bindings.NewDMClientWithGoEventModel(
-			cmixID, privateIdentity, model)
+			cmixID, notificationsID, privateIdentity, model, cbs)
 		if err != nil {
-			reject(utils.JsTrace(err))
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(newDMClientJS(cm))
 		}
@@ -259,7 +290,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
 	}
 
@@ -271,11 +302,11 @@ func (dmc *DMClient) ExportPrivateIdentity(_ js.Value, args []js.Value) any {
 //
 // Returns:
 //   - The nickname (string).
-//   - Throws TypeError if the channel has no nickname set.
+//   - Throws an error if the channel has no nickname set.
 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
 	}
 
@@ -286,38 +317,82 @@ func (dmc *DMClient) GetNickname(_ js.Value, _ []js.Value) any {
 //
 // Parameters:
 //   - args[0] - The nickname to set (string).
+//
+// Returns:
+//   - Throws an error if setting the nickname fails.
 func (dmc *DMClient) SetNickname(_ js.Value, args []js.Value) any {
-	dmc.api.SetNickname(args[0].String())
+	err := dmc.api.SetNickname(args[0].String())
+	if err != nil {
+		exception.ThrowTrace(err)
+		return nil
+	}
+
 	return nil
 }
 
-// IsBlocked indicates if the given sender is blocked.
-// Blocking is controlled by the receiver/EventModel.
+// BlockPartner prevents receiving messages and notifications from the partner.
 //
 // Parameters:
-//   - args[0] - Bytes of the sender's ED25519 public key (Uint8Array).
+//   - args[0] - The partner's [ed25519.PublicKey] key to block (Uint8Array).
+//
+// Returns a promise that exits upon completion.
+func (dmc *DMClient) BlockPartner(_ js.Value, args []js.Value) any {
+	partnerPubKey := utils.CopyBytesToGo(args[0])
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		dmc.api.BlockPartner(partnerPubKey)
+		resolve()
+	}
+	return utils.CreatePromise(promiseFn)
+}
+
+// UnblockPartner unblocks a blocked partner to allow DM messages.
+//
+// Parameters:
+//   - args[0] - The partner's [ed25519.PublicKey] to unblock (Uint8Array).
+func (dmc *DMClient) UnblockPartner(_ js.Value, args []js.Value) any {
+	partnerPubKey := utils.CopyBytesToGo(args[0])
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		dmc.api.UnblockPartner(partnerPubKey)
+		resolve()
+	}
+	return utils.CreatePromise(promiseFn)
+}
+
+// IsBlocked indicates if the given partner is blocked.
+//
+// Parameters:
+//   - args[0] - The partner's [ed25519.PublicKey] public key to check
+//     (Uint8Array).
 //
 // Returns:
 //   - boolean
 func (dmc *DMClient) IsBlocked(_ js.Value, args []js.Value) any {
-	return dmc.api.IsBlocked(utils.CopyBytesToGo(args[0]))
+	partnerPubKey := utils.CopyBytesToGo(args[0])
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		isBlocked := dmc.api.IsBlocked(partnerPubKey)
+		resolve(isBlocked)
+	}
+	return utils.CreatePromise(promiseFn)
 }
 
-// GetBlockedSenders returns the ED25519 public keys of all senders who are
-// blocked by this user. Blocking is controlled by the receiver/EventModel.
+// GetBlockedPartners returns all partners who are blocked by this user.
 //
 // Returns:
 //   - JSON of an array of [ed25519.PublicKey] (Uint8Array)
 //
-// Example JSON return:
+// Example return:
 //
 //	[
-//	  "v3TON4ju4FxFvp3D/Df0OFV50QSqmiHPQ/BOHMwRRJ8=",
-//	  "ZsehfwnncIx4NC8WZyhbypC3nfGsiqU21T+bPRC+iIU=",
-//	  "ZuBal443tYZ4j025A6q9xU7xn9ZQF5xB1hbh6LxpBAQ="
+//	  "TYWuCfyGBjNWDtl/Roa6f/o206yYPpuB6sX2kJZTe98=",
+//	  "4JLRzgtW1SZ9c5pE+v0WwrGPj1t19AuU6Gg5IND5ymA=",
+//	  "CWDqF1bnhulW2pko+zgmbDZNaKkmNtFdUgY4bTm2DhA="
 //	]
-func (dmc *DMClient) GetBlockedSenders(_ js.Value, args []js.Value) any {
-	return dmc.api.IsBlocked(utils.CopyBytesToGo(args[0]))
+func (dmc *DMClient) GetBlockedPartners(js.Value, []js.Value) any {
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		blocked := utils.CopyBytesToJS(dmc.api.GetBlockedPartners())
+		resolve(blocked)
+	}
+	return utils.CreatePromise(promiseFn)
 }
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -359,7 +434,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))
 		}
@@ -417,7 +492,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))
 		}
@@ -432,7 +507,8 @@ func (dmc *DMClient) SendReply(_ js.Value, args []js.Value) any {
 // Users will drop the reaction if they do not recognize the reactTo message.
 //
 // Parameters:
-//   - args[0] - Marshalled bytes of the channel [id.ID] (Uint8Array).
+//   - 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] - The user's reaction. This should be a single emoji with no
 //     other characters. As such, a Unicode string is expected (string).
@@ -441,7 +517,7 @@ func (dmc *DMClient) SendReply(_ js.Value, args []js.Value) any {
 //     your own. Alternatively, if reacting to another user's message, you may
 //     retrieve it via the [bindings.ChannelMessageReceptionCallback] registered
 //     using [ChannelsManager.RegisterReceiveHandler] (Uint8Array).
-//   - args[3] - JSON of [xxdk.CMIXParams]. If left empty
+//   - args[4] - JSON of [xxdk.CMIXParams]. If left empty
 //     [bindings.GetDefaultCMixParams] will be used internally (Uint8Array).
 //
 // Returns a promise:
@@ -464,7 +540,128 @@ 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))
+		}
+	}
+
+	return utils.CreatePromise(promiseFn)
+}
+
+// SendInvite is used to send to a DM partner an invitation to another
+// channel.
+//
+// If the channel ID for the invitee channel is not recognized by the Manager,
+// then an error will be returned.
+//
+// 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 the invitee channel [id.ID].
+//     This can be retrieved from [GetChannelJSON]. (Uint8Array).
+//   - args[3] - The contents of the message. The message should be at most 510
+//     bytes. This is expected to be Unicode, and thus a string data type is
+//     expected.
+//   - args[4] - The URL to append the channel info to.
+//   - args[5] - A JSON marshalled [xxdk.CMIXParams]. This may be empty,
+//     and GetDefaultCMixParams will be used internally.
+//
+// Returns a promise:
+//   - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array).
+//   - Rejected with an error if sending fails.
+func (dmc *DMClient) SendInvite(_ js.Value, args []js.Value) any {
+	var (
+		partnerPubKeyBytes = utils.CopyBytesToGo(args[0])
+		partnerToken       = int32(args[1].Int())
+		inviteToJSON       = utils.CopyBytesToGo(args[2])
+		msg                = args[3].String()
+		host               = args[4].String()
+		cmixParamsJSON     = utils.CopyBytesToGo(args[5])
+	)
+
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		sendReport, err := dmc.api.SendInvite(
+			partnerPubKeyBytes, partnerToken, inviteToJSON, msg, host,
+			cmixParamsJSON)
+		if err != nil {
+			reject(exception.NewTrace(err))
+		} else {
+			resolve(utils.CopyBytesToJS(sendReport))
+		}
+	}
+
+	return utils.CreatePromise(promiseFn)
+}
+
+// DeleteMessage sends a message to the partner to delete a message this user
+// sent. Also deletes it from the local database.
+//
+// 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] -The bytes of the [message.ID] of the message to delete. This may
+//     be found in the [bindings.ChannelSendReport] (Uint8Array).
+//   - args[3] - 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) DeleteMessage(_ js.Value, args []js.Value) any {
+	partnerPubKeyBytes := utils.CopyBytesToGo(args[0])
+	partnerToken := int32(args[1].Int())
+	targetMessageIdBytes := utils.CopyBytesToGo(args[2])
+	cmixParamsJSON := utils.CopyBytesToGo(args[3])
+
+	jww.DEBUG.Printf("DeleteMessage(%s, %d, %s)",
+		base64.RawStdEncoding.EncodeToString(partnerPubKeyBytes)[:8],
+		partnerToken,
+		base64.RawStdEncoding.EncodeToString(targetMessageIdBytes))
+
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		sendReport, err := dmc.api.DeleteMessage(partnerPubKeyBytes,
+			partnerToken, targetMessageIdBytes, cmixParamsJSON)
+		if err != nil {
+			reject(exception.NewTrace(err))
 		} else {
 			resolve(utils.CopyBytesToJS(sendReport))
 		}
@@ -515,7 +712,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))
 		}
@@ -566,7 +763,7 @@ type DMUser struct {
 	PublicKey []byte `json:"publicKey"`
 }
 
-// GetShareURL generates a URL that can be used to share a URL to initiate a
+// GetShareURL generates a URL that can be used to share a URL to initiate d
 // direct messages with this user.
 //
 // Parameters:
@@ -574,17 +771,72 @@ type DMUser struct {
 //
 // Returns:
 //   - JSON of [DMShareURL] (Uint8Array).
+//   - Throws an exception on error.
 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
 	}
 
 	return utils.CopyBytesToJS(urlReport)
 }
 
+// GetNotificationLevel gets the notification level for a given DM partner's
+// public key
+//
+// Parameters:
+//   - args[0] - The partner's [ed25519.PublicKey] (Uint8Array)
+//
+// Returns:
+//   - The [dm.NotificationLevel] of the DM conversation (int).
+//
+// Returns a promise:
+//   - Resolves to the [dm.NotificationLevel] of the DM conversation (int).
+//   - Rejected with an error if getting the notification level fails.
+func (dmc *DMClient) GetNotificationLevel(_ js.Value, args []js.Value) any {
+	partnerPubKey := utils.CopyBytesToGo(args[0])
+
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		level, err := dmc.api.GetNotificationLevel(partnerPubKey)
+		if err != nil {
+			reject(exception.NewTrace(err))
+		} else {
+			resolve(level)
+		}
+	}
+
+	return utils.CreatePromise(promiseFn)
+}
+
+// SetMobileNotificationsLevel sets the notification level for the given DM
+// conversation partner.
+//
+// Parameters:
+//   - args[0] - The partner's [ed25519.PublicKey] (Uint8Array).
+//   - args[1] - The [dm.NotificationLevel] to set for the DM conversation (int).
+//
+// Returns a promise:
+//   - Resolves on success.
+//   - Rejected with an error if setting the notification level fails.
+func (dmc *DMClient) SetMobileNotificationsLevel(_ js.Value,
+	args []js.Value) any {
+	partnerPubKey := utils.CopyBytesToGo(args[0])
+	level := args[1].Int()
+
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		err := dmc.api.SetMobileNotificationsLevel(partnerPubKey, level)
+		if err != nil {
+			reject(exception.NewTrace(err))
+		} else {
+			resolve()
+		}
+	}
+
+	return utils.CreatePromise(promiseFn)
+}
+
 // DecodeDMShareURL decodes the user's URL into a [DMUser].
 //
 // Parameters:
@@ -593,45 +845,69 @@ func (dmc *DMClient) GetShareURL(_ js.Value, args []js.Value) any {
 //
 // Returns:
 //   - JSON of [DMUser] (Uint8Array).
+//   - Throws an exception on error.
 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
 	}
 
 	return utils.CopyBytesToJS(report)
 }
 
-////////////////////////////////////////////////////////////////////////////////
-// Channel Receiving Logic and Callback Registration                          //
-////////////////////////////////////////////////////////////////////////////////
-
-// channelMessageReceptionCallback wraps Javascript callbacks to adhere to the
-// [bindings.ChannelMessageReceptionCallback] interface.
-type dmReceptionCallback struct {
-	callback func(args ...any) js.Value
-}
-
-// Callback returns the context for a channel message.
+// GetDmNotificationReportsForMe 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:
-//   - receivedChannelMessageReport - Returns the JSON of
-//     [bindings.ReceivedChannelMessageReport] (Uint8Array).
-//   - err - Returns an error on failure (Error).
+//   - args[0] - JSON of [dm.NotificationFilter] (Uint8Array).
+//   - args[1] - CSV containing notification data (string).
 //
-// Returns:
-//   - It must return a unique UUID for the message that it can be referenced by
-//     later (int).
-func (cmrCB *dmReceptionCallback) Callback(
-	receivedChannelMessageReport []byte, err error) int {
-	uuid := cmrCB.callback(
-		utils.CopyBytesToJS(receivedChannelMessageReport),
-		utils.JsTrace(err))
-
-	return uuid.Int()
+// Example JSON of a slice of [dm.NotificationFilter]:
+//
+//	{
+//	  "identifier": "MWL6mvtZ9UUm7jP3ainyI4erbRl+wyVaO5MOWboP0rA=",
+//	  "myID": "AqDqg6Tcs359dBNRBCX7XHaotRDhz1ZRQNXIsGaubvID",
+//	  "tags": [
+//	    "61334HtH85DPIifvrM+JzRmLqfV5R4AMEmcPelTmFX0=",
+//	    "zc/EPwtx5OKTVdwLcI15bghjJ7suNhu59PcarXE+m9o=",
+//	    "FvArzVJ/082UEpMDCWJsopCLeLnxJV6NXINNkJTk3k8="
+//	  ],
+//	  "PublicKeys": {
+//	    "61334HtH85DPIifvrM+JzRmLqfV5R4AMEmcPelTmFX0=": "b3HygDv8gjteune9wgBm3YtVuAo2foOusRmj0m5nl6E=",
+//	    "FvArzVJ/082UEpMDCWJsopCLeLnxJV6NXINNkJTk3k8=": "uOLitBZcCh2TEW406jXHJ+Rsi6LybsH8R1u4Mxv/7hA=",
+//	    "zc/EPwtx5OKTVdwLcI15bghjJ7suNhu59PcarXE+m9o=": "lqLD1EzZBxB8PbILUJIfFq4JI0RKThpUQuNlTNgZAWk="
+//	  },
+//	  "allowedTypes": {"1": {}, "2": {}}
+//	}
+//
+// Returns a promise:
+//   - Resolves to a JSON of a slice of [dm.NotificationReport] (Uint8Array).
+//   - Rejected with an error if getting the reports fails.
+//
+// Example slice of [dm.NotificationReport] return:
+//
+//	[
+//	  {"partner": "WUSO3trAYeBf4UeJ5TEL+Q4usoyFf0shda0YUmZ3z8k=", "type": 1},
+//	  {"partner": "5MY652JsVv5YLE6wGRHIFZBMvLklACnT5UtHxmEOJ4o=", "type": 2}
+//	]
+func GetDmNotificationReportsForMe(_ js.Value, args []js.Value) any {
+	notificationFilterJson := utils.CopyBytesToGo(args[0])
+	notificationDataCsv := args[1].String()
+
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		forMe, err := bindings.GetDmNotificationReportsForMe(
+			notificationFilterJson, notificationDataCsv)
+		if err != nil {
+			reject(exception.NewTrace(err))
+		} else {
+			resolve(utils.CopyBytesToJS(forMe))
+		}
+	}
+
+	return utils.CreatePromise(promiseFn)
 }
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -660,6 +936,9 @@ func (emb *dmReceiverBuilder) Build(path string) bindings.DMReceiver {
 		receiveReply:     utils.WrapCB(emJs, "ReceiveReply"),
 		receiveReaction:  utils.WrapCB(emJs, "ReceiveReaction"),
 		updateSentStatus: utils.WrapCB(emJs, "UpdateSentStatus"),
+		deleteMessage:    utils.WrapCB(emJs, "DeleteMessage"),
+		getConversation:  utils.WrapCB(emJs, "GetConversation"),
+		getConversations: utils.WrapCB(emJs, "GetConversations"),
 	}
 }
 
@@ -671,31 +950,34 @@ type dmReceiver struct {
 	receiveReply     func(args ...any) js.Value
 	receiveReaction  func(args ...any) js.Value
 	updateSentStatus func(args ...any) js.Value
-	blockSender      func(args ...any) js.Value
-	unblockSender    func(args ...any) js.Value
+	deleteMessage    func(args ...any) js.Value
 	getConversation  func(args ...any) js.Value
 	getConversations func(args ...any) js.Value
 }
 
-// Receive is called whenever a message is received on a given channel.
+// Receive is called when a raw direct message is received with unknown type.
 // It may be called multiple times on the same message. It is incumbent on the
 // user of the API to filter such called by message ID.
 //
+// The user must interpret the message type and perform their own message
+// parsing.
+//
 // Parameters:
-//   - channelID - Marshalled bytes of the channel [id.ID] (Uint8Array).
-//   - messageID - The bytes of the [channel.MessageID] of the received message
+//   - messageID - The bytes of the [dm.MessageID] of the received message
 //     (Uint8Array).
 //   - nickname - The nickname of the sender of the message (string).
-//   - text - The content of the message (string).
-//   - pubKey - The sender's Ed25519 public key (Uint8Array).
-//   - dmToken - The dmToken (int32).
-//   - codeset - The codeset version (int).
+//   - text - The bytes content of the message (Uint8Array).
+//   - partnerKey - The partner's [ed25519.PublicKey]. This is required to
+//     respond (Uint8Array).
+//   - senderKey - The sender's [ed25519.PublicKey] (Uint8Array).
+//   - dmToken - The senders direct messaging token. This is required to respond
+//     (int).
+//   - codeset - The codeset version (int)
 //   - timestamp - Time the message was received; represented as nanoseconds
 //     since unix epoch (int).
-//   - lease - The number of nanoseconds that the message is valid for (int).
-//   - roundId - The ID of the round that the message was received on (int).
-//   - msgType - The type of message ([channels.MessageType]) to send (int).
-//   - status - The [channels.SentStatus] of the message (int).
+//   - roundID - The ID of the round that the message was received on (int).
+//   - mType - The type of message ([channels.MessageType]) to send (int).
+//   - status - the [dm.SentStatus] of the message (int).
 //
 // Statuses will be enumerated as such:
 //
@@ -706,39 +988,42 @@ type dmReceiver struct {
 // Returns:
 //   - A non-negative unique UUID for the message that it can be referenced by
 //     later with [dmReceiver.UpdateSentStatus].
-func (em *dmReceiver) Receive(messageID []byte, nickname string,
-	text []byte, partnerKey, senderKey []byte, dmToken int32, codeset int, timestamp,
+func (em *dmReceiver) Receive(messageID []byte, nickname string, text,
+	partnerKey, senderKey []byte, dmToken int32, codeset int, timestamp,
 	roundId, mType, status int64) int64 {
-	uuid := em.receive(messageID, nickname, text, partnerKey, senderKey, dmToken,
-		codeset, timestamp, roundId, mType, status)
+	uuid := em.receive(utils.CopyBytesToJS(messageID), nickname,
+		utils.CopyBytesToJS(text), utils.CopyBytesToJS(partnerKey),
+		utils.CopyBytesToJS(senderKey),
+		dmToken, codeset, timestamp, roundId, mType, status)
 
 	return int64(uuid.Int())
 }
 
-// ReceiveText is called whenever a message is received that is a reply on a
-// given channel. It may be called multiple times on the same message. It is
-// incumbent on the user of the API to filter such called by message ID.
+// ReceiveText is called whenever a direct message is received that is a text
+// type. It may be called multiple times on the same message. It is incumbent on
+// the user of the API to filter such called by message ID.
 //
 // Messages may arrive our of order, so a reply in theory can arrive before the
 // initial message. As a result, it may be important to buffer replies.
 //
 // Parameters:
-//   - channelID - Marshalled bytes of the channel [id.ID] (Uint8Array).
-//   - messageID - The bytes of the [channel.MessageID] of the received message
+//   - messageID - The bytes of the [dm.MessageID] of the received message
 //     (Uint8Array).
-//   - reactionTo - The [channel.MessageID] for the message that received a
-//     reply (Uint8Array).
-//   - senderUsername - The username of the sender of the message (string).
+//   - nickname - The nickname of the sender of the message (string).
 //   - text - The content of the message (string).
-//   - partnerKey, senderKey - The sender's Ed25519 public key (Uint8Array).
-//   - dmToken - The dmToken (int32).
-//   - codeset - The codeset version (int).
+//   - partnerKey - The partner's [ed25519.PublicKey]. This is required to
+//     respond (Uint8Array).
+//   - senderKey - The sender's [ed25519.PublicKey] (Uint8Array).
+//   - dmToken - The senders direct messaging token. This is required to respond
+//     (int).
+//   - codeset - The codeset version (int)
 //   - timestamp - Time the message was received; represented as nanoseconds
 //     since unix epoch (int).
-//   - lease - The number of nanoseconds that the message is valid for (int).
 //   - roundId - The ID of the round that the message was received on (int).
 //   - msgType - The type of message ([channels.MessageType]) to send (int).
 //   - status - The [channels.SentStatus] of the message (int).
+//   - roundID - The ID of the round that the message was received on (int).
+//   - status - the [dm.SentStatus] of the message (int).
 //
 // Statuses will be enumerated as such:
 //
@@ -753,36 +1038,40 @@ func (em *dmReceiver) ReceiveText(messageID []byte, nickname, text string,
 	partnerKey, senderKey []byte, dmToken int32, codeset int, timestamp,
 	roundId, status int64) int64 {
 
-	uuid := em.receiveText(messageID, nickname, text, partnerKey, senderKey, dmToken,
-		codeset, timestamp, roundId, status)
+	uuid := em.receiveText(utils.CopyBytesToJS(messageID), nickname, text,
+		utils.CopyBytesToJS(partnerKey), utils.CopyBytesToJS(senderKey),
+		dmToken, codeset, timestamp, roundId, status)
 
 	return int64(uuid.Int())
 }
 
-// ReceiveReply is called whenever a message is received that is a reply on a
-// given channel. It may be called multiple times on the same message. It is
-// incumbent on the user of the API to filter such called by message ID.
+// ReceiveReply is called whenever a direct message is received that is a reply.
+// It may be called multiple times on the same message. It is incumbent on the
+// user of the API to filter such called by message ID.
 //
 // Messages may arrive our of order, so a reply in theory can arrive before the
 // initial message. As a result, it may be important to buffer replies.
 //
 // Parameters:
-//   - channelID - Marshalled bytes of the channel [id.ID] (Uint8Array).
-//   - messageID - The bytes of the [channel.MessageID] of the received message
+//   - messageID - The bytes of the [dm.MessageID] of the received message
 //     (Uint8Array).
-//   - reactionTo - The [channel.MessageID] for the message that received a
-//     reply (Uint8Array).
-//   - senderUsername - The username of the sender of the message (string).
+//   - reactionTo - The [dm.MessageID] for the message that received a reply
+//     (Uint8Array).
+//   - nickname - The nickname of the sender of the message (string).
 //   - text - The content of the message (string).
-//   - partnerKey, senderKey - The sender's Ed25519 public key (Uint8Array).
-//   - dmToken - The dmToken (int32).
-//   - codeset - The codeset version (int).
+//   - partnerKey - The partner's [ed25519.PublicKey]. This is required to
+//     respond (Uint8Array).
+//   - senderKey - The sender's [ed25519.PublicKey] (Uint8Array).
+//   - dmToken - The senders direct messaging token. This is required to respond
+//     (int).
+//   - codeset - The codeset version (int)
 //   - timestamp - Time the message was received; represented as nanoseconds
 //     since unix epoch (int).
-//   - lease - The number of nanoseconds that the message is valid for (int).
 //   - roundId - The ID of the round that the message was received on (int).
 //   - msgType - The type of message ([channels.MessageType]) to send (int).
 //   - status - The [channels.SentStatus] of the message (int).
+//   - roundID - The ID of the round that the message was received on (int).
+//   - status - the [dm.SentStatus] of the message (int).
 //
 // Statuses will be enumerated as such:
 //
@@ -793,39 +1082,44 @@ func (em *dmReceiver) ReceiveText(messageID []byte, nickname, text string,
 // Returns:
 //   - A non-negative unique UUID for the message that it can be referenced by
 //     later with [dmReceiver.UpdateSentStatus].
-func (em *dmReceiver) ReceiveReply(messageID, replyTo []byte, nickname,
+func (em *dmReceiver) ReceiveReply(messageID, reactionTo []byte, nickname,
 	text string, partnerKey, senderKey []byte, dmToken int32, codeset int,
 	timestamp, roundId, status int64) int64 {
-	uuid := em.receiveReply(messageID, replyTo, nickname, text, partnerKey, senderKey,
+	uuid := em.receiveReply(utils.CopyBytesToJS(messageID),
+		utils.CopyBytesToJS(reactionTo), nickname, text,
+		utils.CopyBytesToJS(partnerKey), utils.CopyBytesToJS(senderKey),
 		dmToken, codeset, timestamp, roundId, status)
 
 	return int64(uuid.Int())
 }
 
-// ReceiveReaction is called whenever a reaction to a message is received on a
-// given channel. It may be called multiple times on the same reaction. It is
+// ReceiveReaction is called whenever a reaction to a direct message is
+// received. It may be called multiple times on the same reaction. It is
 // incumbent on the user of the API to filter such called by message ID.
 //
 // Messages may arrive our of order, so a reply in theory can arrive before the
 // initial message. As a result, it may be important to buffer reactions.
 //
 // Parameters:
-//   - channelID - Marshalled bytes of the channel [id.ID] (Uint8Array).
-//   - messageID - The bytes of the [channel.MessageID] of the received message
+//   - messageID - The bytes of the [dm.MessageID] of the received message
 //     (Uint8Array).
-//   - reactionTo - The [channel.MessageID] for the message that received a
-//     reply (Uint8Array).
-//   - senderUsername - The username of the sender of the message (string).
-//   - reaction - The contents of the reaction message (string).
-//   - partnerKey, senderKey - The sender's Ed25519 public key (Uint8Array).
-//   - dmToken - The dmToken (int32).
-//   - codeset - The codeset version (int).
+//   - reactionTo - The [dm.MessageID] for the message that received a reply
+//     (Uint8Array).
+//   - nickname - The nickname of the sender of the message (string).
+//   - reaction - The content of the reaction message (string).
+//   - partnerKey - The partner's [ed25519.PublicKey]. This is required to
+//     respond (Uint8Array).
+//   - senderKey - The sender's [ed25519.PublicKey] (Uint8Array).
+//   - dmToken - The senders direct messaging token. This is required to respond
+//     (int).
+//   - codeset - The codeset version (int)
 //   - timestamp - Time the message was received; represented as nanoseconds
 //     since unix epoch (int).
-//   - lease - The number of nanoseconds that the message is valid for (int).
 //   - roundId - The ID of the round that the message was received on (int).
 //   - msgType - The type of message ([channels.MessageType]) to send (int).
 //   - status - The [channels.SentStatus] of the message (int).
+//   - roundID - The ID of the round that the message was received on (int).
+//   - status - the [dm.SentStatus] of the message (int).
 //
 // Statuses will be enumerated as such:
 //
@@ -838,16 +1132,16 @@ func (em *dmReceiver) ReceiveReply(messageID, replyTo []byte, nickname,
 //     later with [dmReceiver.UpdateSentStatus].
 func (em *dmReceiver) ReceiveReaction(messageID, reactionTo []byte,
 	nickname, reaction string, partnerKey, senderKey []byte, dmToken int32,
-	codeset int, timestamp, roundId,
-	status int64) int64 {
-	uuid := em.receiveReaction(messageID, reactionTo, nickname, reaction,
-		partnerKey, senderKey, dmToken, codeset, timestamp, roundId, status)
+	codeset int, timestamp, roundId, status int64) int64 {
+	uuid := em.receiveReaction(utils.CopyBytesToJS(messageID),
+		utils.CopyBytesToJS(reactionTo), nickname, reaction,
+		utils.CopyBytesToJS(partnerKey), utils.CopyBytesToJS(senderKey),
+		dmToken, codeset, timestamp, roundId, status)
 
 	return int64(uuid.Int())
 }
 
-// UpdateSentStatus is called whenever the sent status of a message has
-// changed.
+// UpdateSentStatus is called whenever the sent status of a message has changed.
 //
 // Parameters:
 //   - uuid - The unique identifier for the message (int).
@@ -863,39 +1157,37 @@ func (em *dmReceiver) ReceiveReaction(messageID, reactionTo []byte,
 //	Sent      =  0
 //	Delivered =  1
 //	Failed    =  2
-func (em *dmReceiver) UpdateSentStatus(uuid int64, messageID []byte,
-	timestamp, roundID, status int64) {
-	em.updateSentStatus(uuid, utils.CopyBytesToJS(messageID),
-		timestamp, roundID, status)
-}
-
-// BlockSender silences messages sent by the indicated sender
-// public key.
-//
-// Parameters:
-//   - senderPubKey - The unique public key for the conversation.
-func (em *dmReceiver) BlockSender(senderPubKey []byte) {
-	em.blockSender(senderPubKey)
+func (em *dmReceiver) UpdateSentStatus(
+	uuid int64, messageID []byte, timestamp, roundID, status int64) {
+	em.updateSentStatus(
+		uuid, utils.CopyBytesToJS(messageID), timestamp, roundID, status)
 }
 
-// UnblockSender silences messages sent by the indicated sender
-// public key.
+// DeleteMessage deletes the message with the given [message.ID] belonging to
+// the sender. If the message exists and belongs to the sender, then it is
+// deleted and [DeleteMessage] returns true. If it does not exist, it returns
+// false.
 //
 // Parameters:
-//   - senderPubKey - The unique public key for the conversation.
-func (em *dmReceiver) UnblockSender(senderPubKey []byte) {
-	em.unblockSender(senderPubKey)
+//   - messageID - The bytes of the [message.ID] of the message to delete
+//     (Uint8Array).
+//   - senderPubKey - The [ed25519.PublicKey] of the sender of the message
+//     (Uint8Array).
+func (em *dmReceiver) DeleteMessage(messageID, senderPubKey []byte) bool {
+	return em.deleteMessage(
+		utils.CopyBytesToJS(messageID), utils.CopyBytesToJS(senderPubKey)).Bool()
 }
 
 // GetConversation returns the conversation held by the model (receiver).
 //
 // Parameters:
-//   - senderPubKey - The unique public key for the conversation.
+//   - senderPubKey - The unique public key for the conversation (Uint8Array).
 //
 // Returns:
 //   - JSON of [dm.ModelConversation] (Uint8Array).
 func (em *dmReceiver) GetConversation(senderPubKey []byte) []byte {
-	result := utils.CopyBytesToGo(em.getConversation(senderPubKey))
+	result := utils.CopyBytesToGo(
+		em.getConversation(utils.CopyBytesToJS(senderPubKey)))
 
 	var conversation dm.ModelConversation
 	err := json.Unmarshal(result, &conversation)
@@ -924,146 +1216,6 @@ func (em *dmReceiver) GetConversations() []byte {
 	return conversationsBytes
 }
 
-////////////////////////////////////////////////////////////////////////////////
-// DM DB Cipher                                                               //
-////////////////////////////////////////////////////////////////////////////////
-
-// DMDbCipher wraps the [bindings.DMDbCipher] object so its methods
-// can be wrapped to be Javascript compatible.
-type DMDbCipher struct {
-	api *bindings.DMDbCipher
-}
-
-// newDMDbCipherJS creates a new Javascript compatible object
-// (map[string]any) that matches the [DMDbCipher] structure.
-func newDMDbCipherJS(api *bindings.DMDbCipher) map[string]any {
-	c := DMDbCipher{api}
-	channelDbCipherMap := map[string]any{
-		"GetID":         js.FuncOf(c.GetID),
-		"Encrypt":       js.FuncOf(c.Encrypt),
-		"Decrypt":       js.FuncOf(c.Decrypt),
-		"MarshalJSON":   js.FuncOf(c.MarshalJSON),
-		"UnmarshalJSON": js.FuncOf(c.UnmarshalJSON),
-	}
-
-	return channelDbCipherMap
-}
-
-// NewDMsDatabaseCipher constructs a [DMDbCipher] object.
-//
-// Parameters:
-//   - args[0] - The tracked [Cmix] object ID (int).
-//   - args[1] - The password for storage. This should be the same password
-//     passed into [NewCmix] (Uint8Array).
-//   - args[2] - The maximum size of a payload to be encrypted. A payload passed
-//     into [DMDbCipher.Encrypt] that is larger than this value will result
-//     in an error (int).
-//
-// Returns:
-//   - JavaScript representation of the [DMDbCipher] object.
-//   - Throws a TypeError if creating the cipher fails.
-func NewDMsDatabaseCipher(_ js.Value, args []js.Value) any {
-	cmixId := args[0].Int()
-	password := utils.CopyBytesToGo(args[1])
-	plaintTextBlockSize := args[2].Int()
-
-	cipher, err := bindings.NewDMsDatabaseCipher(
-		cmixId, password, plaintTextBlockSize)
-	if err != nil {
-		utils.Throw(utils.TypeError, err)
-		return nil
-	}
-
-	return newDMDbCipherJS(cipher)
-}
-
-// GetID returns the ID for this [bindings.DMDbCipher] in the
-// channelDbCipherTracker.
-//
-// Returns:
-//   - Tracker ID (int).
-func (c *DMDbCipher) GetID(js.Value, []js.Value) any {
-	return c.api.GetID()
-}
-
-// Encrypt will encrypt the raw data. It will return a ciphertext. Padding is
-// done on the plaintext so all encrypted data looks uniform at rest.
-//
-// Parameters:
-//   - args[0] - The data to be encrypted (Uint8Array). This must be smaller
-//     than the block size passed into [NewDMsDatabaseCipher]. If it is
-//     larger, this will return an error.
-//
-// Returns:
-//   - The ciphertext of the plaintext passed in (Uint8Array).
-//   - Throws a TypeError 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)
-		return nil
-	}
-
-	return utils.CopyBytesToJS(ciphertext)
-}
-
-// Decrypt will decrypt the passed in encrypted value. The plaintext will be
-// returned by this function. Any padding will be discarded within this
-// function.
-//
-// Parameters:
-//   - args[0] - the encrypted data returned by [DMDbCipher.Encrypt]
-//     (Uint8Array).
-//
-// Returns:
-//   - The plaintext of the ciphertext passed in (Uint8Array).
-//   - Throws a TypeError 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)
-		return nil
-	}
-
-	return utils.CopyBytesToJS(plaintext)
-}
-
-// MarshalJSON marshals the cipher into valid JSON.
-//
-// Returns:
-//   - JSON of the cipher (Uint8Array).
-//   - Throws a TypeError 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)
-		return nil
-	}
-
-	return utils.CopyBytesToJS(data)
-}
-
-// UnmarshalJSON unmarshalls JSON into the cipher. This function adheres to the
-// json.Unmarshaler interface.
-//
-// Note that this function does not transfer the internal RNG. Use
-// [channel.NewCipherFromJSON] to properly reconstruct a cipher from JSON.
-//
-// Parameters:
-//   - args[0] - JSON data to unmarshal (Uint8Array).
-//
-// Returns:
-//   - JSON of the cipher (Uint8Array).
-//   - Throws a TypeError 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)
-		return nil
-	}
-	return nil
-}
-
 // truncate truncates the string to length n. If the string is trimmed, then
 // ellipses (...) are appended.
 func truncate(s string, n int) string {
diff --git a/wasm/dm_test.go b/wasm/dm_test.go
index 58f837ea0bb857deaede29fd9f5c37f4cd936e34..f959c9463fe793d7cac78927dcdc8eba5980d5f7 100644
--- a/wasm/dm_test.go
+++ b/wasm/dm_test.go
@@ -62,43 +62,3 @@ func Test_DMClientMethods(t *testing.T) {
 		}
 	}
 }
-
-// Tests that the map representing DMDbCipher returned by newDMDbCipherJS
-// contains all of the methods on DMDbCipher.
-func Test_newDMDbCipherJS(t *testing.T) {
-	cipherType := reflect.TypeOf(&DMDbCipher{})
-
-	cipher := newDMDbCipherJS(&bindings.DMDbCipher{})
-	if len(cipher) != cipherType.NumMethod() {
-		t.Errorf("DMDbCipher JS object does not have all methods."+
-			"\nexpected: %d\nreceived: %d", cipherType.NumMethod(), len(cipher))
-	}
-
-	for i := 0; i < cipherType.NumMethod(); i++ {
-		method := cipherType.Method(i)
-
-		if _, exists := cipher[method.Name]; !exists {
-			t.Errorf("Method %s does not exist.", method.Name)
-		}
-	}
-}
-
-// Tests that DMDbCipher has all the methods that [bindings.DMDbCipher] has.
-func Test_DMDbCipherMethods(t *testing.T) {
-	cipherType := reflect.TypeOf(&DMDbCipher{})
-	binCipherType := reflect.TypeOf(&bindings.DMDbCipher{})
-
-	if binCipherType.NumMethod() != cipherType.NumMethod() {
-		t.Errorf("WASM DMDbCipher object does not have all methods from "+
-			"bindings.\nexpected: %d\nreceived: %d",
-			binCipherType.NumMethod(), cipherType.NumMethod())
-	}
-
-	for i := 0; i < binCipherType.NumMethod(); i++ {
-		method := binCipherType.Method(i)
-
-		if _, exists := cipherType.MethodByName(method.Name); !exists {
-			t.Errorf("Method %s does not exist.", method.Name)
-		}
-	}
-}
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 dbff9722411ddac19999b28b137739ec31279a45..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
 	}
 
@@ -165,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
 	}
 
@@ -186,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
 	}
 
@@ -205,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
 	}
 
@@ -225,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
 	}
 
@@ -271,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
 	}
 
@@ -377,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
@@ -389,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/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 59c4485c516894972b0cbcfa74b49218f28299f8..3185814f71c14827254fe5cb8f852659c57ed471 100644
--- a/wasm_test.go
+++ b/wasm_test.go
@@ -39,10 +39,9 @@ func TestPublicFunctions(t *testing.T) {
 
 		// These functions are used internally by the WASM bindings but are not
 		// exposed
-		"NewEventModel":                   {},
 		"NewChannelsManagerGoEventModel":  {},
 		"LoadChannelsManagerGoEventModel": {},
-		"GetChannelDbCipherTrackerFromID": {},
+		"GetDbCipherTrackerFromID":        {},
 
 		// Version functions were renamed to differentiate between WASM and
 		// client versions
@@ -50,8 +49,7 @@ func TestPublicFunctions(t *testing.T) {
 		"GetDependencies": {},
 
 		// DM Functions these are used but not exported by
-		// WASM bindins, so are not exposed.
-		"NewDMReceiver":               {},
+		// WASM bindings, so are not exposed.
 		"NewDMClientWithGoEventModel": {},
 		"GetDMDbCipherTrackerFromID":  {},
 
@@ -66,6 +64,9 @@ func TestPublicFunctions(t *testing.T) {
 
 		// 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..7d5a4c3b5e3ceb5e1784dc5b119d4ea82213ad15 100644
--- a/worker/README.md
+++ b/worker/README.md
@@ -18,12 +18,12 @@ package main
 
 import (
 	"fmt"
-	"gitlab.com/elixxir/xxdk-wasm/utils/worker"
+	"gitlab.com/elixxir/xxdk-wasm/worker"
 )
 
 func main() {
 	fmt.Println("Starting WebAssembly Worker.")
-	tm := worker.NewThreadManager("exampleWebWorker")
+	tm := worker.NewThreadManager("exampleWebWorker", true)
 	tm.SignalReady()
 	<-make(chan bool)
 }
@@ -47,8 +47,8 @@ To start the worker, call `worker.NewManager` with the Javascript file to launch
 the worker.
 
 ```go
-wm, err := worker.NewManager("workerWasm.js", "exampleWebWorker")
+m, err := worker.NewManager("workerWasm.js", "exampleWebWorker", true)
 if err != nil {
-	return nil, err
+return nil, err
 }
 ```
\ No newline at end of file
diff --git a/worker/manager.go b/worker/manager.go
index 3a5831db7c07a54927429b8d818dd66db7b9fc7a..ca371a576e67f24e648c70d43b7fe1bffc6ad8c2 100644
--- a/worker/manager.go
+++ b/worker/manager.go
@@ -10,15 +10,11 @@
 package worker
 
 import (
-	"encoding/json"
-	"sync"
 	"syscall/js"
 	"time"
 
+	"github.com/hack-pad/safejs"
 	"github.com/pkg/errors"
-	jww "github.com/spf13/jwalterweatherman"
-
-	"gitlab.com/elixxir/xxdk-wasm/utils"
 )
 
 // initID is the ID for the first item in the callback list. If the list only
@@ -31,81 +27,43 @@ const (
 	// workerInitialConnectionTimeout is the time to wait to receive initial
 	// contact from a new worker before timing out.
 	workerInitialConnectionTimeout = 90 * time.Second
-
-	// ResponseTimeout is the general time to wait after sending a message to
-	// receive a response before timing out.
-	ResponseTimeout = 30 * time.Second
 )
 
-// receiveQueueChanSize is the size of the channel that received messages are
-// put on.
-const receiveQueueChanSize = 100
-
-// 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.
 type Manager struct {
-	// worker is the Worker Javascript object.
-	// Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker
-	worker js.Value
-
-	// callbacks are a list of ReceptionCallback that handle a specific message
-	// received from the worker. Each callback is keyed on a tag specifying how
-	// the received message should be handled. If the message is a reply to a
-	// message sent to the worker, then the callback is also keyed on a unique
-	// ID. If the message is not a reply, then it appears on initID.
-	callbacks map[Tag]map[uint64]ReceptionCallback
-
-	// responseIDs is a list of the newest ID to assign to each callback when
-	// registered. The IDs are used to connect a reply from the worker to the
-	// original message sent by the main thread.
-	responseIDs map[Tag]uint64
-
-	// receiveQueue is the channel that all received messages 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{}
-
-	// name describes the worker. It is used for debugging and logging purposes.
-	name string
+	mm *MessageManager
 
-	// messageLogging determines if debug message logs should be printed every
-	// time a message is sent/received to/from the worker.
-	messageLogging bool
-
-	mux sync.Mutex
+	// Wrapper of the Worker Javascript object.
+	// Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker
+	w Worker
 }
 
 // NewManager generates a new Manager. This functions will only return once
 // communication with the worker has been established.
 func NewManager(aURL, name string, messageLogging bool) (*Manager, error) {
-	// Create new worker options with the given name
-	opts := newWorkerOptions("", "", name)
-
-	m := &Manager{
-		worker:         js.Global().Get("Worker").New(aURL, opts),
-		callbacks:      make(map[Tag]map[uint64]ReceptionCallback),
-		responseIDs:    make(map[Tag]uint64),
-		receiveQueue:   make(chan js.Value, receiveQueueChanSize),
-		quit:           make(chan struct{}),
-		name:           name,
-		messageLogging: messageLogging,
+	w, err := NewWorker(aURL, newWorkerOptions("", "", name))
+	if err != nil {
+		return nil, errors.Wrapf(err, "failed to construct Worker")
 	}
 
-	// Start thread to process responses from worker
-	go m.processThread()
+	p := DefaultParams()
+	p.MessageLogging = messageLogging
+	mm, err := NewMessageManager(w.Value, name+"-main", p)
+	if err != nil {
+		return nil, errors.Wrapf(err, "failed to construct message manager")
+	}
 
-	// Register listeners on the Javascript worker object that receive messages
-	// and errors from the worker
-	m.addEventListeners()
+	m := &Manager{
+		mm: mm,
+		w:  w,
+	}
 
 	// Register a callback that will receive initial message from worker
 	// indicating that it is ready
 	ready := make(chan struct{})
-	m.RegisterCallback(readyTag, func([]byte) { ready <- struct{}{} })
+	mm.RegisterCallback(readyTag, func([]byte, func([]byte)) {
+		ready <- struct{}{}
+	})
 
 	// Wait for the ready signal from the worker
 	select {
@@ -113,263 +71,124 @@ func NewManager(aURL, name string, messageLogging bool) (*Manager, error) {
 	case <-time.After(workerInitialConnectionTimeout):
 		return nil, errors.Errorf("[WW] [%s] timed out after %s waiting for "+
 			"initial message from worker",
-			m.name, workerInitialConnectionTimeout)
+			mm.name, workerInitialConnectionTimeout)
 	}
 
 	return m, nil
 }
 
-// Stop closes the worker manager and terminates the worker.
-func (m *Manager) Stop() {
-	// Stop processThread
-	select {
-	case m.quit <- struct{}{}:
-	}
-
-	// Terminate the worker
-	go m.terminate()
-}
-
-// processThread processes received messages sequentially.
-func (m *Manager) processThread() {
-	jww.INFO.Printf("[WW] [%s] Starting process thread.", m.name)
-	for {
-		select {
-		case <-m.quit:
-			jww.INFO.Printf("[WW] [%s] Quitting process thread.", m.name)
-			return
-		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))
-			}
-		}
-	}
-}
-
-// SendMessage sends a message to the worker with the given tag. If a reception
-// callback is specified, then the message is given a unique ID to handle the
-// reply. Set receptionCB to nil if no reply is expected.
-func (m *Manager) SendMessage(
-	tag Tag, data []byte, receptionCB ReceptionCallback) {
-	var id uint64
-	if receptionCB != nil {
-		id = m.registerReplyCallback(tag, receptionCB)
-	}
-
-	if m.messageLogging {
-		jww.DEBUG.Printf("[WW] [%s] Main sending message for %q and ID %d "+
-			"with data: %s", m.name, tag, id, data)
-	}
+// NewManagerFromScript generates a new Manager. This functions will only return
+// once communication with the worker has been established.
+// TODO: test or remove
+func NewManagerFromScript(
+	jsScript, name string, messageLogging bool) (*Manager, error) {
 
-	msg := Message{
-		Tag:  tag,
-		ID:   id,
-		Data: data,
-	}
-	payload, err := json.Marshal(msg)
+	blob, err := jsBlob.New([]any{jsScript}, map[string]any{
+		"type": "text/javascript",
+	})
 	if err != nil {
-		jww.FATAL.Panicf("[WW] [%s] Main failed to marshal %T for %q and "+
-			"ID %d going to worker: %+v", m.name, msg, tag, id, err)
+		return nil, err
 	}
-
-	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 js.Value) {
-	m.receiveQueue <- data
-}
-
-// processReceivedMessage processes the message received from the worker and
-// calls the associated callback. This functions blocks until the callback
-// returns.
-func (m *Manager) processReceivedMessage(data []byte) error {
-	var msg Message
-	err := json.Unmarshal(data, &msg)
+	objectURL, err := jsURL.Call("createObjectURL", blob)
 	if err != nil {
-		return err
-	}
-
-	if m.messageLogging {
-		jww.DEBUG.Printf("[WW] [%s] Main received message for %q and ID %d "+
-			"with data: %s", m.name, msg.Tag, msg.ID, msg.Data)
+		return nil, err
 	}
-
-	callback, err := m.getCallback(msg.Tag, msg.ID, msg.DeleteCB)
+	objectURLStr, err := objectURL.String()
 	if err != nil {
-		return err
+		return nil, err
 	}
 
-	callback(msg.Data)
-
-	return nil
+	return NewManager(objectURLStr, name, messageLogging)
 }
 
-// getCallback returns the callback for the given ID or returns an error if no
-// callback is found. The callback is deleted from the map if specified in the
-// message. This function is thread safe.
-func (m *Manager) getCallback(
-	tag Tag, id uint64, deleteCB bool) (ReceptionCallback, error) {
-	m.mux.Lock()
-	defer m.mux.Unlock()
-	callbacks, exists := m.callbacks[tag]
-	if !exists {
-		return nil, errors.Errorf("no callbacks found for tag %q", tag)
-	}
-
-	callback, exists := callbacks[id]
-	if !exists {
-		return nil, errors.Errorf("no %q callback found for ID %d", tag, id)
-	}
-
-	if deleteCB {
-		delete(m.callbacks[tag], id)
-		if len(m.callbacks[tag]) == 0 {
-			delete(m.callbacks, tag)
-		}
-	}
+// Stop closes the worker manager and terminates the worker.
+func (m *Manager) Stop() error {
+	m.mm.Stop()
 
-	return callback, nil
+	// Terminate the worker
+	err := m.w.Terminate()
+	return errors.Wrapf(err, "failed to terminate worker %q", m.mm.name)
 }
 
-// RegisterCallback registers the reception callback for the given tag. If a
-// previous callback was registered, it is overwritten. This function is thread
-// safe.
-func (m *Manager) RegisterCallback(tag Tag, receptionCB ReceptionCallback) {
-	m.mux.Lock()
-	defer m.mux.Unlock()
-
-	id := initID
-
-	jww.DEBUG.Printf("[WW] [%s] Main registering callback for tag %q and ID %d",
-		m.name, tag, id)
-
-	m.callbacks[tag] = map[uint64]ReceptionCallback{id: receptionCB}
+// SendMessage sends a message to the worker with the given tag and waits for a
+// response. An error is returned on failure to send or on timeout.
+func (m *Manager) SendMessage(tag Tag, data []byte) (response []byte, err error) {
+	return m.mm.Send(tag, data)
 }
 
-// RegisterCallback registers the reception callback for the given tag and a new
-// unique ID used to associate the reply to the callback. Returns the ID that
-// was registered. If a previous callback was registered, it is overwritten.
-// This function is thread safe.
-func (m *Manager) registerReplyCallback(
-	tag Tag, receptionCB ReceptionCallback) uint64 {
-	m.mux.Lock()
-	defer m.mux.Unlock()
-	id := m.getNextID(tag)
-
-	jww.DEBUG.Printf("[WW] [%s] Main registering callback for tag %q and ID %d",
-		m.name, tag, id)
-
-	if _, exists := m.callbacks[tag]; !exists {
-		m.callbacks[tag] = make(map[uint64]ReceptionCallback)
-	}
-	m.callbacks[tag][id] = receptionCB
-
-	return id
+// SendTimeout sends a message to the worker with the given tag and waits for a
+// response. An error is returned on failure to send or on the specified
+// timeout.
+func (m *Manager) SendTimeout(
+	tag Tag, data []byte, timeout time.Duration) (response []byte, err error) {
+	return m.mm.SendTimeout(tag, data, timeout)
 }
 
-// getNextID returns the next unique ID for the given tag. This function is not
-// thread-safe.
-func (m *Manager) getNextID(tag Tag) uint64 {
-	if _, exists := m.responseIDs[tag]; !exists {
-		m.responseIDs[tag] = initID
-	}
+// SendNoResponse sends a message to the worker with the given tag. It returns
+// immediately and does not wait for a response.
+func (m *Manager) SendNoResponse(tag Tag, data []byte) error {
+	return m.mm.SendNoResponse(tag, data)
+}
 
-	id := m.responseIDs[tag]
-	m.responseIDs[tag]++
-	return id
+// RegisterCallback registers the callback for the given tag. Previous tags are
+// overwritten. This function is thread safe.
+func (m *Manager) RegisterCallback(tag Tag, receiverCB ReceiverCallback) {
+	m.mm.RegisterCallback(tag, receiverCB)
 }
 
-// GetWorker returns the web worker object. This returned so the worker object
-// can be returned to the Javascript layer for it to communicate with the worker
-// thread.
-func (m *Manager) GetWorker() js.Value { return m.worker }
+// GetWorker returns the Worker wrapper for the Worker Javascript object. This
+// is returned so the worker object can be returned to the Javascript layer for
+// it to communicate with the worker thread.
+func (m *Manager) GetWorker() js.Value { return safejs.Unsafe(m.w.Value) }
 
 // Name returns the name of the web worker object.
-func (m *Manager) Name() string { return m.name }
+func (m *Manager) Name() string { return m.mm.name }
 
 ////////////////////////////////////////////////////////////////////////////////
-// Javascript Call Wrappers                                                   //
+// Worker Wrapper                                                             //
 ////////////////////////////////////////////////////////////////////////////////
 
-// addEventListeners adds the event listeners needed to receive a message from
-// the worker. Two listeners were added; one to receive messages from the worker
-// and the other to receive errors.
-func (m *Manager) addEventListeners() {
-	// Create a listener for when the message event is fired on the worker. This
-	// 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(args[0].Get("data"))
-		return nil
-	})
-
-	// Create listener for when an error event is fired on the worker. This
-	// occurs when an error occurs in the worker.
-	// 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.FATAL.Panicf("[WW] [%s] Main received error event: %+v",
-			m.name, js.Error{Value: event})
-		return nil
-	})
-
-	// Create listener for when a messageerror event is fired on the worker.
-	// This occurs when it receives a message that cannot be deserialized.
-	// 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: %+v",
-			m.name, js.Error{Value: event})
-		return nil
-	})
-
-	// Register each event listener on the worker using addEventListener
-	// Doc: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
-	m.worker.Call("addEventListener", "message", messageEvent)
-	m.worker.Call("addEventListener", "error", errorEvent)
-	m.worker.Call("addEventListener", "messageerror", messageerrorEvent)
+// Worker wraps a Javascript Worker object.
+//
+// Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker
+type Worker struct {
+	MessagePort
 }
 
-// postMessage sends a message to the worker.
-//
-// 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.
+var (
+	jsWorker         = safejs.MustGetGlobal("Worker")
+	jsMessageChannel = safejs.MustGetGlobal("MessageChannel")
+	jsURL            = safejs.MustGetGlobal("URL")
+	jsBlob           = safejs.MustGetGlobal("Blob")
+)
+
+// NewWorker creates a Javascript Worker object that executes the script at the
+// specified URL.
 //
-// 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.
+// It returns any thrown exceptions as errors.
 //
-// Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker/postMessage
-func (m *Manager) postMessage(msg []byte) {
-	buffer := utils.CopyBytesToJS(msg)
-	m.worker.Call("postMessage", buffer, []any{buffer.Get("buffer")})
+// Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker/Worker
+func NewWorker(aURL string, options map[string]any) (w Worker, err error) {
+	v, err := jsWorker.New(aURL, options)
+	if err != nil {
+		return Worker{}, err
+	}
+
+	mp, err := NewMessagePort(v)
+	if err != nil {
+		return Worker{}, err
+	}
+
+	return Worker{MessagePort: mp}, nil
 }
 
-// terminate immediately terminates the Worker. This does not offer the worker
+// Terminate immediately terminates the Worker. This does not offer the worker
 // an opportunity to finish its operations; it is stopped at once.
 //
 // Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker/terminate
-func (m *Manager) terminate() {
-	m.worker.Call("terminate")
+func (w Worker) Terminate() error {
+	_, err := w.Call("terminate")
+	return err
 }
 
 // newWorkerOptions creates a new Javascript object containing optional
@@ -377,8 +196,8 @@ func (m *Manager) terminate() {
 //
 // Each property is optional; leave a property empty to use the defaults (as
 // documented). The available properties are:
-//   - type - The type of worker to create. The value can be either "classic" or
-//     "module". If not specified, the default used is "classic".
+//   - workerType - The type of worker to create. The value can be either
+//     "classic" or "module". If not specified, the default used is "classic".
 //   - credentials - The type of credentials to use for the worker. The value
 //     can be "omit", "same-origin", or "include". If it is not specified, or if
 //     the type is "classic", then the default used is "omit" (no credentials
@@ -387,7 +206,7 @@ func (m *Manager) terminate() {
 //     purposes.
 //
 // Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker/Worker#options
-func newWorkerOptions(workerType, credentials, name string) js.Value {
+func newWorkerOptions(workerType, credentials, name string) map[string]any {
 	options := make(map[string]any, 3)
 	if workerType != "" {
 		options["type"] = workerType
@@ -398,5 +217,5 @@ func newWorkerOptions(workerType, credentials, name string) js.Value {
 	if name != "" {
 		options["name"] = name
 	}
-	return js.ValueOf(options)
+	return options
 }
diff --git a/worker/manager_test.go b/worker/manager_test.go
index 49a395a9c45a33f25892cc1efc86e263ccf06063..67f5320d87fb977942a922f15e6e6ff4147fd09e 100644
--- a/worker/manager_test.go
+++ b/worker/manager_test.go
@@ -10,201 +10,39 @@
 package worker
 
 import (
-	"encoding/json"
-	"reflect"
+	"syscall/js"
 	"testing"
-	"time"
 )
 
-// Tests Manager.processReceivedMessage calls the expected callback.
-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{})
-	cb := func([]byte) { cbChan <- struct{}{} }
-	m.callbacks[msg.Tag] = map[uint64]ReceptionCallback{msg.ID: cb}
-
-	data, err := json.Marshal(msg)
-	if err != nil {
-		t.Fatalf("Failed to JSON marshal Message: %+v", err)
-	}
-
-	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)
-	}
-}
-
-// Tests Manager.getCallback returns the expected callback and deletes only the
-// given callback when deleteCB is true.
-func TestManager_getCallback(t *testing.T) {
-	m := &Manager{callbacks: make(map[Tag]map[uint64]ReceptionCallback)}
-
-	// Add new callback and check that it is returned by getCallback
-	tag, id1 := readyTag, uint64(5)
-	cb := func([]byte) {}
-	m.callbacks[tag] = map[uint64]ReceptionCallback{id1: cb}
-
-	received, err := m.getCallback(tag, id1, false)
-	if err != nil {
-		t.Errorf("getCallback error for tag %q and ID %d: %+v", tag, id1, err)
-	}
-
-	if reflect.ValueOf(cb).Pointer() != reflect.ValueOf(received).Pointer() {
-		t.Errorf("Wrong callback.\nexpected: %p\nreceived: %p", cb, received)
-	}
-
-	// Add new callback under the same tag but with deleteCB set to true and
-	// check that it is returned by getCallback and that it was deleted from the
-	// map while id1 was not
-	id2 := uint64(56)
-	cb = func([]byte) {}
-	m.callbacks[tag][id2] = cb
-
-	received, err = m.getCallback(tag, id2, true)
-	if err != nil {
-		t.Errorf("getCallback error for tag %q and ID %d: %+v", tag, id2, err)
-	}
-
-	if reflect.ValueOf(cb).Pointer() != reflect.ValueOf(received).Pointer() {
-		t.Errorf("Wrong callback.\nexpected: %p\nreceived: %p", cb, received)
-	}
-
-	received, err = m.getCallback(tag, id1, false)
-	if err != nil {
-		t.Errorf("getCallback error for tag %q and ID %d: %+v", tag, id1, err)
-	}
-
-	received, err = m.getCallback(tag, id2, true)
-	if err == nil {
-		t.Errorf("getCallback did not get error when trying to get deleted "+
-			"callback for tag %q and ID %d", tag, id2)
-	}
-}
-
-// Tests that Manager.RegisterCallback registers a callback that is then called
-// by Manager.processReceivedMessage.
-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{})
-	cb := func([]byte) { cbChan <- struct{}{} }
-	m.RegisterCallback(msg.Tag, cb)
-
-	data, err := json.Marshal(msg)
-	if err != nil {
-		t.Fatalf("Failed to JSON marshal Message: %+v", err)
-	}
-
-	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)
-	}
-}
-
-// Tests that Manager.registerReplyCallback registers a callback that is then
-// called by Manager.processReceivedMessage.
-func TestManager_registerReplyCallback(t *testing.T) {
-	m := &Manager{
-		callbacks:   make(map[Tag]map[uint64]ReceptionCallback),
-		responseIDs: make(map[Tag]uint64),
-	}
-
-	msg := Message{Tag: readyTag, ID: 5}
-	cbChan := make(chan struct{})
-	cb := func([]byte) { cbChan <- struct{}{} }
-	m.registerReplyCallback(msg.Tag, cb)
-	m.callbacks[msg.Tag] = map[uint64]ReceptionCallback{msg.ID: cb}
-
-	data, err := json.Marshal(msg)
-	if err != nil {
-		t.Fatalf("Failed to JSON marshal Message: %+v", err)
-	}
-
-	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)
-	}
-}
-
-// Tests that Manager.getNextID returns the expected ID for various Tags.
-func TestManager_getNextID(t *testing.T) {
-	m := &Manager{
-		callbacks:   make(map[Tag]map[uint64]ReceptionCallback),
-		responseIDs: make(map[Tag]uint64),
-	}
-
-	for _, tag := range []Tag{readyTag, "test", "A", "B", "C"} {
-		id := m.getNextID(tag)
-		if id != initID {
-			t.Errorf("ID for new tag %q is not initID."+
-				"\nexpected: %d\nreceived: %d", tag, initID, id)
-		}
-
-		for j := uint64(1); j < 100; j++ {
-			id = m.getNextID(tag)
-			if id != j {
-				t.Errorf("Unexpected ID for tag %q."+
-					"\nexpected: %d\nreceived: %d", tag, j, id)
-			}
-		}
-	}
-}
-
-////////////////////////////////////////////////////////////////////////////////
-// Javascript Call Wrappers                                                   //
-////////////////////////////////////////////////////////////////////////////////
-
 // Tests that newWorkerOptions returns a Javascript object with the expected
 // type, credentials, and name fields.
 func Test_newWorkerOptions(t *testing.T) {
-	for i, workerType := range []string{"classic", "module"} {
-		for j, credentials := range []string{"omit", "same-origin", "include"} {
-			for k, name := range []string{"name1", "name2", "name3"} {
+	for _, workerType := range []string{"classic", "module"} {
+		for _, credentials := range []string{"omit", "same-origin", "include"} {
+			for _, name := range []string{"name1", "name2", "name3"} {
 				opts := newWorkerOptions(workerType, credentials, name)
 
-				v := opts.Get("type").String()
-				if v != workerType {
-					t.Errorf("Unexpected type (%d, %d, %d)."+
-						"\nexpected: %s\nreceived: %s", i, j, k, workerType, v)
+				optsJS := js.ValueOf(opts)
+
+				typeJS := optsJS.Get("type").String()
+				if typeJS != workerType {
+					t.Errorf("Unexected type (type:%s credentials:%s name:%s)"+
+						"\nexpected: %s\nreceived: %s",
+						workerType, credentials, name, workerType, typeJS)
 				}
 
-				v = opts.Get("credentials").String()
-				if v != credentials {
-					t.Errorf("Unexpected credentials (%d, %d, %d)."+
-						"\nexpected: %s\nreceived: %s", i, j, k, credentials, v)
+				credentialsJS := optsJS.Get("credentials").String()
+				if typeJS != workerType {
+					t.Errorf("Unexected credentials (type:%s credentials:%s "+
+						"name:%s)\nexpected: %s\nreceived: %s", workerType,
+						credentials, name, credentials, credentialsJS)
 				}
 
-				v = opts.Get("name").String()
-				if v != name {
-					t.Errorf("Unexpected name (%d, %d, %d)."+
-						"\nexpected: %s\nreceived: %s", i, j, k, name, v)
+				nameJS := optsJS.Get("name").String()
+				if typeJS != workerType {
+					t.Errorf("Unexected name (type:%s credentials:%s name:%s)"+
+						"\nexpected: %s\nreceived: %s",
+						workerType, credentials, name, name, nameJS)
 				}
 			}
 		}
diff --git a/worker/message.go b/worker/message.go
index 3d3bc23f131ef96cdc97a3e8684d1386b07cbeb2..c9de4c1d65b3674eacde808f8d776eb2085c6dbb 100644
--- a/worker/message.go
+++ b/worker/message.go
@@ -14,6 +14,6 @@ package worker
 type Message struct {
 	Tag      Tag    `json:"tag"`
 	ID       uint64 `json:"id"`
-	DeleteCB bool   `json:"deleteCB"`
+	Response bool   `json:"response"`
 	Data     []byte `json:"data"`
 }
diff --git a/worker/messageChannel.go b/worker/messageChannel.go
new file mode 100644
index 0000000000000000000000000000000000000000..75de2938e38ff9bbb3ff4f0a3f3090fa91027f01
--- /dev/null
+++ b/worker/messageChannel.go
@@ -0,0 +1,103 @@
+////////////////////////////////////////////////////////////////////////////////
+// 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 worker
+
+import (
+	"github.com/hack-pad/safejs"
+	"github.com/pkg/errors"
+
+	"gitlab.com/elixxir/wasm-utils/utils"
+)
+
+// MessageChannel wraps a Javascript MessageChannel object.
+//
+// Doc: https://developer.mozilla.org/en-US/docs/Web/API/MessageChannel
+type MessageChannel struct {
+	safejs.Value
+}
+
+// NewMessageChannel returns a new MessageChannel object with two new
+// MessagePort objects.
+//
+// Doc: https://developer.mozilla.org/en-US/docs/Web/API/MessageChannel/MessageChannel
+func NewMessageChannel() (MessageChannel, error) {
+	v, err := jsMessageChannel.New()
+	if err != nil {
+		return MessageChannel{}, err
+	}
+	return MessageChannel{v}, nil
+}
+
+// CreateMessageChannel creates a new Javascript MessageChannel between two
+// workers. The [Channel] tag will be used as the prefix in the name of the
+// MessageChannel when printing to logs. The key is used to look up the callback
+// registered on the worker to handle the MessageChannel creation.
+//
+// Doc: https://developer.mozilla.org/en-US/docs/Web/API/MessageChannel
+func CreateMessageChannel(w1, w2 *Manager, channelName, key string) error {
+	// Create a Javascript MessageChannel
+	mc, err := NewMessageChannel()
+	if err != nil {
+		return err
+	}
+	channelNameJS := utils.CopyBytesToJS([]byte(channelName))
+	keyJS := utils.CopyBytesToJS([]byte(key))
+
+	port1, err := mc.Port1()
+	if err != nil {
+		return errors.Wrap(err, "could not get port1")
+	}
+
+	port2, err := mc.Port2()
+	if err != nil {
+		return errors.Wrap(err, "could not get port2")
+	}
+
+	obj1 := map[string]any{
+		"port": port1.Value, "channel": channelNameJS, "key": keyJS}
+	err = w1.w.PostMessageTransfer(obj1, port1.Value)
+	if err != nil {
+		return errors.Wrap(err, "failed to send port1")
+	}
+
+	obj2 := map[string]any{
+		"port": port2.Value, "channel": channelNameJS, "key": keyJS}
+	err = w2.w.PostMessageTransfer(obj2, port2.Value)
+	if err != nil {
+		return errors.Wrap(err, "failed to send port2")
+	}
+
+	return nil
+}
+
+// Port1 returns the first port of the message channel — the port attached to
+// the context that originated the channel.
+//
+// Doc: https://developer.mozilla.org/en-US/docs/Web/API/MessageChannel/port1
+func (mc MessageChannel) Port1() (MessagePort, error) {
+	v, err := mc.Get("port1")
+	if err != nil {
+		return MessagePort{}, err
+	}
+	return NewMessagePort(v)
+}
+
+// Port2 returns the second port of the message channel — the port attached to
+// the context at the other end of the channel, which the message is initially
+// sent to.
+//
+// Doc: https://developer.mozilla.org/en-US/docs/Web/API/MessageChannel/port2
+func (mc MessageChannel) Port2() (MessagePort, error) {
+	v, err := mc.Get("port2")
+	if err != nil {
+		return MessagePort{}, err
+	}
+	return NewMessagePort(v)
+}
diff --git a/worker/messageEvent.go b/worker/messageEvent.go
new file mode 100644
index 0000000000000000000000000000000000000000..cf924a2848ddfc5f47554f1e280a1f3655b8ec98
--- /dev/null
+++ b/worker/messageEvent.go
@@ -0,0 +1,50 @@
+////////////////////////////////////////////////////////////////////////////////
+// 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 worker
+
+import (
+	"github.com/hack-pad/safejs"
+	"github.com/pkg/errors"
+)
+
+// MessageEvent is received from the channel returned by Listen().
+// Represents a JS MessageEvent.
+type MessageEvent struct {
+	data   safejs.Value
+	err    error
+	target MessagePort
+}
+
+// Data returns this event's data or a parse error
+func (e MessageEvent) Data() (safejs.Value, error) {
+	return e.data, errors.Wrapf(e.err, "failed to parse MessageEvent %+v", e.data)
+}
+
+func parseMessageEvent(v safejs.Value) MessageEvent {
+	value, err := v.Get("target")
+	if err != nil {
+		return MessageEvent{err: err}
+	}
+
+	target, err := NewMessagePort(value)
+	if err != nil {
+		return MessageEvent{err: err}
+	}
+
+	data, err := v.Get("data")
+	if err != nil {
+		return MessageEvent{err: err}
+	}
+
+	return MessageEvent{
+		data:   data,
+		target: target,
+	}
+}
diff --git a/worker/messageManager.go b/worker/messageManager.go
new file mode 100644
index 0000000000000000000000000000000000000000..e70369c3829e886a4d585d6a3eabf430c2152d2c
--- /dev/null
+++ b/worker/messageManager.go
@@ -0,0 +1,419 @@
+////////////////////////////////////////////////////////////////////////////////
+// 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 worker
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"sync"
+	"syscall/js"
+	"time"
+
+	"github.com/aquilax/truncate"
+	"github.com/hack-pad/safejs"
+	"github.com/pkg/errors"
+	jww "github.com/spf13/jwalterweatherman"
+
+	"gitlab.com/elixxir/wasm-utils/exception"
+	"gitlab.com/elixxir/wasm-utils/utils"
+)
+
+// SenderCallback is called when the sender of a message gets a response. The
+// message is the response from the receiver.
+type SenderCallback func(message []byte)
+
+// ReceiverCallback is called when receiving a message from the sender. Reply
+// can optionally be used to send a response to the caller, triggering the
+// [SenderCallback].
+type ReceiverCallback func(message []byte, reply func(message []byte))
+
+// NewPortCallback is called with a MessagePort Javascript object when received.
+type NewPortCallback func(port js.Value, channelName string)
+
+// MessageManager manages the sending and receiving of messages to a remote
+// browser context (e.g., Worker and MessagePort)
+type MessageManager struct {
+	// The underlying Javascript object that sends and receives messages.
+	p MessagePort
+
+	// senderCallbacks are a list of SenderCallback that are called when
+	// receiving a response. The uint64 is a unique ID that connects each
+	// received reply to its original message.
+	senderCallbacks map[Tag]map[uint64]SenderCallback
+
+	// receiverCallbacks are a list of ReceiverCallback that are called when
+	// receiving a message.
+	receiverCallbacks map[Tag]ReceiverCallback
+
+	// responseIDs is a list of the newest ID to assign to each senderCallbacks
+	// when registered. The IDs are used to connect a reply to the original
+	// message.
+	responseIDs map[Tag]uint64
+
+	// messageChannelCB is a list of callbacks that are called when a new
+	// message channel is received.
+	messageChannelCB map[string]NewPortCallback
+
+	// quit, when triggered, stops the thread that processes received messages.
+	quit chan struct{}
+
+	// name names the underlying Javascript object. It is used for debugging and
+	// logging purposes.
+	name string
+
+	Params
+
+	mux sync.Mutex
+}
+
+// NewMessageManager generates a new MessageManager. This functions will only
+// return once communication with the remote thread has been established.
+// TODO: test
+func NewMessageManager(
+	v safejs.Value, name string, p Params) (*MessageManager, error) {
+	mm := initMessageManager(name, p)
+	mp, err := NewMessagePort(v)
+	if err != nil {
+		return nil, errors.Wrap(err, "invalid MessagePort value")
+	}
+	mm.p = mp
+
+	ctx, cancel := context.WithCancel(context.Background())
+	events, err := mm.p.Listen(ctx)
+	if err != nil {
+		cancel()
+		return nil, err
+	}
+
+	// Start thread to process responses
+	go mm.messageReception(events, cancel)
+
+	return mm, nil
+}
+
+// initMessageManager initialises a new empty MessageManager.
+func initMessageManager(name string, p Params) *MessageManager {
+	return &MessageManager{
+		senderCallbacks:   make(map[Tag]map[uint64]SenderCallback),
+		receiverCallbacks: make(map[Tag]ReceiverCallback),
+		responseIDs:       make(map[Tag]uint64),
+		messageChannelCB:  make(map[string]NewPortCallback),
+		quit:              make(chan struct{}),
+		name:              name,
+		Params:            p,
+	}
+}
+
+// Send sends the data to the remote thread with the given tag and waits for a
+// response. Returns an error if calling postMessage throws an exception,
+// marshalling the message to send fails, or if receiving a response times out.
+//
+// It is preferable to use [Send] over [SendNoResponse] as it will report a
+// timeout when the remote thread crashes and [SendNoResponse] will not.
+// TODO: test
+func (mm *MessageManager) Send(tag Tag, data []byte) (response []byte, err error) {
+	return mm.SendTimeout(tag, data, mm.ResponseTimeout)
+}
+
+// SendTimeout sends the data to the remote thread with a custom timeout. Refer
+// to [Send] for more information.
+// TODO: test
+func (mm *MessageManager) SendTimeout(
+	tag Tag, data []byte, timeout time.Duration) (response []byte, err error) {
+	responseCh := make(chan []byte)
+	id := mm.registerSenderCallback(tag, func(msg []byte) { responseCh <- msg })
+
+	err = mm.sendMessage(tag, id, data)
+	if err != nil {
+		return nil, err
+	}
+
+	select {
+	case response = <-responseCh:
+		return response, nil
+	case <-time.After(timeout):
+		return nil,
+			errors.Errorf("timed out after %s waiting for response", timeout)
+	}
+}
+
+// SendNoResponse sends the data to the remote thread with the given tag;
+// however, unlike [Send], it returns immediately and does not wait for a
+// response. Returns an error if calling postMessage throws an exception,
+// marshalling the message to send fails, or if receiving a response times out.
+//
+// It is preferable to use [Send] over [SendNoResponse] as it will report a
+// timeout when the remote thread crashes and [SendNoResponse] will not.
+// TODO: test
+func (mm *MessageManager) SendNoResponse(tag Tag, data []byte) error {
+	return mm.sendMessage(tag, initID, data)
+}
+
+// sendMessage packages the data into a Message with the tag and ID and sends it
+// to the remote thread.
+// TODO: test
+func (mm *MessageManager) sendMessage(tag Tag, id uint64, data []byte) error {
+	if mm.MessageLogging {
+		jww.DEBUG.Printf("[WW] [%s] Sending message for %q and ID %d: %s",
+			mm.name, tag, id, truncate.Truncate(
+				fmt.Sprintf("%q", data), 64, "...", truncate.PositionMiddle))
+	}
+
+	msg := Message{
+		Tag:      tag,
+		ID:       id,
+		Response: false,
+		Data:     data,
+	}
+	payload, err := json.Marshal(msg)
+	if err != nil {
+		return err
+	}
+
+	return mm.p.PostMessageTransferBytes(payload)
+}
+
+// sendResponse sends a reply to the remote thread with the given tag and ID.
+// TODO: test
+func (mm *MessageManager) sendResponse(tag Tag, id uint64, data []byte) error {
+	if mm.MessageLogging {
+		jww.DEBUG.Printf("[WW] [%s] Sending reply for %q and ID %d: %s",
+			mm.name, tag, id, truncate.Truncate(
+				fmt.Sprintf("%q", data), 64, "...", truncate.PositionMiddle))
+	}
+
+	msg := Message{
+		Tag:      tag,
+		ID:       id,
+		Response: true,
+		Data:     data,
+	}
+
+	payload, err := json.Marshal(msg)
+	if err != nil {
+		return err
+	}
+
+	return mm.p.PostMessageTransferBytes(payload)
+}
+
+// messageReception processes received messages sequentially.
+// TODO: test
+func (mm *MessageManager) messageReception(
+	events <-chan MessageEvent, cancel context.CancelFunc) {
+	jww.INFO.Printf("[WW] [%s] Starting message reception thread.", mm.name)
+	for {
+		select {
+		case <-mm.quit:
+			cancel()
+			jww.INFO.Printf(
+				"[WW] [%s] Quitting message reception thread.", mm.name)
+			return
+		case event := <-events:
+
+			safeData, err := event.Data()
+			if err != nil {
+				exception.Throwf("Failed to process message: %+v", err)
+			}
+			data := safejs.Unsafe(safeData)
+
+			switch data.Type() {
+			case js.TypeObject:
+				if data.Get("constructor").Equal(utils.Uint8Array) {
+					err = mm.processReceivedMessage(utils.CopyBytesToGo(data))
+					if err != nil {
+						jww.ERROR.Printf("[WW] [%s] Failed to process "+
+							"received message: %+v", mm.name, err)
+					}
+					break
+				} else if port := data.Get("port"); port.Truthy() {
+					err = mm.processReceivedPort(port, data)
+					if err != nil {
+						jww.ERROR.Printf("[WW] [%s] Failed to process "+
+							"received MessagePort: %+v", mm.name, err)
+					}
+					break
+				}
+				fallthrough
+
+			default:
+				jww.ERROR.Printf("[WW] [%s] Cannot handle data of type %q: %s",
+					mm.name, data.Type(), utils.JsToJson(data))
+			}
+		}
+	}
+}
+
+// processReceivedMessage processes the received message and calls the
+// associated callback. This functions blocks until the callback returns.
+func (mm *MessageManager) processReceivedMessage(data []byte) error {
+	var msg Message
+	err := json.Unmarshal(data, &msg)
+	if err != nil {
+		return err
+	}
+
+	if mm.MessageLogging {
+		jww.DEBUG.Printf("[WW] [%s] Received message for %q and ID %d "+
+			"with data: %s", mm.name, msg.Tag, msg.ID, truncate.Truncate(
+			fmt.Sprintf("%q", data), 64, "...", truncate.PositionMiddle))
+	}
+
+	if msg.Response {
+		callback, err := mm.getSenderCallback(msg.Tag, msg.ID)
+		if err != nil {
+			return err
+		}
+
+		callback(msg.Data)
+	} else {
+		callback, err := mm.getReceiverCallback(msg.Tag)
+		if err != nil {
+			return err
+		}
+
+		callback(msg.Data, func(message []byte) {
+			if err = mm.sendResponse(msg.Tag, msg.ID, message); err != nil {
+				jww.FATAL.Panicf("[WW] [%s] Failed to send response for %q "+
+					"and ID %d: %+v", mm.name, msg.Tag, msg.ID, err)
+			}
+		})
+	}
+
+	return nil
+}
+
+// processReceivedPort processes the received Javascript MessagePort and calls
+// the associated NewPortCallback callback. This functions blocks until the
+// callback returns.
+func (mm *MessageManager) processReceivedPort(port js.Value, data js.Value) error {
+	channel := string(utils.CopyBytesToGo(data.Get("channel")))
+	key := string(utils.CopyBytesToGo(data.Get("key")))
+
+	jww.INFO.Printf("[WW] [%s] Received new MessageChannel %q for key %q.",
+		mm.name, channel, key)
+
+	mm.mux.Lock()
+	cb, exists := mm.messageChannelCB[key]
+	mm.mux.Unlock()
+
+	if !exists {
+		return errors.Errorf(
+			"Failed to find callback for channel %q and key %q.", channel, key)
+	} else {
+		cb(port, channel)
+	}
+
+	return nil
+}
+
+// RegisterCallback registers the callback for the given tag. Previous tags are
+// overwritten. This function is thread safe.
+func (mm *MessageManager) RegisterCallback(tag Tag, receiverCB ReceiverCallback) {
+	mm.mux.Lock()
+	defer mm.mux.Unlock()
+
+	jww.DEBUG.Printf("[WW] [%s] Registering receiver callback for tag %q",
+		mm.name, tag)
+
+	mm.receiverCallbacks[tag] = receiverCB
+}
+
+// getReceiverCallback returns the ReceiverCallback for the given Tag or returns
+// an error if no callback is found. This function is thread safe.
+func (mm *MessageManager) getReceiverCallback(tag Tag) (ReceiverCallback, error) {
+	mm.mux.Lock()
+	defer mm.mux.Unlock()
+
+	callback, exists := mm.receiverCallbacks[tag]
+	if !exists {
+		return nil, errors.Errorf("no receiver callbacks found for tag %q", tag)
+	}
+
+	return callback, nil
+}
+
+// registerSenderCallback registers the callback for the given tag and a new
+// unique ID used to associate the reply to the callback. Returns the ID that
+// was registered. If a previous callback was registered, it is overwritten.
+// This function is thread safe.
+func (mm *MessageManager) registerSenderCallback(
+	tag Tag, senderCB SenderCallback) uint64 {
+	mm.mux.Lock()
+	defer mm.mux.Unlock()
+	id := mm.getNextID(tag)
+
+	jww.DEBUG.Printf("[WW] [%s] Registering callback for tag %q and ID %d",
+		mm.name, tag, id)
+
+	if _, exists := mm.senderCallbacks[tag]; !exists {
+		mm.senderCallbacks[tag] = make(map[uint64]SenderCallback)
+	}
+	mm.senderCallbacks[tag][id] = senderCB
+
+	return id
+}
+
+// getSenderCallback returns the SenderCallback for the given Tag and ID or
+// returns an error if no callback is found. The callback is deleted from the
+// map once found. This function is thread safe.
+func (mm *MessageManager) getSenderCallback(
+	tag Tag, id uint64) (SenderCallback, error) {
+	mm.mux.Lock()
+	defer mm.mux.Unlock()
+	callbacks, exists := mm.senderCallbacks[tag]
+	if !exists {
+		return nil, errors.Errorf("no sender callbacks found for tag %q", tag)
+	}
+
+	callback, exists := callbacks[id]
+	if !exists {
+		return nil,
+			errors.Errorf("no %q sender callback found for ID %d", tag, id)
+	}
+
+	delete(mm.senderCallbacks[tag], id)
+	if len(mm.senderCallbacks[tag]) == 0 {
+		delete(mm.senderCallbacks, tag)
+	}
+
+	return callback, nil
+}
+
+// RegisterMessageChannelCallback registers a callback that will be called when
+// a MessagePort with the given Channel is received.
+func (mm *MessageManager) RegisterMessageChannelCallback(
+	key string, fn NewPortCallback) {
+	mm.mux.Lock()
+	defer mm.mux.Unlock()
+	mm.messageChannelCB[key] = fn
+}
+
+// Stop closes the message reception thread and closes the port.
+// TODO: test
+func (mm *MessageManager) Stop() {
+	// Stop messageReception
+	select {
+	case mm.quit <- struct{}{}:
+	}
+}
+
+// getNextID returns the next unique ID for the given tag. This function is not
+// thread-safe.
+func (mm *MessageManager) getNextID(tag Tag) uint64 {
+	if _, exists := mm.responseIDs[tag]; !exists {
+		mm.responseIDs[tag] = initID
+	}
+
+	id := mm.responseIDs[tag]
+	mm.responseIDs[tag]++
+	return id
+}
diff --git a/worker/messageManager_test.go b/worker/messageManager_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..bd01f64a3a50b3d1d246fa8cd85c04eda94a4899
--- /dev/null
+++ b/worker/messageManager_test.go
@@ -0,0 +1,344 @@
+////////////////////////////////////////////////////////////////////////////////
+// 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 worker
+
+import (
+	"encoding/json"
+	"github.com/hack-pad/safejs"
+	"gitlab.com/elixxir/wasm-utils/utils"
+	"reflect"
+	"strconv"
+	"syscall/js"
+	"testing"
+	"time"
+)
+
+func TestNewMessageManager(t *testing.T) {
+}
+
+// Unit test of initMessageManager.
+func Test_initMessageManager(t *testing.T) {
+	expected := &MessageManager{
+		senderCallbacks:   make(map[Tag]map[uint64]SenderCallback),
+		receiverCallbacks: make(map[Tag]ReceiverCallback),
+		responseIDs:       make(map[Tag]uint64),
+		messageChannelCB:  make(map[string]NewPortCallback),
+		quit:              make(chan struct{}),
+		name:              "name",
+		Params:            DefaultParams(),
+	}
+
+	received := initMessageManager(expected.name, expected.Params)
+
+	received.quit = expected.quit
+	if !reflect.DeepEqual(expected, received) {
+		t.Errorf("Unexpected MessageManager.\nexpected: %+v\nreceived: %+v",
+			expected, received)
+	}
+}
+
+func TestMessageManager_Send(t *testing.T) {
+}
+
+func TestMessageManager_SendTimeout(t *testing.T) {
+}
+
+func TestMessageManager_SendNoResponse(t *testing.T) {
+}
+
+func TestMessageManager_sendMessage(t *testing.T) {
+}
+
+func TestMessageManager_sendResponse(t *testing.T) {
+}
+
+func TestMessageManager_messageReception(t *testing.T) {
+}
+
+// Tests MessageManager.processReceivedMessage calls the expected callback.
+func TestMessageManager_processReceivedMessage(t *testing.T) {
+	mm := initMessageManager("", DefaultParams())
+
+	msg := Message{Tag: readyTag, ID: 5}
+	cbChan := make(chan struct{})
+	cb := func([]byte, func([]byte)) { cbChan <- struct{}{} }
+	mm.RegisterCallback(msg.Tag, cb)
+
+	data, err := json.Marshal(msg)
+	if err != nil {
+		t.Fatalf("Failed to JSON marshal Message: %+v", err)
+	}
+
+	go func() {
+		select {
+		case <-cbChan:
+		case <-time.After(10 * time.Millisecond):
+			t.Error("Timed out waiting for callback to be called.")
+		}
+	}()
+
+	err = mm.processReceivedMessage(data)
+	if err != nil {
+		t.Errorf("Failed to receive message: %+v", err)
+	}
+
+	msg = Message{Tag: "tag", Response: true}
+	cbChan = make(chan struct{})
+	cb2 := func([]byte) { cbChan <- struct{}{} }
+	mm.registerSenderCallback(msg.Tag, cb2)
+
+	go func() {
+		select {
+		case <-cbChan:
+		case <-time.After(10 * time.Millisecond):
+			t.Error("Timed out waiting for callback to be called.")
+		}
+	}()
+
+	err = mm.processReceivedMessage(data)
+	if err != nil {
+		t.Errorf("Failed to receive message: %+v", err)
+	}
+}
+
+// Tests MessageManager.processReceivedPort calls the expected callback.
+func TestMessageManager_processReceivedPort(t *testing.T) {
+	mm := initMessageManager("", DefaultParams())
+
+	cbChan := make(chan string)
+	cb := func(port js.Value, channelName string) { cbChan <- channelName }
+	key := "testKey"
+	channelName := "channel"
+	mm.RegisterMessageChannelCallback(key, cb)
+
+	mc, err := NewMessageChannel()
+	if err != nil {
+		t.Fatal(err)
+	}
+	port1, err := mc.Port1()
+	if err != nil {
+		t.Fatalf("Failed to get port1: %+v", err)
+	}
+
+	obj := map[string]any{
+		"port":    safejs.Unsafe(port1.Value),
+		"channel": utils.CopyBytesToJS([]byte(channelName)),
+		"key":     utils.CopyBytesToJS([]byte(key))}
+
+	go func() {
+		select {
+		case name := <-cbChan:
+			if channelName != name {
+				t.Errorf("Received incorrect channel name."+
+					"\nexpected: %q\nrecieved: %q", channelName, name)
+			}
+		case <-time.After(10 * time.Millisecond):
+			t.Error("Timed out waiting for callback to be called.")
+		}
+	}()
+
+	data := js.ValueOf(obj)
+	err = mm.processReceivedPort(data.Get("port"), data)
+	if err != nil {
+		t.Errorf("Failed to receive message: %+v", err)
+	}
+}
+
+// Tests that MessageManager.RegisterCallback registers a callback that is then
+// called by MessageManager.processReceivedMessage.
+func TestMessageManager_RegisterCallback(t *testing.T) {
+	mm := initMessageManager("", DefaultParams())
+
+	msg := Message{Tag: readyTag, ID: initID}
+	cbChan := make(chan struct{})
+	cb := func([]byte, func([]byte)) { cbChan <- struct{}{} }
+	mm.RegisterCallback(msg.Tag, cb)
+
+	data, err := json.Marshal(msg)
+	if err != nil {
+		t.Fatalf("Failed to JSON marshal Message: %+v", err)
+	}
+
+	go func() {
+		select {
+		case <-cbChan:
+		case <-time.After(10 * time.Millisecond):
+			t.Error("Timed out waiting for callback to be called.")
+		}
+	}()
+
+	err = mm.processReceivedMessage(data)
+	if err != nil {
+		t.Errorf("Failed to receive message: %+v", err)
+	}
+}
+
+// Tests MessageManager.getReceiverCallback returns the expected callback.
+func TestMessageManager_getReceiverCallback(t *testing.T) {
+	mm := initMessageManager("", DefaultParams())
+
+	expected := make(map[Tag]ReceiverCallback)
+	for i := 0; i < 5; i++ {
+		tag := Tag("tag" + strconv.Itoa(i))
+		cb := func([]byte, func([]byte)) {}
+		mm.RegisterCallback(tag, cb)
+		expected[tag] = cb
+	}
+
+	for tag, cb := range expected {
+		r, err := mm.getReceiverCallback(tag)
+		if err != nil {
+			t.Errorf("Error getting callback for tag %q: %+v", tag, err)
+		}
+
+		if reflect.ValueOf(cb).Pointer() != reflect.ValueOf(r).Pointer() {
+			t.Errorf("Wrong callback for tag %q."+
+				"\nexpected: %p\nreceived: %p", tag, cb, r)
+		}
+	}
+}
+
+// Tests that MessageManager.registerSenderCallback registers a callback that is
+// then called by MessageManager.processReceivedMessage.
+func TestMessageManager_registerSenderCallback(t *testing.T) {
+	mm := initMessageManager("", DefaultParams())
+
+	msg := Message{Tag: readyTag, Response: true}
+	cbChan := make(chan struct{})
+	cb := func([]byte) { cbChan <- struct{}{} }
+	msg.ID = mm.registerSenderCallback(msg.Tag, cb)
+
+	data, err := json.Marshal(msg)
+	if err != nil {
+		t.Fatalf("Failed to JSON marshal Message: %+v", err)
+	}
+
+	go func() {
+		select {
+		case <-cbChan:
+		case <-time.After(10 * time.Millisecond):
+			t.Error("Timed out waiting for callback to be called.")
+		}
+	}()
+
+	err = mm.processReceivedMessage(data)
+	if err != nil {
+		t.Errorf("Failed to receive message: %+v", err)
+	}
+}
+
+// Tests MessageManager.getSenderCallback returns the expected callback and
+// deletes it.
+func TestMessageManager_getSenderCallback(t *testing.T) {
+	mm := initMessageManager("", DefaultParams())
+
+	expected := make(map[Tag]map[uint64]SenderCallback)
+	for i := 0; i < 5; i++ {
+		tag := Tag("tag" + strconv.Itoa(i))
+		expected[tag] = make(map[uint64]SenderCallback)
+		for j := 0; j < 10; j++ {
+			cb := func([]byte) {}
+			id := mm.registerSenderCallback(tag, cb)
+			expected[tag][id] = cb
+		}
+	}
+
+	for tag, callbacks := range expected {
+		for id, cb := range callbacks {
+			r, err := mm.getSenderCallback(tag, id)
+			if err != nil {
+				t.Errorf("Error getting callback for tag %q and ID %d: %+v",
+					tag, id, err)
+			}
+
+			if reflect.ValueOf(cb).Pointer() != reflect.ValueOf(r).Pointer() {
+				t.Errorf("Wrong callback for tag %q and ID %d."+
+					"\nexpected: %p\nreceived: %p", tag, id, cb, r)
+			}
+
+			// Check that the callback was deleted
+			if r, err = mm.getSenderCallback(tag, id); err == nil {
+				t.Errorf("Did not get error when for callback that should be "+
+					"deleted for tag %q and ID %d: %p", tag, id, r)
+			}
+		}
+		if callbacks, exists := mm.senderCallbacks[tag]; exists {
+			t.Errorf("Empty map for tag %s not deleted: %+v", tag, callbacks)
+		}
+	}
+}
+
+// Tests that MessageManager.RegisterMessageChannelCallback registers a callback
+// that is then called by MessageManager.processReceivedPort.
+func TestMessageManager_RegisterMessageChannelCallback(t *testing.T) {
+	mm := initMessageManager("", DefaultParams())
+
+	cbChan := make(chan string)
+	cb := func(port js.Value, channelName string) { cbChan <- channelName }
+	key := "testKey"
+	channelName := "channel"
+	mm.RegisterMessageChannelCallback(key, cb)
+
+	mc, err := NewMessageChannel()
+	if err != nil {
+		t.Fatal(err)
+	}
+	port1, err := mc.Port1()
+	if err != nil {
+		t.Fatalf("Failed to get port1: %+v", err)
+	}
+
+	obj := map[string]any{
+		"port":    safejs.Unsafe(port1.Value),
+		"channel": utils.CopyBytesToJS([]byte(channelName)),
+		"key":     utils.CopyBytesToJS([]byte(key))}
+
+	go func() {
+		select {
+		case name := <-cbChan:
+			if channelName != name {
+				t.Errorf("Received incorrect channel name."+
+					"\nexpected: %q\nrecieved: %q", channelName, name)
+			}
+		case <-time.After(10 * time.Millisecond):
+			t.Error("Timed out waiting for callback to be called.")
+		}
+	}()
+
+	data := js.ValueOf(obj)
+	err = mm.processReceivedPort(data.Get("port"), data)
+	if err != nil {
+		t.Errorf("Failed to receive message: %+v", err)
+	}
+}
+
+func TestMessageManager_Stop(t *testing.T) {
+}
+
+// Tests that MessageManager.getNextID returns the expected ID for various Tags.
+func TestMessageManager_getNextID(t *testing.T) {
+	mm := initMessageManager("", DefaultParams())
+
+	for _, tag := range []Tag{readyTag, "test", "A", "B", "C"} {
+		id := mm.getNextID(tag)
+		if id != initID {
+			t.Errorf("ID for new tag %q is not initID."+
+				"\nexpected: %d\nreceived: %d", tag, initID, id)
+		}
+
+		for j := initID + 1; j < 100; j++ {
+			id = mm.getNextID(tag)
+			if id != j {
+				t.Errorf("Unexpected ID for tag %q."+
+					"\nexpected: %d\nreceived: %d", tag, j, id)
+			}
+		}
+	}
+}
diff --git a/worker/messagePort.go b/worker/messagePort.go
new file mode 100644
index 0000000000000000000000000000000000000000..10724555a9b439776bdcdb604640da2590138f4b
--- /dev/null
+++ b/worker/messagePort.go
@@ -0,0 +1,134 @@
+////////////////////////////////////////////////////////////////////////////////
+// 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 worker
+
+import (
+	"context"
+	"syscall/js"
+
+	"github.com/hack-pad/safejs"
+	"github.com/pkg/errors"
+
+	"gitlab.com/elixxir/wasm-utils/utils"
+)
+
+// MessagePort wraps a Javascript MessagePort object.
+//
+// Doc: https://developer.mozilla.org/en-US/docs/Web/API/MessagePort
+type MessagePort struct {
+	safejs.Value
+}
+
+// NewMessagePort wraps the given MessagePort.
+func NewMessagePort(v safejs.Value) (MessagePort, error) {
+	method, err := v.Get("postMessage")
+	if err != nil {
+		return MessagePort{}, err
+	}
+	if method.Type() != safejs.TypeFunction {
+		return MessagePort{}, errors.New("postMessage is not a function")
+	}
+	return MessagePort{v}, nil
+}
+
+// PostMessage sends a message from the port.
+func (mp MessagePort) PostMessage(message any) error {
+	_, err := mp.Call("postMessage", message)
+	return err
+}
+
+// PostMessageTransfer sends a message from the port and transfers ownership of
+// objects to other browsing contexts.
+func (mp MessagePort) PostMessageTransfer(message any, transfer ...any) error {
+	_, err := mp.Call("postMessage", message, transfer)
+	return err
+}
+
+// PostMessageTransferBytes sends the message bytes from the port via transfer.
+func (mp MessagePort) PostMessageTransferBytes(message []byte) error {
+	buffer := utils.CopyBytesToJS(message)
+	return mp.PostMessageTransfer(buffer, buffer.Get("buffer"))
+}
+
+// Listen registers listeners on the MessagePort and returns all events on the
+// returned channel.
+func (mp MessagePort) Listen(
+	ctx context.Context) (_ <-chan MessageEvent, err error) {
+	ctx, cancel := context.WithCancel(ctx)
+	defer func() {
+		if err != nil {
+			cancel()
+		}
+	}()
+
+	events := make(chan MessageEvent)
+	messageHandler, err := nonBlocking(func(args []safejs.Value) {
+		events <- parseMessageEvent(args[0])
+	})
+	if err != nil {
+		return nil, err
+	}
+	errorHandler, err := nonBlocking(func(args []safejs.Value) {
+		events <- MessageEvent{err: js.Error{Value: safejs.Unsafe(args[0])}}
+	})
+	if err != nil {
+		return nil, err
+	}
+	messageErrorHandler, err := nonBlocking(func(args []safejs.Value) {
+		events <- parseMessageEvent(args[0])
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	go func() {
+		<-ctx.Done()
+		_, err := mp.Call("removeEventListener", "message", messageHandler)
+		if err == nil {
+			messageHandler.Release()
+		}
+		_, err = mp.Call("removeEventListener", "error", errorHandler)
+		if err == nil {
+			errorHandler.Release()
+		}
+		_, err = mp.Call("removeEventListener", "messageerror", messageErrorHandler)
+		if err == nil {
+			messageErrorHandler.Release()
+		}
+		close(events)
+	}()
+	_, err = mp.Call("addEventListener", "message", messageHandler)
+	if err != nil {
+		return nil, err
+	}
+	_, err = mp.Call("addEventListener", "error", errorHandler)
+	if err != nil {
+		return nil, err
+	}
+	_, err = mp.Call("addEventListener", "messageerror", messageErrorHandler)
+	if err != nil {
+		return nil, err
+	}
+	if start, err := mp.Get("start"); err == nil {
+		if truthy, err := start.Truthy(); err == nil && truthy {
+			if _, err := mp.Call("start"); err != nil {
+				return nil, err
+			}
+		}
+	}
+	return events, nil
+}
+
+func nonBlocking(fn func(args []safejs.Value)) (safejs.Func, error) {
+	return safejs.FuncOf(func(_ safejs.Value, args []safejs.Value) any {
+		go fn(args)
+		return nil
+	})
+}
diff --git a/worker/params.go b/worker/params.go
new file mode 100644
index 0000000000000000000000000000000000000000..adddb34860381018d77ba28511f5cadefb7a48ac
--- /dev/null
+++ b/worker/params.go
@@ -0,0 +1,31 @@
+////////////////////////////////////////////////////////////////////////////////
+// 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 worker
+
+import "time"
+
+// Params are parameters used in the [MessageManager].
+type Params struct {
+	// MessageLogging indicates if a DEBUG message should be printed every time
+	// a message is sent or received.
+	MessageLogging bool
+
+	// ResponseTimeout is the default timeout to wait for a response before
+	// timing out and returning an error.
+	ResponseTimeout time.Duration
+}
+
+// DefaultParams returns the default parameters.
+func DefaultParams() Params {
+	return Params{
+		MessageLogging:  false,
+		ResponseTimeout: 30 * time.Second,
+	}
+}
diff --git a/worker/tag.go b/worker/tag.go
index fa458c291802c47d0ffb1970ef75202b936561a4..3bb27a08e30241b233bec63225eac3e266fda9f7 100644
--- a/worker/tag.go
+++ b/worker/tag.go
@@ -14,5 +14,10 @@ type Tag string
 
 // Generic tags used by all workers.
 const (
-	readyTag Tag = "Ready"
+	readyTag Tag = "<WW>Ready</WW>"
+)
+
+const (
+	Channel1LogMsgChanName = "Channel1Logger"
+	LoggerTag              = "logger"
 )
diff --git a/worker/thread.go b/worker/thread.go
index 203b00302a97c22f4e44501e2c721b52312c14ab..b67c3464c885bf129aeffef44fef1683c44881f7 100644
--- a/worker/thread.go
+++ b/worker/thread.go
@@ -10,287 +10,151 @@
 package worker
 
 import (
-	"encoding/json"
-	"sync"
 	"syscall/js"
+	"time"
 
+	"github.com/hack-pad/safejs"
 	"github.com/pkg/errors"
 	jww "github.com/spf13/jwalterweatherman"
-
-	"gitlab.com/elixxir/xxdk-wasm/utils"
 )
 
 // 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)
+type ThreadReceptionCallback func(message []byte, reply func(message []byte))
 
 // ThreadManager queues incoming messages from the main thread and handles them
 // based on their tag.
 type ThreadManager struct {
-	// messages is a list of queued messages sent from the main thread.
-	messages chan js.Value
-
-	// callbacks is a list of callbacks to handle messages that come from the
-	// main thread keyed on the callback tag.
-	callbacks map[Tag]ThreadReceptionCallback
-
-	// 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{}
+	mm *MessageManager
 
-	// name describes the worker. It is used for debugging and logging purposes.
-	name string
-
-	// messageLogging determines if debug message logs should be printed every
-	// time a message is sent/received to/from the worker.
-	messageLogging bool
-
-	mux sync.Mutex
+	// Wrapper of the DedicatedWorkerGlobalScope.
+	// Doc: https://developer.mozilla.org/en-US/docs/Web/API/DedicatedWorkerGlobalScope
+	t Thread
 }
 
 // NewThreadManager initialises a new ThreadManager.
-func NewThreadManager(name string, messageLogging bool) *ThreadManager {
-	tm := &ThreadManager{
-		messages:       make(chan js.Value, 100),
-		callbacks:      make(map[Tag]ThreadReceptionCallback),
-		receiveQueue:   make(chan js.Value, receiveQueueChanSize),
-		quit:           make(chan struct{}),
-		name:           name,
-		messageLogging: messageLogging,
+func NewThreadManager(name string, messageLogging bool) (*ThreadManager, error) {
+	t, err := NewThread()
+	if err != nil {
+		return nil, errors.Wrapf(err, "failed to construct GlobalSelf")
 	}
-	// Start thread to process messages from the main thread
-	go tm.processThread()
-
-	tm.addEventListeners()
 
-	return tm
-}
+	p := DefaultParams()
+	p.MessageLogging = messageLogging
+	mm, err := NewMessageManager(t.Value, name+"-remote", p)
+	if err != nil {
+		return nil, errors.Wrapf(err, "failed to construct message manager")
+	}
 
-// Stop closes the thread manager and stops the worker.
-func (tm *ThreadManager) Stop() {
-	// Stop processThread
-	select {
-	case tm.quit <- struct{}{}:
+	tm := &ThreadManager{
+		mm: mm,
+		t:  t,
 	}
 
-	// Terminate the worker
-	go tm.close()
+	return tm, nil
 }
 
-// processThread processes received messages sequentially.
-func (tm *ThreadManager) processThread() {
-	jww.INFO.Printf("[WW] [%s] Starting worker process thread.", tm.name)
-	for {
-		select {
-		case <-tm.quit:
-			jww.INFO.Printf("[WW] [%s] Quitting worker process thread.", tm.name)
-			return
-		case msgData := <-tm.receiveQueue:
+// Stop closes the thread manager and stops the worker.
+func (tm *ThreadManager) Stop() error {
+	tm.mm.Stop()
 
-			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
+	// Close the worker
+	err := tm.t.Close()
+	return errors.Wrapf(err, "failed to close worker %q", tm.mm.name)
+}
 
-			default:
-				jww.ERROR.Printf("[WW] [%s] Cannot handle data of type %s "+
-					"from main thread: %s",
-					tm.name, msgData.Type(), utils.JsToJson(msgData))
-			}
-		}
-	}
+func (tm *ThreadManager) GetWorker() js.Value {
+	return safejs.Unsafe(tm.t.Value)
 }
 
 // SignalReady sends a signal to the main thread indicating that the worker is
 // ready. Once the main thread receives this, it will initiate communication.
 // Therefore, this should only be run once all listeners are ready.
 func (tm *ThreadManager) SignalReady() {
-	tm.SendMessage(readyTag, nil)
-}
-
-// SendMessage sends a message to the main thread for the given tag.
-func (tm *ThreadManager) SendMessage(tag Tag, data []byte) {
-	msg := Message{
-		Tag:      tag,
-		ID:       initID,
-		DeleteCB: false,
-		Data:     data,
-	}
-
-	if tm.messageLogging {
-		jww.DEBUG.Printf("[WW] [%s] Worker sending message for %q with data: %s",
-			tm.name, tag, data)
-	}
-
-	payload, err := json.Marshal(msg)
+	err := tm.mm.SendNoResponse(readyTag, nil)
 	if err != nil {
-		jww.FATAL.Panicf("[WW] [%s] Worker failed to marshal %T for %q going "+
-			"to main: %+v", tm.name, msg, tag, err)
+		jww.FATAL.Panicf(
+			"[WW] [%s] Failed to send ready signal: %+v", tm.Name(), err)
 	}
-
-	go tm.postMessage(payload)
 }
 
-// sendResponse sends a reply to the main thread with the given tag and ID.
-func (tm *ThreadManager) sendResponse(tag Tag, id uint64, data []byte) error {
-	msg := Message{
-		Tag:      tag,
-		ID:       id,
-		DeleteCB: true,
-		Data:     data,
-	}
-
-	if tm.messageLogging {
-		jww.DEBUG.Printf("[WW] [%s] Worker sending reply for %q and ID %d "+
-			"with data: %s", tm.name, tag, id, data)
-	}
-
-	payload, err := json.Marshal(msg)
-	if err != nil {
-		return errors.Errorf("worker failed to marshal %T for %q and ID "+
-			"%d going to main: %+v", msg, tag, id, err)
-	}
-
-	go tm.postMessage(payload)
-
-	return nil
+// RegisterMessageChannelCallback registers a callback that will be called when
+// a MessagePort with the given Channel is received.
+func (tm *ThreadManager) RegisterMessageChannelCallback(
+	tag string, fn NewPortCallback) {
+	tm.mm.RegisterMessageChannelCallback(tag, fn)
 }
 
-// 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 js.Value) {
-	tm.receiveQueue <- data
+// SendMessage sends a message to the main thread with the given tag and waits
+// for a response. An error is returned on failure to send or on timeout.
+func (tm *ThreadManager) SendMessage(
+	tag Tag, data []byte) (response []byte, err error) {
+	return tm.mm.Send(tag, data)
 }
 
-// processReceivedMessage processes the message received from the main thread
-// and calls the associated callback. If the registered callback returns a
-// response, it is sent to the main thread. This functions blocks until the
-// callback returns.
-func (tm *ThreadManager) processReceivedMessage(data []byte) error {
-	var msg Message
-	err := json.Unmarshal(data, &msg)
-	if err != nil {
-		return err
-	}
-
-	if tm.messageLogging {
-		jww.DEBUG.Printf("[WW] [%s] Worker received message for %q and ID %d "+
-			"with data: %s", tm.name, msg.Tag, msg.ID, msg.Data)
-	}
-
-	tm.mux.Lock()
-	callback, exists := tm.callbacks[msg.Tag]
-	tm.mux.Unlock()
-	if !exists {
-		return errors.Errorf("no callback found for tag %q", msg.Tag)
-	}
-
-	// Call callback and register response with its return
-	response, err := callback(msg.Data)
-	if err != nil {
-		return errors.Errorf("callback for %q and ID %d returned an error: %+v",
-			msg.Tag, msg.ID, err)
-	}
-	if response != nil {
-		return tm.sendResponse(msg.Tag, msg.ID, response)
-	}
+// SendTimeout sends a message to the main thread with the given tag and waits
+// for a response. An error is returned on failure to send or on the specified
+// timeout.
+func (tm *ThreadManager) SendTimeout(
+	tag Tag, data []byte, timeout time.Duration) (response []byte, err error) {
+	return tm.mm.SendTimeout(tag, data, timeout)
+}
 
-	return nil
+// SendNoResponse sends a message to the main thread with the given tag. It
+// returns immediately and does not wait for a response.
+func (tm *ThreadManager) SendNoResponse(tag Tag, data []byte) error {
+	return tm.mm.SendNoResponse(tag, data)
 }
 
-// RegisterCallback registers the callback with the given tag overwriting any
-// previous registered callbacks with the same tag. This function is thread
-// safe.
-//
-// If the callback returns anything but nil, it will be returned as a response.
-func (tm *ThreadManager) RegisterCallback(
-	tag Tag, receptionCallback ThreadReceptionCallback) {
-	jww.DEBUG.Printf(
-		"[WW] [%s] Worker registering callback for tag %q", tm.name, tag)
-	tm.mux.Lock()
-	tm.callbacks[tag] = receptionCallback
-	tm.mux.Unlock()
+// RegisterCallback registers the callback for the given tag. Previous tags are
+// overwritten. This function is thread safe.
+func (tm *ThreadManager) RegisterCallback(tag Tag, receiverCB ReceiverCallback) {
+	tm.mm.RegisterCallback(tag, receiverCB)
 }
 
+// Name returns the name of the web worker.
+func (tm *ThreadManager) Name() string { return tm.mm.name }
+
 ////////////////////////////////////////////////////////////////////////////////
 // Javascript Call Wrappers                                                   //
 ////////////////////////////////////////////////////////////////////////////////
 
-// addEventListeners adds the event listeners needed to receive a message from
-// the worker. Two listeners were added; one to receive messages from the worker
-// and the other to receive errors.
-func (tm *ThreadManager) addEventListeners() {
-	// Create a listener for when the message event is fire on the worker. This
-	// 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(args[0].Get("data"))
-		return nil
-	})
+// Thread has the methods of the Javascript DedicatedWorkerGlobalScope.
+//
+// Doc: https://developer.mozilla.org/en-US/docs/Web/API/DedicatedWorkerGlobalScope
+type Thread struct {
+	MessagePort
+}
 
-	// Create listener for when an error event is fired on the worker. This
-	// occurs when an error occurs in the worker.
-	// 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))
-		return nil
-	})
+// NewThread creates a new Thread from Global.
+func NewThread() (Thread, error) {
+	self, err := safejs.Global().Get("self")
+	if err != nil {
+		return Thread{}, err
+	}
 
-	// Create listener for when a messageerror event is fired on the worker.
-	// This occurs when it receives a message that cannot be deserialized.
-	// 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))
-		return nil
-	})
+	mp, err := NewMessagePort(self)
+	if err != nil {
+		return Thread{}, err
+	}
 
-	// Register each event listener on the worker using addEventListener
-	// Doc: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
-	js.Global().Call("addEventListener", "message", messageEvent)
-	js.Global().Call("addEventListener", "error", errorEvent)
-	js.Global().Call("addEventListener", "messageerror", messageerrorEvent)
+	return Thread{mp}, nil
 }
 
-// postMessage sends a message from this worker to the main WASM thread.
+// Name returns the name that the Worker was (optionally) given when it was
+// created.
 //
-// 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 []byte) {
-	buffer := utils.CopyBytesToJS(aMessage)
-	js.Global().Call("postMessage", buffer, []any{buffer.Get("buffer")})
+// Doc: https://developer.mozilla.org/en-US/docs/Web/API/DedicatedWorkerGlobalScope/name
+func (t *Thread) Name() string {
+	return safejs.Unsafe(t.Value).Get("name").String()
 }
 
-// close discards any tasks queued in the worker's event loop, effectively
+// Close discards any tasks queued in the worker's event loop, effectively
 // closing this particular scope.
 //
-// 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.
-//
 // Doc: https://developer.mozilla.org/en-US/docs/Web/API/DedicatedWorkerGlobalScope/close
-func (tm *ThreadManager) close() {
-	js.Global().Call("close")
+func (t *Thread) Close() error {
+	_, err := t.Call("close")
+	return err
 }
diff --git a/worker/thread_test.go b/worker/thread_test.go
deleted file mode 100644
index ada6de8fc00916699b45eaee1483830a277b9fc9..0000000000000000000000000000000000000000
--- a/worker/thread_test.go
+++ /dev/null
@@ -1,73 +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 worker
-
-import (
-	"encoding/json"
-	"testing"
-	"time"
-)
-
-// Tests that ThreadManager.processReceivedMessage calls the expected callback.
-func TestThreadManager_processReceivedMessage(t *testing.T) {
-	tm := &ThreadManager{callbacks: make(map[Tag]ThreadReceptionCallback)}
-
-	msg := Message{Tag: readyTag, ID: 5}
-	cbChan := make(chan struct{})
-	cb := func([]byte) ([]byte, error) { cbChan <- struct{}{}; return nil, nil }
-	tm.callbacks[msg.Tag] = cb
-
-	data, err := json.Marshal(msg)
-	if err != nil {
-		t.Fatalf("Failed to JSON marshal Message: %+v", err)
-	}
-
-	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)
-	}
-}
-
-// Tests that ThreadManager.RegisterCallback registers a callback that is then
-// called by ThreadManager.processReceivedMessage.
-func TestThreadManager_RegisterCallback(t *testing.T) {
-	tm := &ThreadManager{callbacks: make(map[Tag]ThreadReceptionCallback)}
-
-	msg := Message{Tag: readyTag, ID: 5}
-	cbChan := make(chan struct{})
-	cb := func([]byte) ([]byte, error) { cbChan <- struct{}{}; return nil, nil }
-	tm.RegisterCallback(msg.Tag, cb)
-
-	data, err := json.Marshal(msg)
-	if err != nil {
-		t.Fatalf("Failed to JSON marshal Message: %+v", err)
-	}
-
-	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)
-	}
-}