diff --git a/Makefile b/Makefile
index 4a6c40fd662d82ea2d2060f874c3a88ca266a0e3..916f6fd97a7a4b8e1018f3dd904f0cef7c0fc756 100644
--- a/Makefile
+++ b/Makefile
@@ -12,8 +12,8 @@ build:
 	go mod tidy
 
 update_release:
-	GOFLAGS="" go get -d gitlab.com/elixxir/client/v4@release
-	GOFLAGS="" go get gitlab.com/elixxir/crypto@release
+	GOFLAGS="" go get -d gitlab.com/elixxir/client/v4@project/adminCommands
+	GOFLAGS="" go get gitlab.com/elixxir/crypto@project/adminCommands
 	GOFLAGS="" go get gitlab.com/elixxir/primitives@release
 	GOFLAGS="" go get gitlab.com/xx_network/crypto@release
 	GOFLAGS="" go get gitlab.com/xx_network/primitives@release
diff --git a/go.mod b/go.mod
index 99834562d11b4df8a145098e671b1b798647fd64..e120361848d44dba0ad70b1d388cd72bcc66260e 100644
--- a/go.mod
+++ b/go.mod
@@ -7,15 +7,16 @@ require (
 	github.com/hack-pad/go-indexeddb v0.2.0
 	github.com/pkg/errors v0.9.1
 	github.com/spf13/jwalterweatherman v1.1.0
-	gitlab.com/elixxir/client/v4 v4.3.11
-	gitlab.com/elixxir/crypto v0.0.7-0.20221214192244-6783272c04a0
+	gitlab.com/elixxir/client/v4 v4.3.12-0.20230104175249-e265a4ca4e58
+	gitlab.com/elixxir/crypto v0.0.7-0.20230104175234-604a6cd56b98
 	gitlab.com/elixxir/primitives v0.0.3-0.20221214192222-988b44a6958a
 	gitlab.com/xx_network/crypto v0.0.5-0.20221121220724-8eefdbb0eb46
-	gitlab.com/xx_network/primitives v0.0.4-0.20221209210320-376735467d58
+	gitlab.com/xx_network/primitives v0.0.4-0.20221219230308-4b5550a9247d
 	golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa
 )
 
 require (
+	filippo.io/edwards25519 v1.0.0 // indirect
 	git.xx.network/elixxir/grpc-web-go-client v0.0.0-20221215181401-0b8a26d47532 // indirect
 	github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect
 	github.com/badoux/checkmail v1.2.1 // indirect
@@ -33,6 +34,8 @@ require (
 	github.com/mattn/go-isatty v0.0.14 // indirect
 	github.com/mitchellh/go-homedir v1.1.0 // indirect
 	github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
+	github.com/oasisprotocol/curve25519-voi v0.0.0-20221003100820-41fad3beba17 // indirect
+	github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 // 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
@@ -46,10 +49,13 @@ require (
 	gitlab.com/elixxir/ekv v0.2.1 // indirect
 	gitlab.com/xx_network/comms v0.0.4-0.20221215214252-1275cef8760e // indirect
 	gitlab.com/xx_network/ring v0.0.3-0.20220902183151-a7d3b15bc981 // indirect
+	gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect
+	gitlab.com/yawning/nyquist.git v0.0.0-20221003103146-de5645224a22 // indirect
+	gitlab.com/yawning/x448.git v0.0.0-20221003101044-617eb9b7d9b7 // indirect
 	go.uber.org/atomic v1.10.0 // indirect
 	go.uber.org/ratelimit v0.2.0 // indirect
 	golang.org/x/net v0.0.0-20220822230855-b0a4917ee28c // indirect
-	golang.org/x/sys v0.0.0-20220731174439-a90be440212d // indirect
+	golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec // indirect
 	golang.org/x/text v0.3.7 // 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 394f9f31793bcf8c7a5ae66374cada0b18345486..f6a9c5771a5f13d839a4b85d08a8f282d284262c 100644
--- a/go.sum
+++ b/go.sum
@@ -1,6 +1,8 @@
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
+filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek=
+filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
 git.xx.network/elixxir/grpc-web-go-client v0.0.0-20221215181401-0b8a26d47532 h1:EH4TFLgXGgofV2MsUOgNDmn3X+qfhbQ2RV6zOYRaSdU=
 git.xx.network/elixxir/grpc-web-go-client v0.0.0-20221215181401-0b8a26d47532/go.mod h1:uFKw2wmgtlYMdiIm08dM0Vj4XvX9ZKVCj71c8O7SAPo=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
@@ -255,6 +257,10 @@ github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxzi
 github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
+github.com/oasisprotocol/curve25519-voi v0.0.0-20221003100820-41fad3beba17 h1:lpwUgSIAfvJJ9sQ9BB//ZCjqzzFSuSg8mYOf+8L96+E=
+github.com/oasisprotocol/curve25519-voi v0.0.0-20221003100820-41fad3beba17/go.mod h1:hVoHR2EVESiICEMbg137etN/Lx+lSrHPTD39Z/uE+2s=
+github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 h1:1102pQc2SEPp5+xrS26wEaeb26sZy6k9/ZXlZN+eXE4=
+github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7/go.mod h1:UqoUn6cHESlliMhOnKLWr+CBH+e3bazUPvFj1XZwAjs=
 github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
 github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
 github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
@@ -341,12 +347,15 @@ github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3
 github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
 github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2 h1:5u+EJUQiosu3JFX0XS0qTf5FznsMOzTjGqavBGuCbo0=
 github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2/go.mod h1:4kyMkleCiLkgY6z8gK5BkI01ChBtxR0ro3I1ZDcGM3w=
@@ -369,12 +378,12 @@ 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-20211222005329-7d931ceead6f h1:yXGvNBqzZwAhDYlSnxPRbgor6JWoOt1Z7s3z1O9JR40=
 gitlab.com/elixxir/bloomfilter v0.0.0-20211222005329-7d931ceead6f/go.mod h1:H6jztdm0k+wEV2QGK/KYA+MY9nj9Zzatux/qIvDDv3k=
-gitlab.com/elixxir/client/v4 v4.3.11 h1:LGXdAjnGdWbK1eVtrAn5Fwage7vrlrgPXsoEtUJ4Lyg=
-gitlab.com/elixxir/client/v4 v4.3.11/go.mod h1:gxRW2YXDxCzESBnYqMDKH2I25TGv226TDujdQno3JQw=
+gitlab.com/elixxir/client/v4 v4.3.12-0.20230104175249-e265a4ca4e58 h1:cZFtKQQT3FA6/calIKcDkwPhNjRWW/S9rHF788D03qc=
+gitlab.com/elixxir/client/v4 v4.3.12-0.20230104175249-e265a4ca4e58/go.mod h1:IbslBQ3B9cAoWiHhucRSLcDiF6CMIDmbRlHfoO0sEDA=
 gitlab.com/elixxir/comms v0.0.4-0.20221215214627-7807bfdde33a h1:DuqDqWc5cWjZ3qk98K1Bf9y1dYlyCeIigFmkHWDKc1Q=
 gitlab.com/elixxir/comms v0.0.4-0.20221215214627-7807bfdde33a/go.mod h1:B2Yek4mCbtN2aXZkyZcUffd3sTEZ5WgKD0mRBSVYtF8=
-gitlab.com/elixxir/crypto v0.0.7-0.20221214192244-6783272c04a0 h1:dwCf7wKv2DCuYZZ394bSQWdUOXiABLsEyDvXZUOo83o=
-gitlab.com/elixxir/crypto v0.0.7-0.20221214192244-6783272c04a0/go.mod h1:oRh3AwveOEvpk9E3kRcMGK8fImcEnN0PY4jr9HDgQE8=
+gitlab.com/elixxir/crypto v0.0.7-0.20230104175234-604a6cd56b98 h1:4uAPUenpRJYdnVGJzC3QXPX+dQkztL3YOMC1C0lJGN4=
+gitlab.com/elixxir/crypto v0.0.7-0.20230104175234-604a6cd56b98/go.mod h1:7whUm4bnEdEoiVfMnu3TbHgvlrz0Ywp/Tekqg2Wl7vw=
 gitlab.com/elixxir/ekv v0.2.1 h1:dtwbt6KmAXG2Tik5d60iDz2fLhoFBgWwST03p7T+9Is=
 gitlab.com/elixxir/ekv v0.2.1/go.mod h1:USLD7xeDnuZEavygdrgzNEwZXeLQJK/w1a+htpN+JEU=
 gitlab.com/elixxir/primitives v0.0.3-0.20221214192222-988b44a6958a h1:F17FfEjS+/uDI/TTYQD21S5JvNZ9+p9bieau2nyLCzo=
@@ -383,10 +392,16 @@ gitlab.com/xx_network/comms v0.0.4-0.20221215214252-1275cef8760e h1:l+FiCBP2Lc1+
 gitlab.com/xx_network/comms v0.0.4-0.20221215214252-1275cef8760e/go.mod h1:FR/OyruSuob6+xzSZtk+rXlncbRr6nDKFypX3vwtkFc=
 gitlab.com/xx_network/crypto v0.0.5-0.20221121220724-8eefdbb0eb46 h1:6AHgUpWdJ72RVTTdJSvfThZiYTQNUnrPaTCl/EkRLpg=
 gitlab.com/xx_network/crypto v0.0.5-0.20221121220724-8eefdbb0eb46/go.mod h1:acWUBKCpae/XVaQF7J9RnLAlBT13i5r7gnON+mrIxBk=
-gitlab.com/xx_network/primitives v0.0.4-0.20221209210320-376735467d58 h1:HpeUIf1gIIelLH3LHxEf3/GalecbbtZnOnIegJHALoc=
-gitlab.com/xx_network/primitives v0.0.4-0.20221209210320-376735467d58/go.mod h1:wUxbEBGOBJZ/RkAiVAltlC1uIlIrU0dE113Nq7HiOhw=
+gitlab.com/xx_network/primitives v0.0.4-0.20221219230308-4b5550a9247d h1:D9hEtiQ7xj0yFBkDkb4X4S95RfNoeXxtB1eE4UuFHtk=
+gitlab.com/xx_network/primitives v0.0.4-0.20221219230308-4b5550a9247d/go.mod h1:wUxbEBGOBJZ/RkAiVAltlC1uIlIrU0dE113Nq7HiOhw=
 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=
+gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec/go.mod h1:BZ1RAoRPbCxum9Grlv5aeksu2H8BiKehBYooU2LFiOQ=
+gitlab.com/yawning/nyquist.git v0.0.0-20221003103146-de5645224a22 h1:25fLWFlW+5awvcQhZj4drwVHP4Cmb+iZpfiS6LgE8f0=
+gitlab.com/yawning/nyquist.git v0.0.0-20221003103146-de5645224a22/go.mod h1:VvFd4eOUakA3ieUDzIpPT5GwkBTS/NKvIWr0SlNI8U4=
+gitlab.com/yawning/x448.git v0.0.0-20221003101044-617eb9b7d9b7 h1:ITrNVw6uSwSdEap0RR4us4RV1CHPBHvBZApENRcDk3c=
+gitlab.com/yawning/x448.git v0.0.0-20221003101044-617eb9b7d9b7/go.mod h1:BC2R0OW0tAYTMNLB4UMXwkk7WKokoDZP5n73hyLPyCo=
 go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
 go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
 go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
@@ -413,6 +428,7 @@ golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c=
 golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
@@ -475,6 +491,7 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -492,10 +509,11 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220325203850-36772127a21f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 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-20220731174439-a90be440212d h1:Sv5ogFZatcgIMMtBSTTAgMYsicp25MXBubjXNDKwm80=
-golang.org/x/sys v0.0.0-20220731174439-a90be440212d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec h1:BkDtF2Ih9xZ7le9ndzTA7KJow28VbQW3odyk/8drmuI=
+golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
@@ -588,6 +606,7 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/indexedDb/channels/implementation.go b/indexedDb/channels/implementation.go
index f867e4b2f4ff590cc206562a5bdff87d40fbe03a..26c78490d6c522e16a3ef2df816af4f8e5af6998 100644
--- a/indexedDb/channels/implementation.go
+++ b/indexedDb/channels/implementation.go
@@ -13,11 +13,13 @@ import (
 	"crypto/ed25519"
 	"encoding/base64"
 	"encoding/json"
-	"gitlab.com/elixxir/xxdk-wasm/indexedDb"
+	"strings"
 	"sync"
 	"syscall/js"
 	"time"
 
+	"gitlab.com/elixxir/xxdk-wasm/indexedDb"
+
 	"github.com/hack-pad/go-indexeddb/idb"
 	"github.com/pkg/errors"
 	jww "github.com/spf13/jwalterweatherman"
@@ -26,6 +28,7 @@ import (
 	"gitlab.com/elixxir/client/v4/cmix/rounds"
 	cryptoBroadcast "gitlab.com/elixxir/crypto/broadcast"
 	cryptoChannel "gitlab.com/elixxir/crypto/channel"
+	"gitlab.com/elixxir/crypto/message"
 	"gitlab.com/elixxir/xxdk-wasm/utils"
 	"gitlab.com/xx_network/primitives/id"
 )
@@ -40,6 +43,13 @@ type wasmModel struct {
 	updateMux         sync.Mutex
 }
 
+// DeleteMessage removes a message with the given messageID from storage.
+func (w *wasmModel) DeleteMessage(messageID message.ID) error {
+	msgId := js.ValueOf(base64.StdEncoding.EncodeToString(messageID.Bytes()))
+	return indexedDb.DeleteIndex(w.db, messageStoreName,
+		messageStoreMessageIndex, pkeyName, msgId)
+}
+
 // JoinChannel is called whenever a channel is joined locally.
 func (w *wasmModel) JoinChannel(channel *cryptoBroadcast.Channel) {
 	parentErr := errors.New("failed to JoinChannel")
@@ -76,35 +86,12 @@ func (w *wasmModel) JoinChannel(channel *cryptoBroadcast.Channel) {
 func (w *wasmModel) LeaveChannel(channelID *id.ID) {
 	parentErr := errors.New("failed to LeaveChannel")
 
-	// Prepare the Transaction
-	txn, err := w.db.Transaction(idb.TransactionReadWrite, channelsStoreName)
-	if err != nil {
-		jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr,
-			"Unable to create Transaction: %+v", err))
-		return
-	}
-	store, err := txn.ObjectStore(channelsStoreName)
-	if err != nil {
-		jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr,
-			"Unable to get ObjectStore: %+v", err))
-		return
-	}
-
-	// Perform the operation
-	_, err = store.Delete(js.ValueOf(channelID.String()))
-	if err != nil {
-		jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr,
-			"Unable to Delete Channel: %+v", err))
-		return
-	}
-
-	// Wait for the operation to return
-	ctx, cancel := indexedDb.NewContext()
-	err = txn.Await(ctx)
-	cancel()
+	// Delete the channel from storage
+	err := indexedDb.Delete(w.db, channelsStoreName,
+		js.ValueOf(channelID.String()))
 	if err != nil {
 		jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr,
-			"Deleting Channel failed: %+v", err))
+			"Unable to delete Channel: %+v", err))
 		return
 	}
 
@@ -165,11 +152,10 @@ func (w *wasmModel) deleteMsgByChannel(channelID *id.ID) error {
 //
 // 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.
-func (w *wasmModel) ReceiveMessage(channelID *id.ID,
-	messageID cryptoChannel.MessageID, nickname, text string,
-	pubKey ed25519.PublicKey, codeset uint8,
-	timestamp time.Time, lease time.Duration, round rounds.Round,
-	mType channels.MessageType, status channels.SentStatus) uint64 {
+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
 
@@ -184,7 +170,8 @@ func (w *wasmModel) ReceiveMessage(channelID *id.ID,
 
 	msgToInsert := buildMessage(
 		channelID.Marshal(), messageID.Bytes(), nil, nickname,
-		textBytes, pubKey, codeset, timestamp, lease, round.ID, mType, status)
+		textBytes, pubKey, dmToken, codeset, timestamp, lease, round.ID, mType,
+		false, hidden, status)
 
 	uuid, err := w.receiveHelper(msgToInsert, false)
 	if err != nil {
@@ -201,11 +188,11 @@ func (w *wasmModel) ReceiveMessage(channelID *id.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.
-func (w *wasmModel) ReceiveReply(channelID *id.ID,
-	messageID cryptoChannel.MessageID, replyTo cryptoChannel.MessageID,
-	nickname, text string, pubKey ed25519.PublicKey, codeset uint8,
-	timestamp time.Time, lease time.Duration, round rounds.Round,
-	mType channels.MessageType, status channels.SentStatus) uint64 {
+func (w *wasmModel) ReceiveReply(channelID *id.ID, messageID,
+	replyTo 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
 
@@ -219,8 +206,8 @@ func (w *wasmModel) ReceiveReply(channelID *id.ID,
 	}
 
 	msgToInsert := buildMessage(channelID.Marshal(), messageID.Bytes(),
-		replyTo.Bytes(), nickname, textBytes, pubKey, codeset,
-		timestamp, lease, round.ID, mType, status)
+		replyTo.Bytes(), nickname, textBytes, pubKey, dmToken, codeset,
+		timestamp, lease, round.ID, mType, hidden, false, status)
 
 	uuid, err := w.receiveHelper(msgToInsert, false)
 
@@ -237,11 +224,11 @@ func (w *wasmModel) ReceiveReply(channelID *id.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.
-func (w *wasmModel) ReceiveReaction(channelID *id.ID,
-	messageID cryptoChannel.MessageID, reactionTo cryptoChannel.MessageID,
-	nickname, reaction string, pubKey ed25519.PublicKey, codeset uint8,
-	timestamp time.Time, lease time.Duration, round rounds.Round,
-	mType channels.MessageType, status channels.SentStatus) uint64 {
+func (w *wasmModel) ReceiveReaction(channelID *id.ID, messageID,
+	reactionTo message.ID, nickname, reaction 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(reaction)
 	var err error
 
@@ -256,7 +243,8 @@ func (w *wasmModel) ReceiveReaction(channelID *id.ID,
 
 	msgToInsert := buildMessage(
 		channelID.Marshal(), messageID.Bytes(), reactionTo.Bytes(), nickname,
-		textBytes, pubKey, codeset, timestamp, lease, round.ID, mType, status)
+		textBytes, pubKey, dmToken, codeset, timestamp, lease, round.ID, mType,
+		false, hidden, status)
 
 	uuid, err := w.receiveHelper(msgToInsert, false)
 	if err != nil {
@@ -266,15 +254,54 @@ func (w *wasmModel) ReceiveReaction(channelID *id.ID,
 	return uuid
 }
 
-// UpdateSentStatus is called whenever the [channels.SentStatus] of a message
-// has changed. At this point the message ID goes from empty/unknown to
-// populated.
+// UpdateFromMessageID is called whenever a message with the message ID is
+// modified.
+//
+// The API needs to return the UUID of the modified message that can be
+// referenced at a later time.
+//
+// timestamp, round, pinned, and hidden are all nillable and may be updated
+// based upon the UUID at a later date. If a nil value is passed, then make
+// no update.
+func (w *wasmModel) UpdateFromMessageID(messageID message.ID,
+	timestamp *time.Time, round *rounds.Round, pinned, hidden *bool,
+	status *channels.SentStatus) uint64 {
+	parentErr := errors.New("failed to UpdateFromMessageID")
+
+	// FIXME: this is a bit of race condition without the mux.
+	//        This should be done via the transactions (i.e., make a
+	//        special version of receiveHelper)
+	w.updateMux.Lock()
+	defer w.updateMux.Unlock()
+
+	msgIDStr := base64.StdEncoding.EncodeToString(messageID.Marshal())
+	currentMsgObj, err := indexedDb.GetIndex(w.db, messageStoreName,
+		messageStoreMessageIndex, js.ValueOf(msgIDStr))
+	if err != nil {
+		jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr,
+			"Failed to get message by index: %+v", err))
+		return 0
+	}
+
+	currentMsg := utils.JsToJson(currentMsgObj)
+	uuid, err := w.updateMessage(currentMsg, &messageID, timestamp,
+		round, pinned, hidden, status)
+	if err != nil {
+		jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr,
+			"Unable to updateMessage: %+v", err))
+	}
+	return uuid
+}
+
+// UpdateFromUUID is called whenever a message at the UUID is modified.
 //
-// TODO: Potential race condition due to separate get/update operations.
-func (w *wasmModel) UpdateSentStatus(uuid uint64,
-	messageID cryptoChannel.MessageID, timestamp time.Time, round rounds.Round,
-	status channels.SentStatus) {
-	parentErr := errors.New("failed to UpdateSentStatus")
+// messageID, timestamp, round, pinned, and hidden are all nillable and may be
+// updated based upon the UUID at a later date. If a nil value is passed, then
+// make no update.
+func (w *wasmModel) UpdateFromUUID(uuid uint64, messageID *message.ID,
+	timestamp *time.Time, round *rounds.Round, pinned, hidden *bool,
+	status *channels.SentStatus) {
+	parentErr := errors.New("failed to UpdateFromUUID")
 
 	// FIXME: this is a bit of race condition without the mux.
 	//        This should be done via the transactions (i.e., make a
@@ -293,36 +320,58 @@ func (w *wasmModel) UpdateSentStatus(uuid uint64,
 		return
 	}
 
-	// Extract the existing Message and update the Status
-	newMessage := &Message{}
-	err = json.Unmarshal([]byte(utils.JsToJson(currentMsg)), newMessage)
+	_, err = w.updateMessage(utils.JsToJson(currentMsg), messageID, timestamp,
+		round, pinned, hidden, status)
 	if err != nil {
 		jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr,
-			"Could not JSON unmarshal message: %+v", err))
-		return
+			"Unable to updateMessage: %+v", err))
+	}
+}
+
+// updateMessage is a helper for updating a stored message.
+func (w *wasmModel) updateMessage(currentMsgJson string, messageID *message.ID,
+	timestamp *time.Time, round *rounds.Round, pinned, hidden *bool,
+	status *channels.SentStatus) (uint64, error) {
+
+	newMessage := &Message{}
+	err := json.Unmarshal([]byte(currentMsgJson), newMessage)
+	if err != nil {
+		return 0, err
 	}
 
-	newMessage.Status = uint8(status)
-	if !messageID.Equals(cryptoChannel.MessageID{}) {
+	if status != nil {
+		newMessage.Status = uint8(*status)
+	}
+	if messageID != nil {
 		newMessage.MessageID = messageID.Bytes()
 	}
 
-	if round.ID != 0 {
+	if round != nil {
 		newMessage.Round = uint64(round.ID)
 	}
 
-	if !timestamp.Equal(time.Time{}) {
-		newMessage.Timestamp = timestamp
+	if timestamp != nil {
+		newMessage.Timestamp = *timestamp
+	}
+
+	if pinned != nil {
+		newMessage.Pinned = *pinned
+	}
+
+	if hidden != nil {
+		newMessage.Hidden = *hidden
 	}
 
 	// Store the updated Message
-	_, err = w.receiveHelper(newMessage, true)
+	uuid, err := w.receiveHelper(newMessage, true)
 	if err != nil {
-		jww.ERROR.Printf("%+v", errors.Wrap(parentErr, err.Error()))
+		return 0, err
 	}
 	channelID := &id.ID{}
 	copy(channelID[:], newMessage.ChannelID)
 	go w.receivedMessageCB(uuid, channelID, true)
+
+	return uuid, nil
 }
 
 // buildMessage is a private helper that converts typical [channels.EventModel]
@@ -332,8 +381,9 @@ func (w *wasmModel) UpdateSentStatus(uuid uint64,
 // 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, codeset uint8, timestamp time.Time,
-	lease time.Duration, round id.Round, mType channels.MessageType,
+	text []byte, 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 {
 	return &Message{
 		MessageID:       messageID,
@@ -343,20 +393,21 @@ func buildMessage(channelID, messageID, parentID []byte, nickname string,
 		Timestamp:       timestamp,
 		Lease:           lease,
 		Status:          uint8(status),
-		Hidden:          false,
-		Pinned:          false,
+		Hidden:          hidden,
+		Pinned:          pinned,
 		Text:            text,
 		Type:            uint16(mType),
 		Round:           uint64(round),
 		// User Identity Info
 		Pubkey:         pubKey,
+		DmToken:        dmToken,
 		CodesetVersion: codeset,
 	}
 }
 
 // receiveHelper is a private helper for receiving any sort of message.
-func (w *wasmModel) receiveHelper(newMessage *Message, isUpdate bool) (uint64,
-	error) {
+func (w *wasmModel) receiveHelper(
+	newMessage *Message, isUpdate bool) (uint64, error) {
 	// Convert to jsObject
 	newMessageJson, err := json.Marshal(newMessage)
 	if err != nil {
@@ -374,45 +425,91 @@ func (w *wasmModel) receiveHelper(newMessage *Message, isUpdate bool) (uint64,
 	}
 
 	// Store message to database
-	addReq, err := indexedDb.Put(w.db, messageStoreName, messageObj)
-	if err != nil {
+	result, err := indexedDb.Put(w.db, messageStoreName, messageObj)
+	if err != nil && !strings.Contains(err.Error(),
+		"at least one key does not satisfy the uniqueness requirements") {
+		// Only return non-unique constraint errors so that the case
+		// below this one can be hit and handle duplicate entries properly.
 		return 0, errors.Errorf("Unable to put Message: %+v", err)
 	}
-	res, err := addReq.Result()
-	if err != nil {
-		return 0, errors.Errorf("Unable to get Message result: %+v", err)
-	}
 
 	// NOTE: Sometimes the insert fails to return an error but hits a duplicate
 	//  insert, so this fallthrough returns the UUID entry in that case.
-	if res.IsUndefined() {
-		msgID := cryptoChannel.MessageID{}
+	if result.IsUndefined() {
+		msgID := message.ID{}
 		copy(msgID[:], newMessage.MessageID)
-		uuid, errLookup := w.msgIDLookup(msgID)
-		if uuid != 0 && errLookup == nil {
-			return uuid, nil
+		msg, errLookup := w.msgIDLookup(msgID)
+		if errLookup == nil && msg.ID != 0 {
+			return msg.ID, nil
 		}
 		return 0, errors.Errorf("uuid lookup failure: %+v", err)
 	}
-	uuid := uint64(res.Int())
+	uuid := uint64(result.Int())
 	jww.DEBUG.Printf("Successfully stored message %d", uuid)
 
 	return uuid, nil
 }
 
+// GetMessage returns the message with the given [channel.MessageID].
+func (w *wasmModel) GetMessage(
+	messageID message.ID) (channels.ModelMessage, error) {
+	lookupResult, err := w.msgIDLookup(messageID)
+	if err != nil {
+		return channels.ModelMessage{}, err
+	}
+
+	var channelId *id.ID
+	if lookupResult.ChannelID != nil {
+		channelId, err = id.Unmarshal(lookupResult.ChannelID)
+		if err != nil {
+			return channels.ModelMessage{}, err
+		}
+	}
+
+	var parentMsgId message.ID
+	if lookupResult.ParentMessageID != nil {
+		parentMsgId, err = message.UnmarshalID(lookupResult.ParentMessageID)
+		if err != nil {
+			return channels.ModelMessage{}, err
+		}
+	}
+
+	return channels.ModelMessage{
+		UUID:            lookupResult.ID,
+		Nickname:        lookupResult.Nickname,
+		MessageID:       messageID,
+		ChannelID:       channelId,
+		ParentMessageID: parentMsgId,
+		Timestamp:       lookupResult.Timestamp,
+		Lease:           lookupResult.Lease,
+		Status:          channels.SentStatus(lookupResult.Status),
+		Hidden:          lookupResult.Hidden,
+		Pinned:          lookupResult.Pinned,
+		Content:         lookupResult.Text,
+		Type:            channels.MessageType(lookupResult.Type),
+		Round:           id.Round(lookupResult.Round),
+		PubKey:          lookupResult.Pubkey,
+		CodesetVersion:  lookupResult.CodesetVersion,
+	}, nil
+}
+
 // msgIDLookup gets the UUID of the Message with the given messageID.
-func (w *wasmModel) msgIDLookup(messageID cryptoChannel.MessageID) (uint64,
-	error) {
+func (w *wasmModel) msgIDLookup(messageID message.ID) (*Message, error) {
 	msgIDStr := js.ValueOf(base64.StdEncoding.EncodeToString(messageID.Bytes()))
 	resultObj, err := indexedDb.GetIndex(w.db, messageStoreName,
 		messageStoreMessageIndex, msgIDStr)
 	if err != nil {
-		return 0, err
+		return nil, err
+	} else if resultObj.IsUndefined() {
+		return nil, errors.Errorf("no message for %s found", msgIDStr)
 	}
 
-	uuid := uint64(0)
-	if !resultObj.IsUndefined() {
-		uuid = uint64(resultObj.Get("id").Int())
+	// Process result into string
+	resultMsg := &Message{}
+	err = json.Unmarshal([]byte(utils.JsToJson(resultObj)), resultMsg)
+	if err != nil {
+		return nil, err
 	}
-	return uuid, nil
+	return resultMsg, nil
+
 }
diff --git a/indexedDb/channels/implementation_test.go b/indexedDb/channels/implementation_test.go
index 27f33649eac4d02514a09b6e2117fb148c6b764e..79a02de774ae701b6b579d356690f931f7a53457 100644
--- a/indexedDb/channels/implementation_test.go
+++ b/indexedDb/channels/implementation_test.go
@@ -13,6 +13,7 @@ import (
 	"encoding/json"
 	"fmt"
 	"github.com/hack-pad/go-indexeddb/idb"
+	"gitlab.com/elixxir/crypto/message"
 	"gitlab.com/elixxir/xxdk-wasm/indexedDb"
 	"gitlab.com/elixxir/xxdk-wasm/storage"
 	"gitlab.com/xx_network/crypto/csprng"
@@ -26,7 +27,6 @@ import (
 	"gitlab.com/elixxir/client/v4/channels"
 	"gitlab.com/elixxir/client/v4/cmix/rounds"
 	cryptoBroadcast "gitlab.com/elixxir/crypto/broadcast"
-	"gitlab.com/elixxir/crypto/channel"
 	cryptoChannel "gitlab.com/elixxir/crypto/channel"
 	"gitlab.com/xx_network/primitives/id"
 )
@@ -40,59 +40,107 @@ func dummyCallback(uint64, *id.ID, bool) {}
 
 // Happy path, insert message and look it up
 func TestWasmModel_msgIDLookup(t *testing.T) {
-	cipher, err := cryptoChannel.NewCipher([]byte("testpass"), []byte("testsalt"), 128, csprng.NewSystemRNG())
+	cipher, err := cryptoChannel.NewCipher(
+		[]byte("testpass"), []byte("testsalt"), 128, csprng.NewSystemRNG())
 	if err != nil {
 		t.Fatalf("Failed to create cipher")
 	}
 	for _, c := range []cryptoChannel.Cipher{nil, cipher} {
 		cs := ""
-		if cipher != nil {
+		if c != nil {
 			cs = "_withCipher"
 		}
 		t.Run(fmt.Sprintf("TestWasmModel_msgIDLookup%s", cs), func(t *testing.T) {
 
 			storage.GetLocalStorage().Clear()
-			testString := "test"
-			testMsgId := channel.MakeMessageID([]byte(testString), &id.ID{1})
+			testString := "TestWasmModel_msgIDLookup" + cs
+			testMsgId := message.DeriveChannelMessageID(&id.ID{1}, 0, []byte(testString))
+
 			eventModel, err := newWASMModel(testString, c, dummyCallback)
 			if err != nil {
 				t.Fatalf("%+v", err)
 			}
 
 			testMsg := buildMessage([]byte(testString), testMsgId.Bytes(), nil,
-				testString, []byte(testString), []byte{8, 6, 7, 5}, 0, netTime.Now(),
-				time.Second, 0, 0, channels.Sent)
+				testString, []byte(testString), []byte{8, 6, 7, 5}, 0, 0,
+				netTime.Now(), time.Second, 0, 0, false, false, channels.Sent)
 			_, err = eventModel.receiveHelper(testMsg, false)
 			if err != nil {
 				t.Fatalf("%+v", err)
 			}
 
-			uuid, err := eventModel.msgIDLookup(testMsgId)
+			msg, err := eventModel.msgIDLookup(testMsgId)
 			if err != nil {
 				t.Fatalf("%+v", err)
 			}
-			if uuid == 0 {
+			if msg.ID == 0 {
 				t.Fatalf("Expected to get a UUID!")
 			}
 		})
 	}
 }
 
+// Happy path, insert message and delete it
+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, dummyCallback)
+	if err != nil {
+		t.Fatalf("%+v", err)
+	}
+
+	// Insert a message
+	testMsg := buildMessage([]byte(testString), testMsgId.Bytes(), nil,
+		testString, []byte(testString), []byte{8, 6, 7, 5}, 0, 0, netTime.Now(),
+		time.Second, 0, 0, false, false, channels.Sent)
+	_, err = eventModel.receiveHelper(testMsg, false)
+	if err != nil {
+		t.Fatalf("%+v", err)
+	}
+
+	// Check the resulting status
+	results, err := indexedDb.Dump(eventModel.db, messageStoreName)
+	if err != nil {
+		t.Fatalf("%+v", err)
+	}
+	if len(results) != 1 {
+		t.Fatalf("Expected 1 message to exist")
+	}
+
+	// Delete the message
+	err = eventModel.DeleteMessage(testMsgId)
+	if err != nil {
+		t.Fatalf("%+v", err)
+	}
+
+	// Check the resulting status
+	results, err = indexedDb.Dump(eventModel.db, messageStoreName)
+	if err != nil {
+		t.Fatalf("%+v", err)
+	}
+	if len(results) != 0 {
+		t.Fatalf("Expected no messages to exist")
+	}
+}
+
 // Test wasmModel.UpdateSentStatus happy path and ensure fields don't change.
 func Test_wasmModel_UpdateSentStatus(t *testing.T) {
-	cipher, err := cryptoChannel.NewCipher([]byte("testpass"), []byte("testsalt"), 128, csprng.NewSystemRNG())
+	cipher, err := cryptoChannel.NewCipher(
+		[]byte("testpass"), []byte("testsalt"), 128, csprng.NewSystemRNG())
 	if err != nil {
 		t.Fatalf("Failed to create cipher")
 	}
 	for _, c := range []cryptoChannel.Cipher{nil, cipher} {
 		cs := ""
-		if cipher != nil {
+		if c != nil {
 			cs = "_withCipher"
 		}
-		t.Run(fmt.Sprintf("Test_wasmModel_UpdateSentStatus%s", cs), func(t *testing.T) {
+		t.Run("Test_wasmModel_UpdateSentStatus"+cs, func(t *testing.T) {
 			storage.GetLocalStorage().Clear()
-			testString := "test"
-			testMsgId := channel.MakeMessageID([]byte(testString), &id.ID{1})
+			testString := "Test_wasmModel_UpdateSentStatus" + cs
+			testMsgId := message.DeriveChannelMessageID(
+				&id.ID{1}, 0, []byte(testString))
 			eventModel, err := newWASMModel(testString, c, dummyCallback)
 			if err != nil {
 				t.Fatalf("%+v", err)
@@ -100,8 +148,8 @@ func Test_wasmModel_UpdateSentStatus(t *testing.T) {
 
 			// Store a test message
 			testMsg := buildMessage([]byte(testString), testMsgId.Bytes(), nil,
-				testString, []byte(testString), []byte{8, 6, 7, 5}, 0, netTime.Now(),
-				time.Second, 0, 0, channels.Sent)
+				testString, []byte(testString), []byte{8, 6, 7, 5}, 0, 0, netTime.Now(),
+				time.Second, 0, 0, false, false, channels.Sent)
 			uuid, err := eventModel.receiveHelper(testMsg, false)
 			if err != nil {
 				t.Fatalf("%+v", err)
@@ -118,8 +166,8 @@ func Test_wasmModel_UpdateSentStatus(t *testing.T) {
 
 			// Update the sentStatus
 			expectedStatus := channels.Failed
-			eventModel.UpdateSentStatus(uuid, testMsgId, netTime.Now(),
-				rounds.Round{ID: 8675309}, expectedStatus)
+			eventModel.UpdateFromUUID(
+				uuid, nil, nil, nil, nil, nil, &expectedStatus)
 
 			// Check the resulting status
 			results, err = indexedDb.Dump(eventModel.db, messageStoreName)
@@ -148,16 +196,17 @@ 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([]byte("testpass"), []byte("testsalt"), 128, csprng.NewSystemRNG())
+	cipher, err := cryptoChannel.NewCipher(
+		[]byte("testpass"), []byte("testsalt"), 128, csprng.NewSystemRNG())
 	if err != nil {
 		t.Fatalf("Failed to create cipher")
 	}
 	for _, c := range []cryptoChannel.Cipher{nil, cipher} {
 		cs := ""
-		if cipher != nil {
+		if c != nil {
 			cs = "_withCipher"
 		}
-		t.Run(fmt.Sprintf("Test_wasmModel_JoinChannel_LeaveChannel%s", cs), func(t *testing.T) {
+		t.Run("Test_wasmModel_JoinChannel_LeaveChannel"+cs, func(t *testing.T) {
 			storage.GetLocalStorage().Clear()
 			eventModel, err := newWASMModel("test", c, dummyCallback)
 			if err != nil {
@@ -199,18 +248,19 @@ 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([]byte("testpass"), []byte("testsalt"), 128, csprng.NewSystemRNG())
+	cipher, err := cryptoChannel.NewCipher(
+		[]byte("testpass"), []byte("testsalt"), 128, csprng.NewSystemRNG())
 	if err != nil {
 		t.Fatalf("Failed to create cipher")
 	}
 	for _, c := range []cryptoChannel.Cipher{nil, cipher} {
 		cs := ""
-		if cipher != nil {
+		if c != nil {
 			cs = "_withCipher"
 		}
-		t.Run(fmt.Sprintf("Test_wasmModel_UUIDTest%s", cs), func(t *testing.T) {
+		t.Run("Test_wasmModel_UUIDTest"+cs, func(t *testing.T) {
 			storage.GetLocalStorage().Clear()
-			testString := "testHello"
+			testString := "testHello" + cs
 			eventModel, err := newWASMModel(testString, c, dummyCallback)
 			if err != nil {
 				t.Fatalf("%+v", err)
@@ -221,12 +271,12 @@ func Test_wasmModel_UUIDTest(t *testing.T) {
 			for i := 0; i < 10; i++ {
 				// Store a test message
 				channelID := id.NewIdFromBytes([]byte(testString), t)
-				msgID := channel.MessageID{}
+				msgID := message.ID{}
 				copy(msgID[:], testString+fmt.Sprintf("%d", i))
 				rnd := rounds.Round{ID: id.Round(42)}
 				uuid := eventModel.ReceiveMessage(channelID, msgID, "test",
-					testString+fmt.Sprintf("%d", i), []byte{8, 6, 7, 5}, 0,
-					netTime.Now(), time.Hour, rnd, 0, channels.Sent)
+					testString+fmt.Sprintf("%d", i), []byte{8, 6, 7, 5}, 0, 0,
+					netTime.Now(), time.Hour, rnd, 0, channels.Sent, false)
 				uuids[i] = uuid
 			}
 
@@ -244,16 +294,17 @@ 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([]byte("testpass"), []byte("testsalt"), 128, csprng.NewSystemRNG())
+	cipher, err := cryptoChannel.NewCipher(
+		[]byte("testpass"), []byte("testsalt"), 128, csprng.NewSystemRNG())
 	if err != nil {
 		t.Fatalf("Failed to create cipher")
 	}
 	for _, c := range []cryptoChannel.Cipher{nil, cipher} {
 		cs := ""
-		if cipher != nil {
+		if c != nil {
 			cs = "_withCipher"
 		}
-		t.Run(fmt.Sprintf("Test_wasmModel_DuplicateReceives%s", cs), func(t *testing.T) {
+		t.Run("Test_wasmModel_DuplicateReceives"+cs, func(t *testing.T) {
 			storage.GetLocalStorage().Clear()
 			testString := "testHello"
 			eventModel, err := newWASMModel(testString, c, dummyCallback)
@@ -263,15 +314,15 @@ func Test_wasmModel_DuplicateReceives(t *testing.T) {
 
 			uuids := make([]uint64, 10)
 
-			msgID := channel.MessageID{}
+			msgID := message.ID{}
 			copy(msgID[:], testString)
 			for i := 0; i < 10; i++ {
 				// Store a test message
 				channelID := id.NewIdFromBytes([]byte(testString), t)
 				rnd := rounds.Round{ID: id.Round(42)}
 				uuid := eventModel.ReceiveMessage(channelID, msgID, "test",
-					testString+fmt.Sprintf("%d", i), []byte{8, 6, 7, 5}, 0,
-					netTime.Now(), time.Hour, rnd, 0, channels.Sent)
+					testString+fmt.Sprintf("%d", i), []byte{8, 6, 7, 5}, 0, 0,
+					netTime.Now(), time.Hour, rnd, 0, channels.Sent, false)
 				uuids[i] = uuid
 			}
 
@@ -285,22 +336,22 @@ 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([]byte("testpass"), []byte("testsalt"), 128, csprng.NewSystemRNG())
+	cipher, err := cryptoChannel.NewCipher(
+		[]byte("testpass"), []byte("testsalt"), 128, csprng.NewSystemRNG())
 	if err != nil {
 		t.Fatalf("Failed to create cipher")
 	}
 	for _, c := range []cryptoChannel.Cipher{nil, cipher} {
 		cs := ""
-		if cipher != nil {
+		if c != nil {
 			cs = "_withCipher"
 		}
-		t.Run(fmt.Sprintf("Test_wasmModel_deleteMsgByChannel%s", cs), func(t *testing.T) {
+		t.Run("Test_wasmModel_deleteMsgByChannel"+cs, func(t *testing.T) {
 			storage.GetLocalStorage().Clear()
 			testString := "test_deleteMsgByChannel"
 			totalMessages := 10
@@ -324,10 +375,12 @@ func Test_wasmModel_deleteMsgByChannel(t *testing.T) {
 					thisChannel = keepChannel
 				}
 
-				testMsgId := channel.MakeMessageID([]byte(testStr), &id.ID{1})
-				eventModel.ReceiveMessage(thisChannel, testMsgId, testStr, testStr,
-					[]byte{8, 6, 7, 5}, 0, netTime.Now(), time.Second,
-					rounds.Round{ID: id.Round(0)}, 0, channels.Sent)
+				testMsgId := message.DeriveChannelMessageID(
+					&id.ID{byte(i)}, 0, []byte(testStr))
+				eventModel.ReceiveMessage(thisChannel, testMsgId, testStr,
+					testStr, []byte{8, 6, 7, 5}, 0, 0, netTime.Now(),
+					time.Second, rounds.Round{ID: id.Round(0)}, 0,
+					channels.Sent, false)
 			}
 
 			// Check pre-results
@@ -360,16 +413,17 @@ 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([]byte("testpass"), []byte("testsalt"), 128, csprng.NewSystemRNG())
+	cipher, err := cryptoChannel.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} {
 		cs := ""
-		if cipher != nil {
+		if c != nil {
 			cs = "_withCipher"
 		}
-		t.Run(fmt.Sprintf("TestWasmModel_receiveHelper_UniqueIndex%s", cs), func(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, dummyCallback)
@@ -398,55 +452,49 @@ func TestWasmModel_receiveHelper_UniqueIndex(t *testing.T) {
 			}
 
 			// First message insert should succeed
-			testMsgId := channel.MakeMessageID([]byte(testString), &id.ID{1})
+			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, netTime.Now(),
-				time.Second, 0, 0, channels.Sent)
-			_, err = eventModel.receiveHelper(testMsg, false)
+				testString, []byte(testString), []byte{8, 6, 7, 5}, 0, 0,
+				netTime.Now(), time.Second, 0, 0, false, false, channels.Sent)
+			uuid, err := eventModel.receiveHelper(testMsg, false)
 			if err != nil {
 				t.Fatal(err)
 			}
 
-			// The duplicate entry won't fail, but it just silently shouldn't happen
-			_, err = eventModel.receiveHelper(testMsg, false)
-			if err != nil {
-				t.Fatalf("%+v", err)
-			}
-			results, err := indexedDb.Dump(eventModel.db, messageStoreName)
+			// The duplicate entry should return the same UUID
+			duplicateUuid, err := eventModel.receiveHelper(testMsg, false)
 			if err != nil {
-				t.Fatalf("%+v", err)
+				t.Fatal(err)
 			}
-			if len(results) != 1 {
-				t.Fatalf("Expected only a single message, got %d", len(results))
+			if uuid != duplicateUuid {
+				t.Fatalf("Expected UUID %d to match %d", uuid, duplicateUuid)
 			}
 
 			// Now insert a message with a different message ID from the first
-			testMsgId2 := channel.MakeMessageID([]byte(testString), &id.ID{2})
+			testMsgId2 := message.DeriveChannelMessageID(
+				&id.ID{2}, 0, []byte(testString))
 			testMsg = buildMessage([]byte(testString), testMsgId2.Bytes(), nil,
-				testString, []byte(testString), []byte{8, 6, 7, 5}, 0, netTime.Now(),
-				time.Second, 0, 0, channels.Sent)
-			primaryKey, err := eventModel.receiveHelper(testMsg, false)
+				testString, []byte(testString), []byte{8, 6, 7, 5}, 0, 0,
+				netTime.Now(), time.Second, 0, 0, false, false, channels.Sent)
+			uuid2, err := eventModel.receiveHelper(testMsg, false)
 			if err != nil {
 				t.Fatal(err)
 			}
+			if uuid2 == uuid {
+				t.Fatalf("Expected UUID %d to NOT match %d", uuid, duplicateUuid)
+			}
 
 			// Except this time, we update the second entry to have the same
 			// message ID as the first
-			testMsg.ID = primaryKey
+			testMsg.ID = uuid
 			testMsg.MessageID = testMsgId.Bytes()
-			_, err = eventModel.receiveHelper(testMsg, true)
+			duplicateUuid2, err := eventModel.receiveHelper(testMsg, true)
 			if err != nil {
 				t.Fatal(err)
 			}
-
-			// The update to duplicate message ID won't fail,
-			// but it just silently shouldn't happen
-			results, err = indexedDb.Dump(eventModel.db, messageStoreName)
-			if err != nil {
-				t.Fatalf("%+v", err)
+			if duplicateUuid2 != duplicateUuid {
+				t.Fatalf("Expected UUID %d to match %d", uuid, duplicateUuid)
 			}
-			// TODO: Convert JSON to Message, ensure Message ID fields differ
-
 		})
 	}
 }
diff --git a/indexedDb/channels/model.go b/indexedDb/channels/model.go
index 02a3ebd42479a26cffa7e758c8d317a21a906099..078d6bd67f7c43d9e50373b7801f69937cfce2ac 100644
--- a/indexedDb/channels/model.go
+++ b/indexedDb/channels/model.go
@@ -61,6 +61,7 @@ type Message struct {
 
 	// User cryptographic Identity struct -- could be pulled out
 	Pubkey         []byte `json:"pubkey"`
+	DmToken        uint32 `json:"dm_token"`
 	CodesetVersion uint8  `json:"codeset_version"`
 }
 
diff --git a/indexedDb/dm/implementation.go b/indexedDb/dm/implementation.go
new file mode 100644
index 0000000000000000000000000000000000000000..afd4e3bff44d739d7a8d3d687563c390fde7f743
--- /dev/null
+++ b/indexedDb/dm/implementation.go
@@ -0,0 +1,379 @@
+////////////////////////////////////////////////////////////////////////////////
+// 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 channelEventModel
+
+import (
+	"crypto/ed25519"
+	"encoding/json"
+	"strings"
+	"sync"
+	"syscall/js"
+	"time"
+
+	"github.com/pkg/errors"
+	jww "github.com/spf13/jwalterweatherman"
+	"gitlab.com/elixxir/client/v4/cmix/rounds"
+	"gitlab.com/elixxir/client/v4/dm"
+	"gitlab.com/elixxir/xxdk-wasm/indexedDb"
+	"gitlab.com/elixxir/xxdk-wasm/utils"
+	"gitlab.com/xx_network/primitives/id"
+
+	"github.com/hack-pad/go-indexeddb/idb"
+	cryptoChannel "gitlab.com/elixxir/crypto/channel"
+	"gitlab.com/elixxir/crypto/message"
+)
+
+// wasmModel implements [dm.Receiver] interface, which uses the channels
+// system passed an object that adheres to in order to get events on the
+// channel.
+type wasmModel struct {
+	db                *idb.Database
+	cipher            cryptoChannel.Cipher
+	receivedMessageCB MessageReceivedCallback
+	updateMux         sync.Mutex
+}
+
+// joinConversation is used for joining new conversations.
+func (w *wasmModel) joinConversation(nickname string,
+	pubKey ed25519.PublicKey, dmToken uint32, codeset uint8) error {
+	parentErr := errors.New("failed to joinConversation")
+
+	// Build object
+	newConvo := Conversation{
+		Pubkey:         pubKey,
+		Nickname:       nickname,
+		Token:          dmToken,
+		CodesetVersion: codeset,
+		Blocked:        false,
+	}
+
+	// Convert to jsObject
+	newConvoJson, err := json.Marshal(&newConvo)
+	if err != nil {
+		return errors.WithMessagef(parentErr,
+			"Unable to marshal Conversation: %+v", err)
+	}
+	convoObj, err := utils.JsonToJS(newConvoJson)
+	if err != nil {
+		return errors.WithMessagef(parentErr,
+			"Unable to marshal Conversation: %+v", err)
+	}
+
+	_, err = indexedDb.Put(w.db, conversationStoreName, convoObj)
+	if err != nil {
+		return errors.WithMessagef(parentErr,
+			"Unable to put Conversation: %+v", err)
+	}
+	return nil
+}
+
+// buildMessage is a private helper that converts typical [dm.Receiver]
+// inputs into a basic Message structure for insertion into storage.
+//
+// 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 []byte, text []byte,
+	pubKey ed25519.PublicKey, timestamp time.Time, round id.Round,
+	mType dm.MessageType, status dm.Status) *Message {
+	return &Message{
+		MessageID:          messageID,
+		ConversationPubKey: pubKey,
+		ParentMessageID:    parentID,
+		Timestamp:          timestamp,
+		Status:             uint8(status),
+		Text:               text,
+		Type:               uint16(mType),
+		Round:              uint64(round),
+	}
+}
+
+func (w *wasmModel) Receive(messageID message.ID, nickname string, text []byte,
+	pubKey ed25519.PublicKey, dmToken uint32, codeset uint8,
+	timestamp time.Time, round rounds.Round, mType dm.MessageType,
+	status dm.Status) uint64 {
+	parentErr := errors.New("failed to Receive")
+
+	// If there is no extant Conversation, create one.
+	_, err := indexedDb.Get(w.db, conversationStoreName, utils.CopyBytesToJS(pubKey))
+	if err != nil {
+		if strings.Contains(err.Error(), indexedDb.ErrDoesNotExist) {
+			err = w.joinConversation(nickname, pubKey, dmToken, codeset)
+			if err != nil {
+				jww.ERROR.Printf("%+v", err)
+			}
+		} else {
+			jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr,
+				"Unable to get Conversation: %+v", err))
+		}
+		return 0
+	} else {
+		jww.DEBUG.Printf("Conversation with %s already joined", nickname)
+	}
+
+	// Handle encryption, if it is present
+	if w.cipher != nil {
+		text, err = w.cipher.Encrypt(text)
+		if err != nil {
+			jww.ERROR.Printf("Failed to encrypt Message: %+v", err)
+			return 0
+		}
+	}
+
+	msgToInsert := buildMessage(messageID.Bytes(), nil, text,
+		pubKey, timestamp, round.ID, mType, status)
+	uuid, err := w.receiveHelper(msgToInsert, false)
+	if err != nil {
+		jww.ERROR.Printf("Failed to receive Message: %+v", err)
+	}
+
+	go w.receivedMessageCB(uuid, pubKey, false)
+	return uuid
+}
+
+func (w *wasmModel) ReceiveText(messageID message.ID, nickname, text string,
+	pubKey ed25519.PublicKey, dmToken uint32, codeset uint8,
+	timestamp time.Time, round rounds.Round, status dm.Status) uint64 {
+	parentErr := errors.New("failed to ReceiveText")
+
+	// If there is no extant Conversation, create one.
+	_, err := indexedDb.Get(w.db, conversationStoreName, utils.CopyBytesToJS(pubKey))
+	if err != nil {
+		if strings.Contains(err.Error(), indexedDb.ErrDoesNotExist) {
+			err = w.joinConversation(nickname, pubKey, dmToken, codeset)
+			if err != nil {
+				jww.ERROR.Printf("%+v", err)
+			}
+		} else {
+			jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr,
+				"Unable to get Conversation: %+v", err))
+		}
+		return 0
+	} else {
+		jww.DEBUG.Printf("Conversation with %s already joined", nickname)
+	}
+
+	// Handle encryption, if it is present
+	textBytes := []byte(text)
+	if w.cipher != nil {
+		textBytes, err = w.cipher.Encrypt(textBytes)
+		if err != nil {
+			jww.ERROR.Printf("Failed to encrypt Message: %+v", err)
+			return 0
+		}
+	}
+
+	msgToInsert := buildMessage(messageID.Bytes(), nil, textBytes,
+		pubKey, timestamp, round.ID, dm.TextType, status)
+
+	uuid, err := w.receiveHelper(msgToInsert, false)
+	if err != nil {
+		jww.ERROR.Printf("Failed to receive Message: %+v", err)
+	}
+
+	go w.receivedMessageCB(uuid, pubKey, false)
+	return uuid
+}
+
+func (w *wasmModel) ReceiveReply(messageID, reactionTo message.ID, nickname,
+	text string, pubKey ed25519.PublicKey, dmToken uint32, codeset uint8,
+	timestamp time.Time, round rounds.Round, status dm.Status) uint64 {
+	parentErr := errors.New("failed to ReceiveReply")
+
+	// If there is no extant Conversation, create one.
+	_, err := indexedDb.Get(w.db, conversationStoreName, utils.CopyBytesToJS(pubKey))
+	if err != nil {
+		if strings.Contains(err.Error(), indexedDb.ErrDoesNotExist) {
+			err = w.joinConversation(nickname, pubKey, dmToken, codeset)
+			if err != nil {
+				jww.ERROR.Printf("%+v", err)
+			}
+		} else {
+			jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr,
+				"Unable to get Conversation: %+v", err))
+		}
+		return 0
+	} else {
+		jww.DEBUG.Printf("Conversation with %s already joined", nickname)
+	}
+
+	// Handle encryption, if it is present
+	textBytes := []byte(text)
+	if w.cipher != nil {
+		textBytes, err = w.cipher.Encrypt(textBytes)
+		if err != nil {
+			jww.ERROR.Printf("Failed to encrypt Message: %+v", err)
+			return 0
+		}
+	}
+
+	msgToInsert := buildMessage(messageID.Bytes(), reactionTo.Marshal(), textBytes,
+		pubKey, timestamp, round.ID, dm.TextType, status)
+
+	uuid, err := w.receiveHelper(msgToInsert, false)
+	if err != nil {
+		jww.ERROR.Printf("Failed to receive Message: %+v", err)
+	}
+
+	go w.receivedMessageCB(uuid, pubKey, false)
+	return uuid
+}
+
+func (w *wasmModel) ReceiveReaction(messageID, reactionTo message.ID, nickname,
+	reaction string, pubKey ed25519.PublicKey, dmToken uint32, codeset uint8,
+	timestamp time.Time, round rounds.Round, status dm.Status) uint64 {
+	parentErr := errors.New("failed to ReceiveText")
+
+	// If there is no extant Conversation, create one.
+	_, err := indexedDb.Get(w.db, conversationStoreName, utils.CopyBytesToJS(pubKey))
+	if err != nil {
+		if strings.Contains(err.Error(), indexedDb.ErrDoesNotExist) {
+			err = w.joinConversation(nickname, pubKey, dmToken, codeset)
+			if err != nil {
+				jww.ERROR.Printf("%+v", err)
+			}
+		} else {
+			jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr,
+				"Unable to get Conversation: %+v", err))
+		}
+		return 0
+	} else {
+		jww.DEBUG.Printf("Conversation with %s already joined", nickname)
+	}
+
+	// Handle encryption, if it is present
+	textBytes := []byte(reaction)
+	if w.cipher != nil {
+		textBytes, err = w.cipher.Encrypt(textBytes)
+		if err != nil {
+			jww.ERROR.Printf("Failed to encrypt Message: %+v", err)
+			return 0
+		}
+	}
+
+	msgToInsert := buildMessage(messageID.Bytes(), nil, textBytes,
+		pubKey, timestamp, round.ID, dm.ReactionType, status)
+
+	uuid, err := w.receiveHelper(msgToInsert, false)
+	if err != nil {
+		jww.ERROR.Printf("Failed to receive Message: %+v", err)
+	}
+
+	go w.receivedMessageCB(uuid, pubKey, false)
+	return uuid
+}
+
+func (w *wasmModel) UpdateSentStatus(uuid uint64, messageID message.ID,
+	timestamp time.Time, round rounds.Round, status dm.Status) {
+	parentErr := errors.New("failed to UpdateSentStatus")
+
+	// FIXME: this is a bit of race condition without the mux.
+	//        This should be done via the transactions (i.e., make a
+	//        special version of receiveHelper)
+	w.updateMux.Lock()
+	defer w.updateMux.Unlock()
+
+	// Convert messageID to the key generated by json.Marshal
+	key := js.ValueOf(uuid)
+
+	// Use the key to get the existing Message
+	currentMsg, err := indexedDb.Get(w.db, messageStoreName, key)
+	if err != nil {
+		jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr,
+			"Unable to get message: %+v", err))
+		return
+	}
+
+	// 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.Status = uint8(status)
+	if !messageID.Equals(message.ID{}) {
+		newMessage.MessageID = messageID.Bytes()
+	}
+
+	if round.ID != 0 {
+		newMessage.Round = uint64(round.ID)
+	}
+
+	if !timestamp.Equal(time.Time{}) {
+		newMessage.Timestamp = timestamp
+	}
+
+	// Store the updated Message
+	_, err = w.receiveHelper(newMessage, true)
+	if err != nil {
+		jww.ERROR.Printf("%+v", errors.Wrap(parentErr, err.Error()))
+	}
+	go w.receivedMessageCB(uuid, newMessage.ConversationPubKey, true)
+}
+
+// receiveHelper is a private helper for receiving any sort of message.
+func (w *wasmModel) receiveHelper(
+	newMessage *Message, isUpdate bool) (uint64, error) {
+	// Convert to jsObject
+	newMessageJson, err := json.Marshal(newMessage)
+	if err != nil {
+		return 0, errors.Errorf("Unable to marshal Message: %+v", err)
+	}
+	messageObj, err := utils.JsonToJS(newMessageJson)
+	if err != nil {
+		return 0, errors.Errorf("Unable to marshal Message: %+v", err)
+	}
+
+	// Unset the primaryKey for inserts so that it can be auto-populated and
+	// incremented
+	if !isUpdate {
+		messageObj.Delete("id")
+	}
+
+	// Store message to database
+	result, err := indexedDb.Put(w.db, messageStoreName, messageObj)
+	if err != nil {
+		return 0, errors.Errorf("Unable to put Message: %+v", err)
+	}
+
+	// NOTE: Sometimes the insert fails to return an error but hits a duplicate
+	//  insert, so this fallthrough returns the UUID entry in that case.
+	if result.IsUndefined() {
+		msgID := message.ID{}
+		copy(msgID[:], newMessage.MessageID)
+		uuid, errLookup := w.msgIDLookup(msgID)
+		if uuid != 0 && errLookup == nil {
+			return uuid, nil
+		}
+		return 0, errors.Errorf("uuid lookup failure: %+v", err)
+	}
+	uuid := uint64(result.Int())
+	jww.DEBUG.Printf("Successfully stored message %d", uuid)
+
+	return uuid, nil
+}
+
+// msgIDLookup gets the UUID of the Message with the given messageID.
+func (w *wasmModel) msgIDLookup(messageID message.ID) (uint64, error) {
+	resultObj, err := indexedDb.GetIndex(w.db, messageStoreName,
+		messageStoreMessageIndex, utils.CopyBytesToJS(messageID.Marshal()))
+	if err != nil {
+		return 0, err
+	}
+
+	uuid := uint64(0)
+	if !resultObj.IsUndefined() {
+		uuid = uint64(resultObj.Get("id").Int())
+	}
+	return uuid, nil
+}
diff --git a/indexedDb/dm/init.go b/indexedDb/dm/init.go
new file mode 100644
index 0000000000000000000000000000000000000000..380df647c810d7fb6239cee9c4e7c1fc4181e4ca
--- /dev/null
+++ b/indexedDb/dm/init.go
@@ -0,0 +1,182 @@
+////////////////////////////////////////////////////////////////////////////////
+// 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 channelEventModel
+
+import (
+	"crypto/ed25519"
+	"syscall/js"
+
+	"github.com/hack-pad/go-indexeddb/idb"
+	"github.com/pkg/errors"
+	jww "github.com/spf13/jwalterweatherman"
+	"gitlab.com/elixxir/client/v4/dm"
+	cryptoChannel "gitlab.com/elixxir/crypto/channel"
+	"gitlab.com/elixxir/xxdk-wasm/indexedDb"
+	"gitlab.com/elixxir/xxdk-wasm/storage"
+)
+
+const (
+	// databaseSuffix is the suffix to be appended to the name of
+	// the database.
+	databaseSuffix = "_speakeasy_dm"
+
+	// currentVersion is the current version of the IndexDb
+	// runtime. Used for migration purposes.
+	currentVersion uint = 1
+)
+
+// 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, pubKey ed25519.PublicKey, update bool)
+
+// NewWASMEventModel returns a [channels.EventModel] backed by a wasmModel.
+// The name should be a base64 encoding of the users public key.
+func NewWASMEventModel(path string, encryption cryptoChannel.Cipher,
+	cb MessageReceivedCallback) (dm.EventModel, error) {
+	databaseName := path + databaseSuffix
+	return newWASMModel(databaseName, encryption, cb)
+}
+
+// newWASMModel creates the given [idb.Database] and returns a wasmModel.
+func newWASMModel(databaseName string, encryption cryptoChannel.Cipher,
+	cb MessageReceivedCallback) (*wasmModel, error) {
+	// Attempt to open database object
+	ctx, cancel := indexedDb.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)
+				return nil
+			}
+
+			jww.INFO.Printf("IndexDb upgrade required: v%d -> v%d",
+				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
+	}
+
+	// Save the encryption status to storage
+	encryptionStatus := encryption != nil
+	loadedEncryptionStatus, err := storage.StoreIndexedDbEncryptionStatus(
+		databaseName, encryptionStatus)
+	if err != nil {
+		return nil, err
+	}
+
+	// Verify encryption status does not change
+	if encryptionStatus != loadedEncryptionStatus {
+		return nil, errors.New(
+			"Cannot load database with different encryption status.")
+	} else if !encryptionStatus {
+		jww.WARN.Printf("IndexedDb encryption disabled!")
+	}
+
+	// Attempt to ensure the database has been properly initialized
+	openRequest, err = idb.Global().Open(ctx, databaseName, currentVersion,
+		func(db *idb.Database, oldVersion, newVersion uint) error {
+			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
+	}
+	wrapper := &wasmModel{db: db, receivedMessageCB: cb, cipher: encryption}
+
+	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 {
+	indexOpts := idb.IndexOptions{
+		Unique:     false,
+		MultiEntry: false,
+	}
+
+	// Build Message ObjectStore and Indexes
+	messageStoreOpts := idb.ObjectStoreOptions{
+		KeyPath:       js.ValueOf(msgPkeyName),
+		AutoIncrement: true,
+	}
+	messageStore, err := db.CreateObjectStore(messageStoreName, messageStoreOpts)
+	if err != nil {
+		return err
+	}
+	_, err = messageStore.CreateIndex(messageStoreMessageIndex,
+		js.ValueOf(messageStoreMessage),
+		idb.IndexOptions{
+			Unique:     true,
+			MultiEntry: false,
+		})
+	if err != nil {
+		return err
+	}
+	_, err = messageStore.CreateIndex(messageStoreConversationIndex,
+		js.ValueOf(messageStoreConversation), indexOpts)
+	if err != nil {
+		return err
+	}
+	_, err = messageStore.CreateIndex(messageStoreParentIndex,
+		js.ValueOf(messageStoreParent), indexOpts)
+	if err != nil {
+		return err
+	}
+	_, err = messageStore.CreateIndex(messageStoreTimestampIndex,
+		js.ValueOf(messageStoreTimestamp), indexOpts)
+	if err != nil {
+		return err
+	}
+
+	// Build Channel ObjectStore
+	conversationStoreOpts := idb.ObjectStoreOptions{
+		KeyPath:       js.ValueOf(convoPkeyName),
+		AutoIncrement: false,
+	}
+	_, err = db.CreateObjectStore(conversationStoreName, conversationStoreOpts)
+	if err != nil {
+		return err
+	}
+
+	// Get the database name and save it to storage
+	if databaseName, err := db.Name(); err != nil {
+		return err
+	} else if err = storage.StoreIndexedDb(databaseName); err != nil {
+		return err
+	}
+
+	return nil
+}
diff --git a/indexedDb/dm/model.go b/indexedDb/dm/model.go
new file mode 100644
index 0000000000000000000000000000000000000000..bb6f34588aa2976c1689b629c6df31219de6b585
--- /dev/null
+++ b/indexedDb/dm/model.go
@@ -0,0 +1,63 @@
+////////////////////////////////////////////////////////////////////////////////
+// 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 channelEventModel
+
+import (
+	"time"
+)
+
+const (
+	// Text representation of primary key value (keyPath).
+	msgPkeyName   = "id"
+	convoPkeyName = "pub_key"
+
+	// Text representation of the names of the various [idb.ObjectStore].
+	messageStoreName      = "messages"
+	conversationStoreName = "conversations"
+
+	// Message index names.
+	messageStoreMessageIndex      = "message_id_index"
+	messageStoreConversationIndex = "conversation_id_index"
+	messageStoreParentIndex       = "parent_message_id_index"
+	messageStoreTimestampIndex    = "timestamp_index"
+
+	// Message keyPath names (must match json struct tags).
+	messageStoreMessage      = "message_id"
+	messageStoreConversation = "conversation_id"
+	messageStoreParent       = "parent_message_id"
+	messageStoreTimestamp    = "timestamp"
+)
+
+// Message defines the IndexedDb representation of a single Message.
+//
+// A Message belongs to one Conversation.
+// A Message may belong to one Message (Parent).
+type Message struct {
+	ID                 uint64    `json:"id"`                   // Matches msgPkeyName
+	MessageID          []byte    `json:"message_id"`           // Index
+	ConversationPubKey []byte    `json:"conversation_pub_key"` // Index
+	ParentMessageID    []byte    `json:"parent_message_id"`    // Index
+	Timestamp          time.Time `json:"timestamp"`            // Index
+	Status             uint8     `json:"status"`
+	Text               []byte    `json:"text"`
+	Type               uint16    `json:"type"`
+	Round              uint64    `json:"round"`
+}
+
+// Conversation defines the IndexedDb representation of a single
+// 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"`
+}
diff --git a/indexedDb/utils.go b/indexedDb/utils.go
index 1aa6ff45805c1c65b9ad27e51a8b0590e7b8201d..a5bef78685ef34452aca396e1aea3f5206cf4ab6 100644
--- a/indexedDb/utils.go
+++ b/indexedDb/utils.go
@@ -22,9 +22,13 @@ import (
 	"time"
 )
 
-// dbTimeout is the global timeout for operations with the storage
-// [context.Context].
-const dbTimeout = time.Second
+const (
+	// dbTimeout is the global timeout for operations with the storage
+	// [context.Context].
+	dbTimeout = time.Second
+	// ErrDoesNotExist is an error string for got undefined on Get operations.
+	ErrDoesNotExist = "result is undefined"
+)
 
 // NewContext builds a context for indexedDb operations.
 func NewContext() (context.Context, context.CancelFunc) {
@@ -32,6 +36,7 @@ func NewContext() (context.Context, context.CancelFunc) {
 }
 
 // Get is a generic helper for getting values from the given [idb.ObjectStore].
+// Only usable by primary key.
 func Get(db *idb.Database, objectStoreName string, key js.Value) (js.Value, error) {
 	parentErr := errors.Errorf("failed to Get %s/%s", objectStoreName, key)
 
@@ -62,8 +67,8 @@ func Get(db *idb.Database, objectStoreName string, key js.Value) (js.Value, erro
 		return js.Undefined(), errors.WithMessagef(parentErr,
 			"Unable to get from ObjectStore: %+v", err)
 	} else if resultObj.IsUndefined() {
-		return js.Undefined(), errors.WithMessage(parentErr,
-			"Unable to get from ObjectStore: result is undefined")
+		return js.Undefined(), errors.WithMessagef(parentErr,
+			"Unable to get from ObjectStore: %s", ErrDoesNotExist)
 	}
 
 	// Process result into string
@@ -74,7 +79,7 @@ func Get(db *idb.Database, objectStoreName string, key js.Value) (js.Value, erro
 
 // GetIndex is a generic helper for getting values from the given
 // [idb.ObjectStore] using the given [idb.Index].
-func GetIndex(db *idb.Database, objectStoreName string,
+func GetIndex(db *idb.Database, objectStoreName,
 	indexName string, key js.Value) (js.Value, error) {
 	parentErr := errors.Errorf("failed to GetIndex %s/%s/%s",
 		objectStoreName, indexName, key)
@@ -111,8 +116,8 @@ func GetIndex(db *idb.Database, objectStoreName string,
 		return js.Undefined(), errors.WithMessagef(parentErr,
 			"Unable to get from ObjectStore: %+v", err)
 	} else if resultObj.IsUndefined() {
-		return js.Undefined(), errors.WithMessage(parentErr,
-			"Unable to get from ObjectStore: result is undefined")
+		return js.Undefined(), errors.WithMessagef(parentErr,
+			"Unable to get from ObjectStore: %s", ErrDoesNotExist)
 	}
 
 	// Process result into string
@@ -123,41 +128,42 @@ func GetIndex(db *idb.Database, objectStoreName string,
 
 // Put is a generic helper for putting values into the given [idb.ObjectStore].
 // Equivalent to insert if not exists else update.
-func Put(db *idb.Database, objectStoreName string, value js.Value) (*idb.Request, error) {
+func Put(db *idb.Database, objectStoreName string, value js.Value) (js.Value, error) {
 	// Prepare the Transaction
 	txn, err := db.Transaction(idb.TransactionReadWrite, objectStoreName)
 	if err != nil {
-		return nil, errors.Errorf("Unable to create Transaction: %+v", err)
+		return js.Undefined(), errors.Errorf("Unable to create Transaction: %+v", err)
 	}
 	store, err := txn.ObjectStore(objectStoreName)
 	if err != nil {
-		return nil, errors.Errorf("Unable to get ObjectStore: %+v", err)
+		return js.Undefined(), errors.Errorf("Unable to get ObjectStore: %+v", err)
 	}
 
 	// Perform the operation
 	request, err := store.Put(value)
 	if err != nil {
-		return nil, errors.Errorf("Unable to Put: %+v", err)
+		return js.Undefined(), errors.Errorf("Unable to Put: %+v", err)
 	}
 
 	// Wait for the operation to return
 	ctx, cancel := NewContext()
-	err = txn.Await(ctx)
+	result, err := request.Await(ctx)
 	cancel()
 	if err != nil {
-		return nil, errors.Errorf("Putting value failed: %+v", err)
+		return js.Undefined(), errors.Errorf("Putting value failed: %+v", err)
 	}
-	jww.DEBUG.Printf("Successfully put value in %s: %v",
+	jww.DEBUG.Printf("Successfully put value in %s: %s",
 		objectStoreName, utils.JsToJson(value))
-	return request, nil
+	return result, nil
 }
 
-// Delete is a generic helper for removing values from the given [idb.ObjectStore].
+// Delete is a generic helper for removing values from the given
+// [idb.ObjectStore]. Only usable by primary key.
 func Delete(db *idb.Database, objectStoreName string, key js.Value) error {
 	parentErr := errors.Errorf("failed to Delete %s/%s", objectStoreName, key)
 
 	// Prepare the Transaction
-	txn, err := db.Transaction(idb.TransactionReadOnly, objectStoreName)
+	txn, err := db.Transaction(idb.TransactionReadWrite, objectStoreName)
 	if err != nil {
 		return errors.WithMessagef(parentErr,
 			"Unable to create Transaction: %+v", err)
@@ -169,20 +175,44 @@ func Delete(db *idb.Database, objectStoreName string, key js.Value) error {
 	}
 
 	// Perform the operation
-	deleteRequest, err := store.Delete(key)
+	_, err = store.Delete(key)
 	if err != nil {
 		return errors.WithMessagef(parentErr,
-			"Unable to Get from ObjectStore: %+v", err)
+			"Unable to Delete from ObjectStore: %+v", err)
 	}
 
 	// Wait for the operation to return
 	ctx, cancel := NewContext()
-	err = deleteRequest.Await(ctx)
+	err = txn.Await(ctx)
 	cancel()
 	if err != nil {
 		return errors.WithMessagef(parentErr,
-			"Unable to delete from ObjectStore: %+v", err)
+			"Unable to Delete from ObjectStore: %+v", err)
+	}
+	jww.DEBUG.Printf("Successfully deleted value at %s/%s",
+		objectStoreName, utils.JsToJson(key))
+	return nil
+}
+
+// DeleteIndex is a generic helper for removing values from the
+// given [idb.ObjectStore] using the given [idb.Index]. Requires passing
+// in the name of the primary key for the store.
+func DeleteIndex(db *idb.Database, objectStoreName,
+	indexName, pkeyName string, key js.Value) error {
+	parentErr := errors.Errorf("failed to DeleteIndex %s/%s", objectStoreName, key)
+
+	value, err := GetIndex(db, objectStoreName, indexName, key)
+	if err != nil {
+		return errors.WithMessagef(parentErr, "%+v", err)
 	}
+
+	err = Delete(db, objectStoreName, value.Get(pkeyName))
+	if err != nil {
+		return errors.WithMessagef(parentErr, "%+v", err)
+	}
+
+	jww.DEBUG.Printf("Successfully deleted value at %s/%s/%s",
+		objectStoreName, indexName, utils.JsToJson(key))
 	return nil
 }
 
diff --git a/main.go b/main.go
index d06333fb8dd96ea545ad3d00fb182e8849ae1dad..de379dbdbf5eb3132e8cc546df5a6b601665e5e6 100644
--- a/main.go
+++ b/main.go
@@ -11,13 +11,14 @@ package main
 
 import (
 	"fmt"
+	"os"
+	"syscall/js"
+
 	jww "github.com/spf13/jwalterweatherman"
 	"gitlab.com/elixxir/client/v4/bindings"
 	"gitlab.com/elixxir/xxdk-wasm/storage"
 	"gitlab.com/elixxir/xxdk-wasm/utils"
 	"gitlab.com/elixxir/xxdk-wasm/wasm"
-	"os"
-	"syscall/js"
 )
 
 func init() {
@@ -77,18 +78,25 @@ func main() {
 		js.FuncOf(wasm.LoadChannelsManagerWithIndexedDbUnsafe))
 	js.Global().Set("NewChannelsManagerWithIndexedDbUnsafe",
 		js.FuncOf(wasm.NewChannelsManagerWithIndexedDbUnsafe))
-	js.Global().Set("GenerateChannel", js.FuncOf(wasm.GenerateChannel))
-	js.Global().Set("GetSavedChannelPrivateKeyUNSAFE",
-		js.FuncOf(wasm.GetSavedChannelPrivateKeyUNSAFE))
 	js.Global().Set("DecodePublicURL", js.FuncOf(wasm.DecodePublicURL))
 	js.Global().Set("DecodePrivateURL", js.FuncOf(wasm.DecodePrivateURL))
 	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("NewChannelsDatabaseCipher",
 		js.FuncOf(wasm.NewChannelsDatabaseCipher))
 
+	// wasm/dm.go
+	js.Global().Set("NewDMClient", js.FuncOf(wasm.NewDMClient))
+	js.Global().Set("NewDMClientWithIndexedDb",
+		js.FuncOf(wasm.NewDMClientWithIndexedDb))
+	js.Global().Set("NewDMClientWithIndexedDbUnsafe",
+		js.FuncOf(wasm.NewDMClientWithIndexedDbUnsafe))
+	js.Global().Set("NewDMsDatabaseCipher",
+		js.FuncOf(wasm.NewDMsDatabaseCipher))
+
 	// wasm/cmix.go
 	js.Global().Set("NewCmix", js.FuncOf(wasm.NewCmix))
 	js.Global().Set("LoadCmix", js.FuncOf(wasm.LoadCmix))
diff --git a/utils/utils.go b/utils/utils.go
index 19399f5a62c5f41269739440d0a9948a7c244200..cb1e46f3859695e7c77fbf0c9403eb24b7b10ff4 100644
--- a/utils/utils.go
+++ b/utils/utils.go
@@ -74,9 +74,11 @@ func CreatePromise(f PromiseFn) any {
 	return Promise.New(handler)
 }
 
-// Await waits on a Javascript value. It returns the results of the then and
-// catch functions once it resolves.
-func Await(awaitable js.Value) ([]js.Value, []js.Value) {
+// 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 {
@@ -96,9 +98,9 @@ func Await(awaitable js.Value) ([]js.Value, []js.Value) {
 	awaitable.Call("then", thenFunc).Call("catch", catchFunc)
 
 	select {
-	case result := <-then:
+	case result = <-then:
 		return result, nil
-	case err := <-catch:
+	case err = <-catch:
 		return nil, err
 	}
 }
diff --git a/wasm/channels.go b/wasm/channels.go
index cf09b017d0263550e6f7da7a4f4f8825567439cc..d32737321c0efbab9ff732917b185a7b2cfbea99 100644
--- a/wasm/channels.go
+++ b/wasm/channels.go
@@ -11,7 +11,10 @@ package wasm
 
 import (
 	"encoding/base64"
-	"gitlab.com/elixxir/xxdk-wasm/indexedDb/channels"
+	"encoding/json"
+	"errors"
+	"gitlab.com/elixxir/client/v4/channels"
+	channelsDb "gitlab.com/elixxir/xxdk-wasm/indexedDb/channels"
 	"gitlab.com/xx_network/primitives/id"
 	"sync"
 	"syscall/js"
@@ -36,11 +39,12 @@ func newChannelsManagerJS(api *bindings.ChannelsManager) map[string]any {
 	cm := ChannelsManager{api}
 	channelsManagerMap := map[string]any{
 		// Basic Channel API
-		"GetID":         js.FuncOf(cm.GetID),
-		"JoinChannel":   js.FuncOf(cm.JoinChannel),
-		"GetChannels":   js.FuncOf(cm.GetChannels),
-		"LeaveChannel":  js.FuncOf(cm.LeaveChannel),
-		"ReplayChannel": js.FuncOf(cm.ReplayChannel),
+		"GetID":           js.FuncOf(cm.GetID),
+		"GenerateChannel": js.FuncOf(cm.GenerateChannel),
+		"JoinChannel":     js.FuncOf(cm.JoinChannel),
+		"GetChannels":     js.FuncOf(cm.GetChannels),
+		"LeaveChannel":    js.FuncOf(cm.LeaveChannel),
+		"ReplayChannel":   js.FuncOf(cm.ReplayChannel),
 
 		// Share URL
 		"GetShareURL": js.FuncOf(cm.GetShareURL),
@@ -51,12 +55,22 @@ func newChannelsManagerJS(api *bindings.ChannelsManager) map[string]any {
 		"SendMessage":           js.FuncOf(cm.SendMessage),
 		"SendReply":             js.FuncOf(cm.SendReply),
 		"SendReaction":          js.FuncOf(cm.SendReaction),
+		"DeleteMessage":         js.FuncOf(cm.DeleteMessage),
+		"PinMessage":            js.FuncOf(cm.PinMessage),
+		"MuteUser":              js.FuncOf(cm.MuteUser),
 		"GetIdentity":           js.FuncOf(cm.GetIdentity),
 		"ExportPrivateIdentity": js.FuncOf(cm.ExportPrivateIdentity),
 		"GetStorageTag":         js.FuncOf(cm.GetStorageTag),
 		"SetNickname":           js.FuncOf(cm.SetNickname),
 		"DeleteNickname":        js.FuncOf(cm.DeleteNickname),
 		"GetNickname":           js.FuncOf(cm.GetNickname),
+		"Muted":                 js.FuncOf(cm.Muted),
+		"GetMutedUsers":         js.FuncOf(cm.GetMutedUsers),
+		"IsChannelAdmin":        js.FuncOf(cm.IsChannelAdmin),
+		"ExportChannelAdminKey": js.FuncOf(cm.ExportChannelAdminKey),
+		"VerifyChannelAdminKey": js.FuncOf(cm.VerifyChannelAdminKey),
+		"ImportChannelAdminKey": js.FuncOf(cm.ImportChannelAdminKey),
+		"DeleteChannelAdminKey": js.FuncOf(cm.DeleteChannelAdminKey),
 
 		// Channel Receiving Logic and Callback Registration
 		"RegisterReceiveHandler": js.FuncOf(cm.RegisterReceiveHandler),
@@ -70,16 +84,19 @@ func newChannelsManagerJS(api *bindings.ChannelsManager) map[string]any {
 //
 // Returns:
 //   - Tracker ID (int).
-func (ch *ChannelsManager) GetID(js.Value, []js.Value) any {
-	return ch.api.GetID()
+func (cm *ChannelsManager) GetID(js.Value, []js.Value) any {
+	return cm.api.GetID()
 }
 
 // GenerateChannelIdentity creates a new private channel identity
-// ([channel.PrivateIdentity]). The public component can be retrieved as JSON
-// via [GetPublicChannelIdentityFromPrivate].
+// ([channel.PrivateIdentity]) from scratch and assigns it a codename.
+//
+// The public component can be retrieved as JSON via
+// [GetPublicChannelIdentityFromPrivate].
 //
 // Parameters:
-//   - args[0] - ID of [Cmix] object in tracker (int).
+//   - args[0] - ID of [Cmix] object in tracker (int). This can be retrieved
+//     using [Cmix.GetID].
 //
 // Returns:
 //   - Marshalled bytes of [channel.PrivateIdentity] (Uint8Array).
@@ -97,8 +114,8 @@ func GenerateChannelIdentity(_ js.Value, args []js.Value) any {
 // identityMap stores identities previously generated by ConstructIdentity.
 var identityMap sync.Map
 
-// ConstructIdentity constructs a [channel.Identity] from a user's public key
-// and codeset version.
+// ConstructIdentity creates a codename in a public [channel.Identity] from an
+// extant identity for a given codeset version.
 //
 // Parameters:
 //   - args[0] - The Ed25519 public key (Uint8Array).
@@ -157,7 +174,8 @@ func constructIdentity(_ js.Value, args []js.Value) any {
 //
 // Parameters:
 //   - args[0] - The password used to encrypt the identity (string).
-//   - args[2] - The encrypted data (Uint8Array).
+//   - args[2] - The encrypted data from [ChannelsManager.ExportPrivateIdentity]
+//     (Uint8Array).
 //
 // Returns:
 //   - JSON of [channel.PrivateIdentity] (Uint8Array).
@@ -202,7 +220,7 @@ func GetPublicChannelIdentity(_ js.Value, args []js.Value) any {
 //
 // Parameters:
 //   - args[0] - Bytes of the private identity
-//     (channel.PrivateIdentity]) (Uint8Array).
+//     ([channel.PrivateIdentity]) (Uint8Array).
 //
 // Returns:
 //   - JSON of the public identity ([channel.Identity]) (Uint8Array).
@@ -220,26 +238,6 @@ func GetPublicChannelIdentityFromPrivate(_ js.Value, args []js.Value) any {
 	return utils.CopyBytesToJS(identity)
 }
 
-// eventModelBuilder adheres to the [bindings.EventModelBuilder] interface.
-type eventModelBuilder struct {
-	build func(args ...any) js.Value
-}
-
-// Build initializes and returns the event model.  It wraps a Javascript object
-// that has all the methods in [bindings.EventModel] to make it adhere to the Go
-// interface [bindings.EventModel].
-func (emb *eventModelBuilder) Build(path string) bindings.EventModel {
-	emJs := emb.build(path)
-	return &eventModel{
-		joinChannel:      utils.WrapCB(emJs, "JoinChannel"),
-		leaveChannel:     utils.WrapCB(emJs, "LeaveChannel"),
-		receiveMessage:   utils.WrapCB(emJs, "ReceiveMessage"),
-		receiveReply:     utils.WrapCB(emJs, "ReceiveReply"),
-		receiveReaction:  utils.WrapCB(emJs, "ReceiveReaction"),
-		updateSentStatus: utils.WrapCB(emJs, "UpdateSentStatus"),
-	}
-}
-
 // NewChannelsManager creates a new [ChannelsManager] from a new private
 // identity ([channel.PrivateIdentity]).
 //
@@ -274,7 +272,8 @@ func NewChannelsManager(_ js.Value, args []js.Value) any {
 	return newChannelsManagerJS(cm)
 }
 
-// LoadChannelsManager loads an existing [ChannelsManager].
+// LoadChannelsManager loads an existing [ChannelsManager] for the given storage
+// tag.
 //
 // This is for loading a manager for an identity that has already been created.
 // The channel manager should have previously been created with
@@ -394,7 +393,7 @@ func newChannelsManagerWithIndexedDb(cmixID int, privateIdentity []byte,
 		cb.Invoke(uuid, utils.CopyBytesToJS(channelID.Marshal()), update)
 	}
 
-	model := channels.NewWASMEventModelBuilder(cipher, messageReceivedCB)
+	model := channelsDb.NewWASMEventModelBuilder(cipher, messageReceivedCB)
 
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
 		cm, err := bindings.NewChannelsManagerGoEventModel(
@@ -493,7 +492,7 @@ func loadChannelsManagerWithIndexedDb(cmixID int, storageTag string,
 		cb.Invoke(uuid, utils.CopyBytesToJS(channelID.Marshal()), updated)
 	}
 
-	model := channels.NewWASMEventModelBuilder(cipher, messageReceivedCB)
+	model := channelsDb.NewWASMEventModelBuilder(cipher, messageReceivedCB)
 
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
 		cm, err := bindings.LoadChannelsManagerGoEventModel(
@@ -508,76 +507,9 @@ func loadChannelsManagerWithIndexedDb(cmixID int, storageTag string,
 	return utils.CreatePromise(promiseFn)
 }
 
-// GenerateChannel is used to create a channel a new channel of which you are
-// the admin. It is only for making new channels, not joining existing ones.
-//
-// It returns a pretty print of the channel and the private key.
-//
-// The name cannot be longer that __ characters. The description cannot be
-// longer than __ and can only use ______ characters.
-//
-// Parameters:
-//   - args[0] - ID of [Cmix] object in tracker (int).
-//   - args[1] - The name of the new channel (string). The name must be between
-//     3 and 24 characters inclusive. It can only include upper and lowercase
-//     unicode letters, digits 0 through 9, and underscores (_). It cannot be
-//     changed once a channel is created.
-//   - args[2] - The description of a channel (string). The description is
-//     optional but cannot be longer than 144 characters and can include all
-//     unicode characters. It cannot be changed once a channel is created.
-//   - args[3] - The [broadcast.PrivacyLevel] of the channel (int). 0 = public,
-//     1 = private, and 2 = secret. Refer to the comment below for more
-//     information.
-//
-// Returns:
-//   - JSON of [bindings.ChannelGeneration], which describes a generated
-//     channel. It contains both the public channel info and the private key for
-//     the channel in PEM format (Uint8Array).
-//   - Throws a TypeError if generating the channel fails.
-//
-// The [broadcast.PrivacyLevel] of a channel indicates the level of channel
-// information revealed when sharing it via URL. For any channel besides public
-// channels, the secret information is encrypted and a password is required to
-// share and join a channel.
-//   - A privacy level of [broadcast.Public] reveals all the information
-//     including the name, description, privacy level, public key and salt.
-//   - A privacy level of [broadcast.Private] reveals only the name and
-//     description.
-//   - A privacy level of [broadcast.Secret] reveals nothing.
-func GenerateChannel(_ js.Value, args []js.Value) any {
-	gen, err := bindings.GenerateChannel(
-		args[0].Int(), args[1].String(), args[2].String(), args[3].Int())
-	if err != nil {
-		utils.Throw(utils.TypeError, err)
-		return nil
-	}
-
-	return utils.CopyBytesToJS(gen)
-}
-
-// GetSavedChannelPrivateKeyUNSAFE loads the private key from storage for the
-// given channel ID.
-//
-// NOTE: This function is unsafe and only for debugging purposes only.
-//
-// Parameters:
-//   - args[0] - ID of [Cmix] object in tracker (int).
-//   - args[1] - The [id.ID] of the channel in base 64 encoding (string).
-//
-// Returns:
-//   - The PEM file of the private key (string).
-//   - Throws a TypeError if retrieving the [Cmix] object or the private key
-//     fails.
-func GetSavedChannelPrivateKeyUNSAFE(_ js.Value, args []js.Value) any {
-	privKey, err := bindings.GetSavedChannelPrivateKeyUNSAFE(
-		args[0].Int(), args[1].String())
-	if err != nil {
-		utils.Throw(utils.TypeError, err)
-		return nil
-	}
-
-	return privKey
-}
+////////////////////////////////////////////////////////////////////////////////
+// Channel Actions                                                            //
+////////////////////////////////////////////////////////////////////////////////
 
 // DecodePublicURL decodes the channel URL into a channel pretty print. This
 // function can only be used for public channel URLs. To get the privacy level
@@ -675,65 +607,86 @@ func GetChannelInfo(_ js.Value, args []js.Value) any {
 	return utils.CopyBytesToJS(ci)
 }
 
-// JoinChannel joins the given channel. It will fail if the channel has already
-// been joined.
-//
-// Parameters:
-//   - args[0] - A portable channel string. Should be received from another user
-//     or generated via [GenerateChannel] (string).
+// GenerateChannel creates a new channel with the user as the admin and returns
+// the pretty print of the channel. This function only create a channel and does
+// not join it.
 //
-// The pretty print will be of the format:
+// The private key is saved to storage and can be accessed with
+// [ChannelsManager.ExportChannelAdminKey].
 //
-//	<Speakeasy-v3:Test_Channel|description:Channel description.|level:Public|created:1666718081766741100|secrets:+oHcqDbJPZaT3xD5NcdLY8OjOMtSQNKdKgLPmr7ugdU=|rCI0wr01dHFStjSFMvsBzFZClvDIrHLL5xbCOPaUOJ0=|493|1|7cBhJxVfQxWo+DypOISRpeWdQBhuQpAZtUbQHjBm8NQ=>
+// Parameters:
+//   - args[0] - The name of the new channel (string). The name must be between
+//     3 and 24 characters inclusive. It can only include upper and lowercase
+//     Unicode letters, digits 0 through 9, and underscores (_). It cannot be
+//     changed once a channel is created.
+//   - args[1] - The description of a channel (string). The description is
+//     optional but cannot be longer than 144 characters and can include all
+//     Unicode characters. It cannot be changed once a channel is created.
+//   - args[2] - The [broadcast.PrivacyLevel] of the channel (int). 0 = public,
+//     1 = private, and 2 = secret. Refer to the comment below for more
+//     information.
 //
 // Returns:
-//   - JSON of [bindings.ChannelInfo], which describes all relevant channel info
-//     (Uint8Array).
-//   - Throws a TypeError if joining the channel fails.
-func (ch *ChannelsManager) JoinChannel(_ js.Value, args []js.Value) any {
-	ci, err := ch.api.JoinChannel(args[0].String())
+//   - The pretty print of the channel (string).
+//   - Throws a TypeError if generating the channel fails.
+//
+// The [broadcast.PrivacyLevel] of a channel indicates the level of channel
+// information revealed when sharing it via URL. For any channel besides public
+// channels, the secret information is encrypted and a password is required to
+// share and join a channel.
+//   - A privacy level of [broadcast.Public] reveals all the information
+//     including the name, description, privacy level, public key and salt.
+//   - A privacy level of [broadcast.Private] reveals only the name and
+//     description.
+//   - A privacy level of [broadcast.Secret] reveals nothing.
+func (cm *ChannelsManager) GenerateChannel(_ js.Value, args []js.Value) any {
+	prettyPrint, err := cm.api.GenerateChannel(
+		args[0].String(), args[1].String(), args[2].Int())
 	if err != nil {
 		utils.Throw(utils.TypeError, err)
 		return nil
 	}
 
-	return utils.CopyBytesToJS(ci)
+	return prettyPrint
 }
 
-// GetChannels returns the IDs of all channels that have been joined.
+// JoinChannel joins the given channel. It will return the error
+// [channels.ChannelAlreadyExistsErr] if the channel has already been joined.
 //
-// Returns:
-//   - JSON of an array of marshalled [id.ID] (Uint8Array).
-//   - Throws a TypeError if getting the channels fails.
+// Parameters:
+//   - args[0] - A portable channel string. Should be received from another user
+//     or generated via [ChannelsManager.GenerateChannel] (string).
 //
-// JSON Example:
+// The pretty print will be of the format:
 //
-//	{
-//	  "U4x/lrFkvxuXu59LtHLon1sUhPJSCcnZND6SugndnVID",
-//	  "15tNdkKbYXoMn58NO6VbDMDWFEyIhTWEGsvgcJsHWAgD"
-//	}
-func (ch *ChannelsManager) GetChannels(js.Value, []js.Value) any {
-	channelList, err := ch.api.GetChannels()
+//	<Speakeasy-v3:Test_Channel|description:Channel description.|level:Public|created:1666718081766741100|secrets:+oHcqDbJPZaT3xD5NcdLY8OjOMtSQNKdKgLPmr7ugdU=|rCI0wr01dHFStjSFMvsBzFZClvDIrHLL5xbCOPaUOJ0=|493|1|7cBhJxVfQxWo+DypOISRpeWdQBhuQpAZtUbQHjBm8NQ=>
+//
+// Returns:
+//   - JSON of [bindings.ChannelInfo], which describes all relevant channel info
+//     (Uint8Array).
+//   - Throws a TypeError if joining the channel fails.
+func (cm *ChannelsManager) JoinChannel(_ js.Value, args []js.Value) any {
+	ci, err := cm.api.JoinChannel(args[0].String())
 	if err != nil {
 		utils.Throw(utils.TypeError, err)
 		return nil
 	}
 
-	return utils.CopyBytesToJS(channelList)
+	return utils.CopyBytesToJS(ci)
 }
 
-// LeaveChannel leaves the given channel. It will return an error if the channel
-// was not previously joined.
+// LeaveChannel leaves the given channel. It will return the error
+// [channels.ChannelDoesNotExistsErr] if the channel was not previously joined.
 //
 // Parameters:
 //   - args[0] - Marshalled bytes of the channel [id.ID] (Uint8Array).
 //
 // Returns:
 //   - Throws a TypeError if the channel does not exist.
-func (ch *ChannelsManager) LeaveChannel(_ js.Value, args []js.Value) any {
+func (cm *ChannelsManager) LeaveChannel(_ js.Value, args []js.Value) any {
 	marshalledChanId := utils.CopyBytesToGo(args[0])
 
-	err := ch.api.LeaveChannel(marshalledChanId)
+	err := cm.api.LeaveChannel(marshalledChanId)
 	if err != nil {
 		utils.Throw(utils.TypeError, err)
 		return nil
@@ -745,15 +698,18 @@ func (ch *ChannelsManager) LeaveChannel(_ js.Value, args []js.Value) any {
 // ReplayChannel replays all messages from the channel within the network's
 // memory (~3 weeks) over the event model.
 //
+// Returns the error [channels.ChannelDoesNotExistsErr] if the channel was not
+// previously joined.
+//
 // Parameters:
-//   - args[0] - Marshalled bytes of the channel [id.ID] (Uint8Array).
+//   - args[0] - Marshalled bytes of the channel's [id.ID] (Uint8Array).
 //
 // Returns:
 //   - Throws a TypeError if the replay fails.
-func (ch *ChannelsManager) ReplayChannel(_ js.Value, args []js.Value) any {
+func (cm *ChannelsManager) ReplayChannel(_ js.Value, args []js.Value) any {
 	marshalledChanId := utils.CopyBytesToGo(args[0])
 
-	err := ch.api.ReplayChannel(marshalledChanId)
+	err := cm.api.ReplayChannel(marshalledChanId)
 	if err != nil {
 		utils.Throw(utils.TypeError, err)
 		return nil
@@ -762,6 +718,28 @@ func (ch *ChannelsManager) ReplayChannel(_ js.Value, args []js.Value) any {
 	return nil
 }
 
+// GetChannels returns the IDs of all channels that have been joined.
+//
+// Returns:
+//   - JSON of an array of marshalled [id.ID] (Uint8Array).
+//   - Throws a TypeError if getting the channels fails.
+//
+// JSON Example:
+//
+//	{
+//	  "U4x/lrFkvxuXu59LtHLon1sUhPJSCcnZND6SugndnVID",
+//	  "15tNdkKbYXoMn58NO6VbDMDWFEyIhTWEGsvgcJsHWAgD"
+//	}
+func (cm *ChannelsManager) GetChannels(js.Value, []js.Value) any {
+	channelList, err := cm.api.GetChannels()
+	if err != nil {
+		utils.Throw(utils.TypeError, err)
+		return nil
+	}
+
+	return utils.CopyBytesToJS(channelList)
+}
+
 ////////////////////////////////////////////////////////////////////////////////
 // Channel Share URL                                                          //
 ////////////////////////////////////////////////////////////////////////////////
@@ -786,8 +764,8 @@ type ShareURL struct {
 // channel. If it is set to 0, then it can be shared unlimited times. The max
 // uses is set as a URL parameter using the key [broadcast.MaxUsesKey]. Note
 // that this number is also encoded in the secret data for private and secret
-// URLs, so if the number is changed in the URL, is will be verified when
-// calling [DecodePublicURL] or [DecodePrivateURL]. There is no enforcement for
+// URLs, so if the number is changed in the URL, it will be verified when
+// calling [DecodePublicURL] and [DecodePrivateURL]. There is no enforcement for
 // public URLs.
 //
 // Parameters:
@@ -800,13 +778,13 @@ type ShareURL struct {
 // Returns:
 //   - JSON of [bindings.ShareURL] (Uint8Array).
 //   - Throws a TypeError if generating the URL fails.
-func (ch *ChannelsManager) GetShareURL(_ js.Value, args []js.Value) any {
+func (cm *ChannelsManager) GetShareURL(_ js.Value, args []js.Value) any {
 	cmixID := args[0].Int()
 	host := args[1].String()
 	maxUses := args[2].Int()
 	marshalledChanId := utils.CopyBytesToGo(args[3])
 
-	su, err := ch.api.GetShareURL(cmixID, host, maxUses, marshalledChanId)
+	su, err := cm.api.GetShareURL(cmixID, host, maxUses, marshalledChanId)
 	if err != nil {
 		utils.Throw(utils.TypeError, err)
 		return nil
@@ -823,7 +801,7 @@ func (ch *ChannelsManager) GetShareURL(_ js.Value, args []js.Value) any {
 //
 // Returns:
 //   - An int that corresponds to the [broadcast.PrivacyLevel] as outlined
-//     below.
+//     below (int).
 //   - Throws a TypeError if parsing the URL fails.
 //
 // Possible returns:
@@ -845,84 +823,54 @@ func GetShareUrlType(_ js.Value, args []js.Value) any {
 // Channel Sending Methods and Reports                                        //
 ////////////////////////////////////////////////////////////////////////////////
 
+// ValidForever returns the value to use for validUntil when you want a message
+// to be available for the maximum amount of time.
+//
+// Returns:
+//   - The maximum amount of time (int).
+func ValidForever(js.Value, []js.Value) any {
+	return bindings.ValidForever()
+}
+
 // SendGeneric is used to send a raw message over a channel. In general, it
-// should be wrapped in a function which defines the wire protocol. If the final
-// message, before being sent over the wire, is too long, this will return an
-// error. Due to the underlying encoding using compression, it isn't possible to
-// define the largest payload that can be sent, but it will always be possible
-// to send a payload of 802 bytes at minimum. The meaning of validUntil depends
-// on the use case.
+// should be wrapped in a function that defines the wire protocol.
+//
+// If the final message, before being sent over the wire, is too long, this
+// will return an error. Due to the underlying encoding using compression,
+// it is not possible to define the largest payload that can be sent, but it
+// will always be possible to send a payload of 802 bytes at minimum.
 //
 // Parameters:
 //   - args[0] - Marshalled bytes of the channel [id.ID] (Uint8Array).
 //   - args[1] - The message type of the message. This will be a valid
 //     [channels.MessageType] (int).
 //   - args[2] - The contents of the message (Uint8Array).
-//   - args[3] - The lease of the message. This will be how long the message is
-//     valid until, in milliseconds. 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 (int).
-//   - args[4] - JSON of [xxdk.CMIXParams]. If left empty
+//   - args[3] - 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[4] - Set tracked to true if the message should be tracked in the
+//     sendTracker, which allows messages to be shown locally before they are
+//     received on the network. In general, all messages that will be displayed
+//     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).
 //
 // Returns a promise:
 //   - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array).
 //   - Rejected with an error if sending fails.
-func (ch *ChannelsManager) SendGeneric(_ js.Value, args []js.Value) any {
+func (cm *ChannelsManager) SendGeneric(_ js.Value, args []js.Value) any {
 	marshalledChanId := utils.CopyBytesToGo(args[0])
 	messageType := args[1].Int()
 	message := utils.CopyBytesToGo(args[2])
 	leaseTimeMS := int64(args[3].Int())
-	cmixParamsJSON := utils.CopyBytesToGo(args[4])
-
-	promiseFn := func(resolve, reject func(args ...any) js.Value) {
-		sendReport, err := ch.api.SendGeneric(
-			marshalledChanId, messageType, message, leaseTimeMS, cmixParamsJSON)
-		if err != nil {
-			reject(utils.JsTrace(err))
-		} else {
-			resolve(utils.CopyBytesToJS(sendReport))
-		}
-	}
-
-	return utils.CreatePromise(promiseFn)
-}
-
-// SendAdminGeneric is used to send a raw message over a channel encrypted with
-// admin keys, identifying it as sent by the admin. In general, it should be
-// wrapped in a function that defines the wire protocol. If the final message,
-// before being sent over the wire, is too long, this will return an error. The
-// message must be at most 510 bytes long.
-//
-// Parameters:
-//   - args[0] - The PEM-encode admin RSA private key (Uint8Array).
-//   - args[1] - Marshalled bytes of the channel [id.ID] (Uint8Array).
-//   - args[2] - The message type of the message. This will be a valid
-//     [channels.MessageType] (int).
-//   - args[3] - The contents of the message (Uint8Array).
-//   - args[4] - The lease of the message. This will be how long the message is
-//     valid until, in milliseconds. 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 (int).
-//   - args[5] - 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 (ch *ChannelsManager) SendAdminGeneric(_ js.Value, args []js.Value) any {
-	adminPrivateKey := utils.CopyBytesToGo(args[0])
-	marshalledChanId := utils.CopyBytesToGo(args[1])
-	messageType := args[2].Int()
-	message := utils.CopyBytesToGo(args[3])
-	leaseTimeMS := int64(args[4].Int())
+	tracked := args[4].Bool()
 	cmixParamsJSON := utils.CopyBytesToGo(args[5])
 
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
-		sendReport, err := ch.api.SendAdminGeneric(adminPrivateKey,
-			marshalledChanId, messageType, message, leaseTimeMS, cmixParamsJSON)
+		sendReport, err := cm.api.SendGeneric(marshalledChanId, messageType,
+			message, leaseTimeMS, tracked, cmixParamsJSON)
 		if err != nil {
 			reject(utils.JsTrace(err))
 		} else {
@@ -934,6 +882,7 @@ func (ch *ChannelsManager) SendAdminGeneric(_ js.Value, args []js.Value) any {
 }
 
 // SendMessage is used to send a formatted message over a channel.
+//
 // Due to the underlying encoding using compression, it isn't possible to define
 // the largest payload that can be sent, but it will always be possible to send
 // a payload of 798 bytes at minimum.
@@ -944,25 +893,25 @@ func (ch *ChannelsManager) SendAdminGeneric(_ js.Value, args []js.Value) any {
 // Parameters:
 //   - args[0] - Marshalled bytes of the channel [id.ID] (Uint8Array).
 //   - args[1] - The contents of the message (string).
-//   - args[2] - The lease of the message. This will be how long the message is
-//     valid until, in milliseconds. 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 (int).
+//   - args[2] - 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[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 (ch *ChannelsManager) SendMessage(_ js.Value, args []js.Value) any {
+func (cm *ChannelsManager) SendMessage(_ js.Value, args []js.Value) any {
 	marshalledChanId := utils.CopyBytesToGo(args[0])
 	message := args[1].String()
 	leaseTimeMS := int64(args[2].Int())
 	cmixParamsJSON := utils.CopyBytesToGo(args[3])
 
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
-		sendReport, err := ch.api.SendMessage(
+		sendReport, err := cm.api.SendMessage(
 			marshalledChanId, message, leaseTimeMS, cmixParamsJSON)
 		if err != nil {
 			reject(utils.JsTrace(err))
@@ -974,15 +923,14 @@ func (ch *ChannelsManager) SendMessage(_ js.Value, args []js.Value) any {
 	return utils.CreatePromise(promiseFn)
 }
 
-// SendReply is used to send a formatted message over a channel. Due to the
-// underlying encoding using compression, it isn't possible to define the
-// largest payload that can be sent, but it will always be possible to send a
-// payload of 766 bytes at minimum.
+// SendReply is used to send a formatted message over a channel.
+//
+// Due to the underlying encoding using compression, it is not possible to
+// define the largest payload that can be sent, but it will always be possible
+// to send a payload of 766 bytes at minimum.
 //
-// If the message ID the reply is sent to is nonexistent, the other side will
-// post the message as a normal message and not a reply. The message will auto
-// delete validUntil after the round it is sent in, lasting forever if
-// [channels.ValidForever] is used.
+// If the message ID that the reply is sent to does not exist, then the other
+// side will post the message as a normal message and not as a reply.
 //
 // Parameters:
 //   - args[0] - Marshalled bytes of the channel [id.ID] (Uint8Array).
@@ -994,18 +942,18 @@ func (ch *ChannelsManager) SendMessage(_ 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  RegisterReceiveHandler (Uint8Array).
-//   - args[3] - The lease of the message. This will be how long the message is
-//     valid until, in milliseconds. 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 (int).
+//   - args[3] - 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[4] - 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 (ch *ChannelsManager) SendReply(_ js.Value, args []js.Value) any {
+func (cm *ChannelsManager) SendReply(_ js.Value, args []js.Value) any {
 	marshalledChanId := utils.CopyBytesToGo(args[0])
 	message := args[1].String()
 	messageToReactTo := utils.CopyBytesToGo(args[2])
@@ -1013,7 +961,7 @@ func (ch *ChannelsManager) SendReply(_ js.Value, args []js.Value) any {
 	cmixParamsJSON := utils.CopyBytesToGo(args[4])
 
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
-		sendReport, err := ch.api.SendReply(marshalledChanId, message,
+		sendReport, err := cm.api.SendReply(marshalledChanId, message,
 			messageToReactTo, leaseTimeMS, cmixParamsJSON)
 		if err != nil {
 			reject(utils.JsTrace(err))
@@ -1025,10 +973,11 @@ func (ch *ChannelsManager) SendReply(_ js.Value, args []js.Value) any {
 	return utils.CreatePromise(promiseFn)
 }
 
-// SendReaction is used to send a reaction to a message over a channel.
-// The reaction must be a single emoji with no other characters, and will
-// be rejected otherwise.
-// Users will drop the reaction if they do not recognize the reactTo message.
+// SendReaction is used to send a reaction to a message over a channel. The
+// reaction must be a single emoji with no other characters, and will be
+// rejected otherwise.
+//
+// Clients will drop the reaction if they do not recognize the reactTo message.
 //
 // Parameters:
 //   - args[0] - Marshalled bytes of the channel [id.ID] (Uint8Array).
@@ -1045,14 +994,14 @@ func (ch *ChannelsManager) SendReply(_ js.Value, args []js.Value) any {
 // Returns a promise:
 //   - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array).
 //   - Rejected with an error if sending fails.
-func (ch *ChannelsManager) SendReaction(_ js.Value, args []js.Value) any {
+func (cm *ChannelsManager) SendReaction(_ js.Value, args []js.Value) any {
 	marshalledChanId := utils.CopyBytesToGo(args[0])
 	reaction := args[1].String()
 	messageToReactTo := utils.CopyBytesToGo(args[2])
 	cmixParamsJSON := utils.CopyBytesToGo(args[3])
 
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
-		sendReport, err := ch.api.SendReaction(
+		sendReport, err := cm.api.SendReaction(
 			marshalledChanId, reaction, messageToReactTo, cmixParamsJSON)
 		if err != nil {
 			reject(utils.JsTrace(err))
@@ -1064,14 +1013,197 @@ func (ch *ChannelsManager) SendReaction(_ js.Value, args []js.Value) any {
 	return utils.CreatePromise(promiseFn)
 }
 
-// GetIdentity returns the marshaled public identity ([channel.Identity]) that
-// the channel is using.
+////////////////////////////////////////////////////////////////////////////////
+// Admin Sending                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+// SendAdminGeneric is used to send a raw message over a channel encrypted with
+// admin keys, identifying it as sent by the admin. In general, it should be
+// wrapped in a function that defines the wire protocol.
+//
+// If the final message, before being sent over the wire, is too long, this will
+// return an error. The message must be at most 510 bytes long.
+//
+// If the user is not an admin of the channel (i.e. does not have a private key
+// for the channel saved to storage), then the error [channels.NotAnAdminErr] is
+// returned.
+//
+// Parameters:
+//   - args[0] - Marshalled bytes of the channel [id.ID] (Uint8Array).
+//   - args[1] - The message type of the message. This will be a valid
+//     [channels.MessageType] (int).
+//   - args[2] - The contents of the message (Uint8Array). The message should be
+//     at most 510 bytes.
+//   - args[3] - 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[4] - Set tracked to true if the message should be tracked in the
+//     sendTracker, which allows messages to be shown locally before they are
+//     received on the network. In general, all messages that will be displayed
+//     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).
+//
+// Returns a promise:
+//   - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array).
+//   - Rejected with an error if sending fails.
+func (cm *ChannelsManager) SendAdminGeneric(_ js.Value, args []js.Value) any {
+	marshalledChanId := utils.CopyBytesToGo(args[0])
+	messageType := args[1].Int()
+	message := utils.CopyBytesToGo(args[2])
+	leaseTimeMS := int64(args[3].Int())
+	tracked := args[4].Bool()
+	cmixParamsJSON := utils.CopyBytesToGo(args[5])
+
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		sendReport, err := cm.api.SendAdminGeneric(marshalledChanId,
+			messageType, message, leaseTimeMS, tracked, cmixParamsJSON)
+		if err != nil {
+			reject(utils.JsTrace(err))
+		} else {
+			resolve(utils.CopyBytesToJS(sendReport))
+		}
+	}
+
+	return utils.CreatePromise(promiseFn)
+}
+
+// DeleteMessage deletes the targeted message from user's view. Users may delete
+// their own messages but only the channel admin can delete other user's
+// messages. If the user is not an admin of the channel or if they are not the
+// sender of the targetMessage, then the error [channels.NotAnAdminErr] is
+// returned.
+//
+// If undoAction is true, then the targeted message is un-deleted.
+//
+// Clients will drop the deletion if they do not recognize the target
+// message.
+//
+// Parameters:
+//   - args[0] - Marshalled bytes of channel [id.ID] (Uint8Array).
+//   - args[1] - The marshalled [channel.MessageID] of the message you want to
+//     delete (Uint8Array).
+//   - args[2] - JSON of [xxdk.CMIXParams]. This may be empty, and
+//     [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) DeleteMessage(_ js.Value, args []js.Value) any {
+	channelIdBytes := utils.CopyBytesToGo(args[0])
+	targetMessageIdBytes := utils.CopyBytesToGo(args[1])
+	cmixParamsJSON := utils.CopyBytesToGo(args[2])
+
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		sendReport, err := cm.api.DeleteMessage(
+			channelIdBytes, targetMessageIdBytes, cmixParamsJSON)
+		if err != nil {
+			reject(utils.JsTrace(err))
+		} else {
+			resolve(utils.CopyBytesToJS(sendReport))
+		}
+	}
+
+	return utils.CreatePromise(promiseFn)
+}
+
+// PinMessage pins the target message to the top of a channel view for all users
+// in the specified channel. Only the channel admin can pin user messages; if
+// the user is not an admin of the channel, then the error
+// [channels.NotAnAdminErr] is returned.
+//
+// If undoAction is true, then the targeted message is unpinned.
+//
+// Clients will drop the pin if they do not recognize the target message.
+//
+// Parameters:
+//   - args[0] - Marshalled bytes of channel [id.ID] (Uint8Array).
+//   - args[1] - The marshalled [channel.MessageID] of the message you want to
+//     pin (Uint8Array).
+//   - args[2] - Set to true to unpin the message (boolean).
+//   - args[3] - The time, in milliseconds, that the message should be pinned
+//     (int). To remain pinned indefinitely, use [ValidForever].
+//   - args[4] - JSON of [xxdk.CMIXParams]. This may be empty, and
+//     [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) PinMessage(_ js.Value, args []js.Value) any {
+	channelIdBytes := utils.CopyBytesToGo(args[0])
+	targetMessageIdBytes := utils.CopyBytesToGo(args[1])
+	undoAction := args[2].Bool()
+	validUntilMS := args[3].Int()
+	cmixParamsJSON := utils.CopyBytesToGo(args[4])
+
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		sendReport, err := cm.api.PinMessage(channelIdBytes,
+			targetMessageIdBytes, undoAction, validUntilMS, cmixParamsJSON)
+		if err != nil {
+			reject(utils.JsTrace(err))
+		} else {
+			resolve(utils.CopyBytesToJS(sendReport))
+		}
+	}
+
+	return utils.CreatePromise(promiseFn)
+}
+
+// MuteUser is used to mute a user in a channel. Muting a user will cause all
+// future messages from the user being dropped on reception. Muted users are
+// also unable to send messages. Only the channel admin can mute a user; if the
+// user is not an admin of the channel, then the error [channels.NotAnAdminErr]
+// is returned.
+//
+// If undoAction is true, then the targeted user will be unmuted.
+//
+// Parameters:
+//   - args[0] - Marshalled bytes of channel [id.ID] (Uint8Array).
+//   - args[1] - The [ed25519.PublicKey] of the user you want to mute
+//     (Uint8Array).
+//   - args[2] - Set to true to unmute the message (boolean).
+//   - args[3] - The time, in milliseconds, that the user should be muted (int).
+//     To remain muted indefinitely, use [ValidForever].
+//   - args[4] - JSON of [xxdk.CMIXParams]. This may be empty, and
+//     [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) MuteUser(_ js.Value, args []js.Value) any {
+	channelIdBytes := utils.CopyBytesToGo(args[0])
+	mutedUserPubKeyBytes := utils.CopyBytesToGo(args[1])
+	undoAction := args[2].Bool()
+	validUntilMS := args[3].Int()
+	cmixParamsJSON := utils.CopyBytesToGo(args[4])
+
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		sendReport, err := cm.api.MuteUser(channelIdBytes, mutedUserPubKeyBytes,
+			undoAction, validUntilMS, cmixParamsJSON)
+		if err != nil {
+			reject(utils.JsTrace(err))
+		} else {
+			resolve(utils.CopyBytesToJS(sendReport))
+		}
+	}
+
+	return utils.CreatePromise(promiseFn)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Other Channel Actions                                                      //
+////////////////////////////////////////////////////////////////////////////////
+
+// GetIdentity returns the public identity ([channel.Identity]) of the user
+// associated with this channel manager.
 //
 // Returns:
 //   - JSON of the [channel.Identity] (Uint8Array).
 //   - Throws TypeError if marshalling the identity fails.
-func (ch *ChannelsManager) GetIdentity(js.Value, []js.Value) any {
-	i, err := ch.api.GetIdentity()
+func (cm *ChannelsManager) GetIdentity(js.Value, []js.Value) any {
+	i, err := cm.api.GetIdentity()
 	if err != nil {
 		utils.Throw(utils.TypeError, err)
 		return nil
@@ -1080,17 +1212,17 @@ func (ch *ChannelsManager) GetIdentity(js.Value, []js.Value) any {
 	return utils.CopyBytesToJS(i)
 }
 
-// ExportPrivateIdentity encrypts and exports the private identity to a portable
-// string.
+// ExportPrivateIdentity encrypts the private identity using the password and
+// exports it to a portable string.
 //
 // Parameters:
-//   - args[0] - Password to encrypt the identity with (string).
+//   - password - The password used to encrypt the private identity (string).
 //
 // Returns:
-//   - JSON of the encrypted private identity (Uint8Array).
+//   - Encrypted portable private identity (Uint8Array).
 //   - Throws TypeError if exporting the identity fails.
-func (ch *ChannelsManager) ExportPrivateIdentity(_ js.Value, args []js.Value) any {
-	i, err := ch.api.ExportPrivateIdentity(args[0].String())
+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)
 		return nil
@@ -1099,12 +1231,13 @@ func (ch *ChannelsManager) ExportPrivateIdentity(_ js.Value, args []js.Value) an
 	return utils.CopyBytesToJS(i)
 }
 
-// GetStorageTag returns the storage tag needed to reload the manager.
+// GetStorageTag returns the tag where this manager is stored. To be used when
+// loading the manager. The storage tag is derived from the public key.
 //
 // Returns:
 //   - Storage tag (string).
-func (ch *ChannelsManager) GetStorageTag(js.Value, []js.Value) any {
-	return ch.api.GetStorageTag()
+func (cm *ChannelsManager) GetStorageTag(js.Value, []js.Value) any {
+	return cm.api.GetStorageTag()
 }
 
 // SetNickname sets the nickname for a given channel. The nickname must be valid
@@ -1117,8 +1250,8 @@ func (ch *ChannelsManager) GetStorageTag(js.Value, []js.Value) any {
 // Returns:
 //   - Throws TypeError if unmarshalling the ID fails or the nickname is
 //     invalid.
-func (ch *ChannelsManager) SetNickname(_ js.Value, args []js.Value) any {
-	err := ch.api.SetNickname(args[0].String(), utils.CopyBytesToGo(args[1]))
+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)
 		return nil
@@ -1127,15 +1260,16 @@ func (ch *ChannelsManager) SetNickname(_ js.Value, args []js.Value) any {
 	return nil
 }
 
-// DeleteNickname deletes the nickname for a given channel.
+// DeleteNickname removes the nickname for a given channel. The name will revert
+// back to the codename for this channel instead.
 //
 // Parameters:
 //   - args[0] - Marshalled bytes if the channel's [id.ID] (Uint8Array).
 //
 // Returns:
 //   - Throws TypeError if deleting the nickname fails.
-func (ch *ChannelsManager) DeleteNickname(_ js.Value, args []js.Value) any {
-	err := ch.api.DeleteNickname(utils.CopyBytesToGo(args[0]))
+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)
 		return nil
@@ -1151,10 +1285,10 @@ func (ch *ChannelsManager) DeleteNickname(_ js.Value, args []js.Value) any {
 //   - args[0] - Marshalled bytes if the channel's [id.ID] (Uint8Array).
 //
 // Returns:
-//   - The nickname (string).
+//   - The nickname set for the channel (string).
 //   - Throws TypeError if the channel has no nickname set.
-func (ch *ChannelsManager) GetNickname(_ js.Value, args []js.Value) any {
-	nickname, err := ch.api.GetNickname(utils.CopyBytesToGo(args[0]))
+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)
 		return nil
@@ -1184,6 +1318,201 @@ func IsNicknameValid(_ js.Value, args []js.Value) any {
 	return nil
 }
 
+// Muted returns true if the user is currently muted in the given channel.
+//
+// Parameters:
+//   - args[0] - Marshalled bytes if the channel's [id.ID] (Uint8Array).
+//
+// 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.
+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)
+		return nil
+	}
+
+	return muted
+}
+
+// GetMutedUsers returns the list of the public keys for each muted user in
+// the channel. If there are no muted user or if the channel does not exist,
+// an empty list is returned.
+//
+// Parameters:
+//   - args[0] - Marshalled bytes if the channel's [id.ID] (Uint8Array).
+//
+// Returns:
+//   - JSON of an array of ed25519.PublicKey (Uint8Array). Look below for an
+//     example.
+//   - Throws a TypeError if the channel ID cannot be unmarshalled.
+//
+// Example return:
+//
+//	["k2IrybDXjJtqxjS6Tx/6m3bXvT/4zFYOJnACNWTvESE=","ocELv7KyeCskLz4cm0klLWhmFLYvQL2FMDco79GTXYw=","mmxoDgoTEYwaRyEzq5Npa24IIs+3B5LXhll/8K5yCv0="]
+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)
+		return nil
+	}
+
+	return utils.CopyBytesToJS(mutedUsers)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Admin Management                                                           //
+////////////////////////////////////////////////////////////////////////////////
+
+// IsChannelAdmin returns true if the user is an admin of the channel.
+//
+// Parameters:
+//   - args[0] - The marshalled bytes of the channel's [id.ID] (Uint8Array)
+//
+// 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.
+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)
+		return nil
+	}
+
+	return isAdmin
+}
+
+// ExportChannelAdminKey gets the private key for the given channel ID, encrypts
+// it with the provided encryptionPassword, and exports it into a portable
+// format. Returns an error if the user is not an admin of the channel.
+//
+// This key can be provided to other users in a channel to grant them admin
+// access using [ChannelsManager.ImportChannelAdminKey].
+//
+// The private key is encrypted using a key generated from the password using
+// Argon2. Each call to ExportChannelAdminKey produces a different encrypted
+// packet regardless if the same password is used for the same channel. It
+// cannot be determined which channel the payload is for nor that two payloads
+// are for the same channel.
+//
+// The passwords between each call are not related. They can be the same or
+// different with no adverse impact on the security properties.
+//
+// Parameters:
+//   - args[0] - Marshalled bytes of the channel's [id.ID] (Uint8Array).
+//   - args[1] - The password used to encrypt the private key (string). The
+//     passwords between each call are not related. They can be the same or
+//     different with no adverse impact on the security properties.
+//
+// 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.
+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)
+		return nil
+	}
+	return utils.CopyBytesToJS(pk)
+}
+
+// VerifyChannelAdminKey verifies that the encrypted private key can be
+// decrypted and that it matches the expected channel. Returns false if private
+// key does not belong to the given channel.
+//
+// Parameters:
+//   - args[0] - Marshalled bytes of the channel's [id.ID] (Uint8Array).
+//   - args[1] - The password used to encrypt the private key (string)
+//   - args[2] - The encrypted channel private key packet (Uint8Array).
+//
+// Returns:
+//   - Returns false if private key does not belong to the given channel ID
+//     (boolean).
+//   - Throws a TypeError 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
+//     invalid password.
+//   - Throws a TypeError 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])
+	encryptionPassword := args[1].String()
+	encryptedPrivKey := utils.CopyBytesToGo(args[2])
+	valid, err := cm.api.VerifyChannelAdminKey(
+		channelID, encryptionPassword, encryptedPrivKey)
+	if err != nil {
+		utils.Throw(utils.TypeError, err)
+		return nil
+	}
+
+	return valid
+}
+
+// ImportChannelAdminKey decrypts and imports the given encrypted private key
+// and grants the user admin access to the channel the private key belongs to.
+// Returns an error if the private key cannot be decrypted or if the private key
+// is for the wrong channel.
+//
+// Parameters:
+//   - args[0] - Marshalled bytes of the channel's [id.ID] (Uint8Array).
+//   - args[1] - The password used to encrypt the private key (string)
+//   - args[2] - The encrypted channel private key packet (Uint8Array).
+//
+// Returns:
+//   - Throws a TypeError 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
+//     invalid password.
+//   - Throws a TypeError with the message [channels.ChannelDoesNotExistsErr] if
+//     the channel has not already been joined.
+//   - Throws a TypeError 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])
+	encryptionPassword := args[1].String()
+	encryptedPrivKey := utils.CopyBytesToGo(args[2])
+	err := cm.api.ImportChannelAdminKey(
+		channelID, encryptionPassword, encryptedPrivKey)
+	if err != nil {
+		utils.Throw(utils.TypeError, err)
+		return nil
+	}
+
+	return nil
+}
+
+// DeleteChannelAdminKey deletes the private key for the given channel.
+//
+// CAUTION: This will remove admin access. This cannot be undone. If the
+// private key is deleted, it cannot be recovered and the channel can never
+// have another admin.
+//
+// Parameters:
+//   - args[0] - The marshalled bytes of the channel's [id.ID] (Uint8Array)
+//
+// Returns:
+//   - Throws a TypeError 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)
+		return nil
+	}
+
+	return nil
+}
+
 ////////////////////////////////////////////////////////////////////////////////
 // Channel Receiving Logic and Callback Registration                          //
 ////////////////////////////////////////////////////////////////////////////////
@@ -1225,15 +1554,28 @@ func (cmrCB *channelMessageReceptionCallback) Callback(
 //   - args[1] - Javascript object that has functions that implement the
 //     [bindings.ChannelMessageReceptionCallback] interface. This callback will
 //     be executed when a channel message of the messageType is received.
+//   - args[2] - A name describing what type of messages the listener picks up.
+//     This is used for debugging and logging (string).
+//   - args[3] - Set to true if this listener can receive messages from normal
+//     users (boolean).
+//   - args[4] - Set to true if this listener can receive messages from admins
+//     (boolean).
+//   - args[5] - Set to true if this listener can receive messages from muted
+//     users (boolean).
 //
 // Returns:
 //   - Throws a TypeError if registering the handler fails.
-func (ch *ChannelsManager) RegisterReceiveHandler(_ js.Value, args []js.Value) any {
+func (cm *ChannelsManager) RegisterReceiveHandler(_ js.Value, args []js.Value) any {
 	messageType := args[0].Int()
 	listenerCb := &channelMessageReceptionCallback{
 		utils.WrapCB(args[1], "Callback")}
+	name := args[2].String()
+	userSpace := args[3].Bool()
+	adminSpace := args[4].Bool()
+	mutedSpace := args[5].Bool()
 
-	err := ch.api.RegisterReceiveHandler(messageType, listenerCb)
+	err := cm.api.RegisterReceiveHandler(
+		messageType, listenerCb, name, userSpace, adminSpace, mutedSpace)
 	if err != nil {
 		utils.Throw(utils.TypeError, err)
 		return nil
@@ -1246,15 +1588,41 @@ func (ch *ChannelsManager) RegisterReceiveHandler(_ js.Value, args []js.Value) a
 // Event Model Logic                                                          //
 ////////////////////////////////////////////////////////////////////////////////
 
+// eventModelBuilder adheres to the [bindings.EventModelBuilder] interface.
+type eventModelBuilder struct {
+	build func(args ...any) js.Value
+}
+
+// Build initializes and returns the event model.  It wraps a Javascript object
+// that has all the methods in [bindings.EventModel] to make it adhere to the Go
+// interface [bindings.EventModel].
+func (emb *eventModelBuilder) Build(path string) bindings.EventModel {
+	emJs := emb.build(path)
+	return &eventModel{
+		joinChannel:         utils.WrapCB(emJs, "JoinChannel"),
+		leaveChannel:        utils.WrapCB(emJs, "LeaveChannel"),
+		receiveMessage:      utils.WrapCB(emJs, "ReceiveMessage"),
+		receiveReply:        utils.WrapCB(emJs, "ReceiveReply"),
+		receiveReaction:     utils.WrapCB(emJs, "ReceiveReaction"),
+		updateFromUUID:      utils.WrapCB(emJs, "UpdateFromUUID"),
+		updateFromMessageID: utils.WrapCB(emJs, "UpdateFromMessageID"),
+		getMessage:          utils.WrapCB(emJs, "GetMessage"),
+		deleteMessage:       utils.WrapCB(emJs, "DeleteMessage"),
+	}
+}
+
 // eventModel wraps Javascript callbacks to adhere to the [bindings.EventModel]
 // interface.
 type eventModel struct {
-	joinChannel      func(args ...any) js.Value
-	leaveChannel     func(args ...any) js.Value
-	receiveMessage   func(args ...any) js.Value
-	receiveReply     func(args ...any) js.Value
-	receiveReaction  func(args ...any) js.Value
-	updateSentStatus func(args ...any) js.Value
+	joinChannel         func(args ...any) js.Value
+	leaveChannel        func(args ...any) js.Value
+	receiveMessage      func(args ...any) js.Value
+	receiveReply        func(args ...any) js.Value
+	receiveReaction     func(args ...any) js.Value
+	updateFromUUID      func(args ...any) js.Value
+	updateFromMessageID func(args ...any) js.Value
+	getMessage          func(args ...any) js.Value
+	deleteMessage       func(args ...any) js.Value
 }
 
 // JoinChannel is called whenever a channel is joined locally.
@@ -1284,6 +1652,7 @@ func (em *eventModel) LeaveChannel(channelID []byte) {
 //   - 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).
 //   - timestamp - Time the message was received; represented as nanoseconds
 //     since unix epoch (int).
@@ -1302,12 +1671,12 @@ func (em *eventModel) LeaveChannel(channelID []byte) {
 //   - A non-negative unique UUID for the message that it can be referenced by
 //     later with [eventModel.UpdateSentStatus].
 func (em *eventModel) ReceiveMessage(channelID, messageID []byte, nickname,
-	text string, pubKey []byte, codeset int, timestamp, lease, roundId, msgType,
-	status int64) int64 {
+	text string, pubKey []byte, dmToken int32, codeset int, timestamp, lease, roundId, msgType,
+	status int64, hidden bool) int64 {
 	uuid := em.receiveMessage(utils.CopyBytesToJS(channelID),
 		utils.CopyBytesToJS(messageID), nickname, text,
-		utils.CopyBytesToJS(pubKey), codeset, timestamp, lease, roundId,
-		msgType, status)
+		utils.CopyBytesToJS(pubKey), dmToken, codeset, timestamp, lease, roundId,
+		msgType, status, hidden)
 
 	return int64(uuid.Int())
 }
@@ -1328,6 +1697,7 @@ func (em *eventModel) ReceiveMessage(channelID, messageID []byte, nickname,
 //   - senderUsername - The username 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).
 //   - timestamp - Time the message was received; represented as nanoseconds
 //     since unix epoch (int).
@@ -1346,12 +1716,12 @@ func (em *eventModel) ReceiveMessage(channelID, messageID []byte, nickname,
 //   - A non-negative unique UUID for the message that it can be referenced by
 //     later with [eventModel.UpdateSentStatus].
 func (em *eventModel) ReceiveReply(channelID, messageID, reactionTo []byte,
-	senderUsername, text string, pubKey []byte, codeset int, timestamp, lease,
-	roundId, msgType, status int64) int64 {
+	senderUsername, text string, pubKey []byte, dmToken int32, codeset int, timestamp, lease,
+	roundId, msgType, status int64, hidden bool) int64 {
 	uuid := em.receiveReply(utils.CopyBytesToJS(channelID),
 		utils.CopyBytesToJS(messageID), utils.CopyBytesToJS(reactionTo),
-		senderUsername, text, utils.CopyBytesToJS(pubKey), codeset,
-		timestamp, lease, roundId, msgType, status)
+		senderUsername, text, utils.CopyBytesToJS(pubKey), dmToken, codeset,
+		timestamp, lease, roundId, msgType, status, hidden)
 
 	return int64(uuid.Int())
 }
@@ -1372,6 +1742,7 @@ func (em *eventModel) ReceiveReply(channelID, messageID, reactionTo []byte,
 //   - senderUsername - The username of the sender of the message (string).
 //   - reaction - The contents of the reaction message (string).
 //   - pubKey - The sender's Ed25519 public key (Uint8Array).
+//   - dmToken - The dmToken (int32).
 //   - codeset - The codeset version (int).
 //   - timestamp - Time the message was received; represented as nanoseconds
 //     since unix epoch (int).
@@ -1390,37 +1761,119 @@ func (em *eventModel) ReceiveReply(channelID, messageID, reactionTo []byte,
 //   - A non-negative unique UUID for the message that it can be referenced by
 //     later with [eventModel.UpdateSentStatus].
 func (em *eventModel) ReceiveReaction(channelID, messageID, reactionTo []byte,
-	senderUsername, reaction string, pubKey []byte, codeset int, timestamp,
-	lease, roundId, msgType, status int64) int64 {
+	senderUsername, reaction string, pubKey []byte, dmToken int32, codeset int, timestamp,
+	lease, roundId, msgType, status int64, hidden bool) int64 {
 	uuid := em.receiveReaction(utils.CopyBytesToJS(channelID),
 		utils.CopyBytesToJS(messageID), utils.CopyBytesToJS(reactionTo),
-		senderUsername, reaction, utils.CopyBytesToJS(pubKey), codeset,
-		timestamp, lease, roundId, msgType, status)
+		senderUsername, reaction, utils.CopyBytesToJS(pubKey), dmToken, codeset,
+		timestamp, lease, roundId, msgType, status, hidden)
 
 	return int64(uuid.Int())
 }
 
-// UpdateSentStatus is called whenever the sent status of a message has
-// changed.
+// UpdateFromUUID is called whenever a message at the UUID is modified.
+//
+// Parameters:
+//   - uuid - The unique identifier of the message in the database (int).
+//   - messageUpdateInfoJSON - JSON of [bindings.MessageUpdateInfo]
+//     (Uint8Array).
+func (em *eventModel) UpdateFromUUID(uuid int64, messageUpdateInfoJSON []byte) {
+	em.updateFromUUID(uuid, utils.CopyBytesToJS(messageUpdateInfoJSON))
+}
+
+// UpdateFromMessageID is called whenever a message with the message ID is
+// modified.
 //
 // Parameters:
-//   - uuid - The unique identifier for the message (int).
 //   - messageID - The bytes of the [channel.MessageID] of the received message
 //     (Uint8Array).
-//   - timestamp - Time the message was received; represented as nanoseconds
-//     since unix epoch (int).
-//   - roundId - The ID of the round that the message was received on (int).
-//   - status - The [channels.SentStatus] of the message (int).
+//   - messageUpdateInfoJSON - JSON of [bindings.MessageUpdateInfo
+//     (Uint8Array).
 //
-// Statuses will be enumerated as such:
+// Returns:
+//   - A non-negative unique uuid for the modified message by which it can be
+//     referenced later with [EventModel.UpdateFromUUID] int).
+func (em *eventModel) UpdateFromMessageID(
+	messageID []byte, messageUpdateInfoJSON []byte) int64 {
+	return int64(em.updateFromMessageID(utils.CopyBytesToJS(messageID),
+		utils.CopyBytesToJS(messageUpdateInfoJSON)).Int())
+}
+
+// GetMessage returns the message with the given [channel.MessageID].
 //
-//	Sent      =  0
-//	Delivered =  1
-//	Failed    =  2
-func (em *eventModel) UpdateSentStatus(
-	uuid int64, messageID []byte, timestamp, roundID, status int64) {
-	em.updateSentStatus(
-		uuid, utils.CopyBytesToJS(messageID), timestamp, roundID, status)
+// Note for developers: The internal Javascript function must return JSON of
+// MessageAndError, which includes the returned [channels.ModelMessage] or any
+// error that occurs during lookup.
+//
+// Parameters:
+//   - messageID - The bytes of the [channel.MessageID] of the message
+//     (Uint8Array).
+//
+// Returns:
+//   - JSON of [channels.ModelMessage] (Uint8Array).
+func (em *eventModel) GetMessage(messageID []byte) ([]byte, error) {
+	messageAndErrorBytes :=
+		utils.CopyBytesToGo(em.getMessage(utils.CopyBytesToJS(messageID)))
+
+	var mae MessageAndError
+	err := json.Unmarshal(messageAndErrorBytes, &mae)
+	if err != nil {
+		return nil, err
+	}
+
+	if mae.Error != "" {
+		return nil, errors.New(mae.Error)
+	}
+
+	return json.Marshal(mae.ModelMessage)
+}
+
+// DeleteMessage deletes the message with the given [channel.MessageID] from
+// the database.
+//
+// Parameters:
+//  - messageID - The bytes of the [channel.MessageID] of the message.
+func (em *eventModel) DeleteMessage(messageID []byte) error {
+	err := em.deleteMessage(utils.CopyBytesToJS(messageID))
+	if !err.IsUndefined() {
+		return js.Error{Value: err}
+	}
+
+	return nil
+}
+
+// MessageAndError contains a message returned by eventModel.GetMessage or any
+// possible error that occurs during lookup. Only one field should be present at
+// a time; if an error occurs, ModelMessage should be empty.
+//
+// Example JSON:
+//
+//	{
+//	  "ModelMessage": {
+//	    "UUID": 50,
+//	    "Nickname": "Nickname",
+//	    "MessageID": "ODg5goFIBvpvqPzuoYGqmvxFYBgj0MMiQxAB51Q2nPs=",
+//	    "ChannelID": "R+xKJTH6m4YRS4f0JggK3fTu10sANmtahS0Qtc8yi/AD",
+//	    "ParentMessageID": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
+//	    "Timestamp": "1955-11-05T12:01:00-07:00",
+//	    "Lease": 21600000000000,
+//	    "Status": 2,
+//	    "Hidden": false,
+//	    "Pinned": false,
+//	    "Content": "VGhpcyBpcyBzb21lIG1lc3NhZ2UgY29udGVudC4=",
+//	    "Type": 1,
+//	    "Round": 7,
+//	    "PubKey": "QyTtpndOf3sDZehVpOBQzQNBe1R2Eae7qlAEDZJ2mLg=",
+//	    "CodesetVersion": 0
+//	  },
+//	  "Error": ""
+//	}
+type MessageAndError struct {
+	// MessageJSON should contain the JSON of channels.ModelMessage.
+	ModelMessage channels.ModelMessage
+
+	// Error should only be filled when an error occurs on message lookup.
+	Error string
 }
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -1542,8 +1995,7 @@ func (c *ChannelDbCipher) MarshalJSON(js.Value, []js.Value) any {
 	return utils.CopyBytesToJS(data)
 }
 
-// UnmarshalJSON unmarshalls JSON into the cipher. This function adheres to the
-// json.Unmarshaler interface.
+// 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.
diff --git a/wasm/connect.go b/wasm/connect.go
index 5d8e347dbb34c9adf4f724eee13a3eda43fc4f57..bfb95a6d882f808277a856dfbda9937c3bb3d687 100644
--- a/wasm/connect.go
+++ b/wasm/connect.go
@@ -80,10 +80,6 @@ func (c *Cmix) Connect(_ js.Value, args []js.Value) any {
 // SendE2E is a wrapper for sending specifically to the [Connection]'s
 // [partner.Manager].
 //
-// Returns:
-//   - []byte - the JSON marshalled bytes of the E2ESendReport object, which can
-//     be passed into [Cmix.WaitForRoundResult] to see if the send succeeded.
-//
 // Parameters:
 //   - args[0] - Message type from [catalog.MessageType] (int).
 //   - args[1] - Message payload (Uint8Array).
diff --git a/wasm/dm.go b/wasm/dm.go
new file mode 100644
index 0000000000000000000000000000000000000000..678ca74f529f1ccce1a0a8cac63242b5073ed835
--- /dev/null
+++ b/wasm/dm.go
@@ -0,0 +1,872 @@
+////////////////////////////////////////////////////////////////////////////////
+// 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 (
+	"crypto/ed25519"
+	"syscall/js"
+
+	indexDB "gitlab.com/elixxir/xxdk-wasm/indexedDb/dm"
+
+	"encoding/base64"
+
+	"gitlab.com/elixxir/client/v4/bindings"
+	"gitlab.com/elixxir/crypto/codename"
+	"gitlab.com/elixxir/xxdk-wasm/utils"
+)
+
+////////////////////////////////////////////////////////////////////////////////
+// Basic Channel API                                                          //
+////////////////////////////////////////////////////////////////////////////////
+
+// DMClient wraps the [bindings.DMClient] object so its methods
+// can be wrapped to be Javascript compatible.
+type DMClient struct {
+	api *bindings.DMClient
+}
+
+// newDMClientJS creates a new Javascript compatible object
+// (map[string]any) that matches the [DMClient] structure.
+func newDMClientJS(api *bindings.DMClient) map[string]any {
+	cm := DMClient{api}
+	dmClientMap := map[string]any{
+		// Basic Channel API
+		"GetID": js.FuncOf(cm.GetID),
+
+		// Identity and Nickname Controls
+		"GetPublicKey":          js.FuncOf(cm.GetPublicKey),
+		"GetToken":              js.FuncOf(cm.GetToken),
+		"GetIdentity":           js.FuncOf(cm.GetIdentity),
+		"ExportPrivateIdentity": js.FuncOf(cm.ExportPrivateIdentity),
+		"SetNickname":           js.FuncOf(cm.SetNickname),
+		"GetNickname":           js.FuncOf(cm.GetNickname),
+
+		// 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),
+	}
+
+	return dmClientMap
+}
+
+// GetPublicKey returns the ecdh Public Key for this [DMClient] in the
+// [DMClient] tracker.
+//
+// Returns:
+//   - Tracker ID (int).
+func (ch *DMClient) GetID(js.Value, []js.Value) any {
+	return ch.api.GetID()
+}
+
+func (ch *DMClient) GetPublicKey(js.Value, []js.Value) any {
+	return ch.api.GetPublicKey()
+}
+
+func (ch *DMClient) GetToken(js.Value, []js.Value) any {
+	return ch.api.GetToken()
+}
+
+// dmReceiverBuilder adheres to the [bindings.DMReceiverBuilder] interface.
+type dmReceiverBuilder struct {
+	build func(args ...any) js.Value
+}
+
+// Build initializes and returns the event model.  It wraps a Javascript object
+// that has all the methods in [bindings.EventModel] to make it adhere to the Go
+// interface [bindings.EventModel].
+func (emb *dmReceiverBuilder) Build(path string) bindings.DMReceiver {
+	emJs := emb.build(path)
+	return &dmReceiver{
+		receive:          utils.WrapCB(emJs, "ReceiveText"),
+		receiveText:      utils.WrapCB(emJs, "ReceiveText"),
+		receiveReply:     utils.WrapCB(emJs, "ReceiveReply"),
+		receiveReaction:  utils.WrapCB(emJs, "ReceiveReaction"),
+		updateSentStatus: utils.WrapCB(emJs, "UpdateSentStatus"),
+	}
+}
+
+// NewDMClient creates a new [DMClient] from a new private
+// identity ([channel.PrivateIdentity]).
+//
+// This is for creating a manager for an identity for the first time. For
+// generating a new one channel identity, use [GenerateChannelIdentity]. To
+// reload this channel manager, use [LoadDMClient], passing in the
+// storage tag retrieved by [DMClient.GetStorageTag].
+//
+// Parameters:
+//   - args[0] - ID of [Cmix] object in tracker (int). This can be retrieved
+//     using [Cmix.GetID].
+//   - args[1] - Bytes of a private identity ([channel.PrivateIdentity]) that is
+//     generated by [GenerateChannelIdentity] (Uint8Array).
+//   - args[2] - A function that initialises and returns a Javascript object
+//     that matches the [bindings.EventModel] interface. The function must match
+//     the Build function in [bindings.EventModelBuilder].
+//
+// Returns:
+//   - Javascript representation of the [DMClient] object.
+//   - Throws a TypeError if creating the manager fails.
+func NewDMClient(_ js.Value, args []js.Value) any {
+	privateIdentity := utils.CopyBytesToGo(args[1])
+
+	em := &dmReceiverBuilder{args[2].Invoke}
+
+	cm, err := bindings.NewDMClient(args[0].Int(), privateIdentity, em)
+	if err != nil {
+		utils.Throw(utils.TypeError, err)
+		return nil
+	}
+
+	return newDMClientJS(cm)
+}
+
+// NewDMClientWithIndexedDb creates a new [DMClient] from a new
+// private identity ([channel.PrivateIdentity]) and using indexedDb as a backend
+// to manage the event model.
+//
+// This is for creating a manager for an identity for the first time. For
+// generating a new one channel identity, use [GenerateChannelIdentity]. To
+// reload this channel manager, use [LoadDMClientWithIndexedDb], passing
+// in the storage tag retrieved by [DMClient.GetStorageTag].
+//
+// This function initialises an indexedDb database.
+//
+// Parameters:
+//   - args[0] - ID of [Cmix] object in tracker (int). This can be retrieved
+//     using [Cmix.GetID].
+//   - args[1] - Bytes of a private identity ([channel.PrivateIdentity]) that is
+//     generated by [GenerateChannelIdentity] (Uint8Array).
+//   - args[2] - 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[3] - ID of [ChannelDbCipher] object in tracker (int). Create this
+//     object with [NewChannelsDatabaseCipher] and get its id with
+//     [ChannelDbCipher.GetID].
+//
+// Returns a promise:
+//   - Resolves to a Javascript representation of the [DMClient] 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.
+func NewDMClientWithIndexedDb(_ js.Value, args []js.Value) any {
+	cmixID := args[0].Int()
+	privateIdentity := utils.CopyBytesToGo(args[1])
+	messageReceivedCB := args[2]
+	cipherID := args[3].Int()
+
+	cipher, err := bindings.GetChannelDbCipherTrackerFromID(cipherID)
+	if err != nil {
+		utils.Throw(utils.TypeError, err)
+	}
+
+	return newDMClientWithIndexedDb(
+		cmixID, privateIdentity, messageReceivedCB, cipher)
+}
+
+// NewDMClientWithIndexedDbUnsafe creates a new [DMClient] from a
+// new private identity ([channel.PrivateIdentity]) and using indexedDb as a
+// backend to manage the event model. However, the data is written in plain text
+// and not encrypted. It is recommended that you do not use this in production.
+//
+// This is for creating a manager for an identity for the first time. For
+// generating a new one channel identity, use [GenerateChannelIdentity]. To
+// reload this channel manager, use [LoadDMClientWithIndexedDbUnsafe],
+// passing in the storage tag retrieved by [DMClient.GetStorageTag].
+//
+// This function initialises an indexedDb database.
+//
+// Parameters:
+//   - args[0] - ID of [Cmix] object in tracker (int). This can be retrieved
+//     using [Cmix.GetID].
+//   - args[1] - Bytes of a private identity ([channel.PrivateIdentity]) that is
+//     generated by [GenerateChannelIdentity] (Uint8Array).
+//   - args[2] - 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
+//
+// Returns a promise:
+//   - Resolves to a Javascript representation of the [DMClient] object.
+//   - Rejected with an error if loading indexedDb or the manager fails.
+func NewDMClientWithIndexedDbUnsafe(_ js.Value, args []js.Value) any {
+	cmixID := args[0].Int()
+	privateIdentity := utils.CopyBytesToGo(args[1])
+	messageReceivedCB := args[2]
+
+	return newDMClientWithIndexedDb(
+		cmixID, privateIdentity, messageReceivedCB, nil)
+}
+
+func newDMClientWithIndexedDb(cmixID int, privateIdentity []byte,
+	cb js.Value, cipher *bindings.ChannelDbCipher) any {
+
+	messageReceivedCB := func(uuid uint64, pubKey ed25519.PublicKey,
+		update bool) {
+		cb.Invoke(uuid, utils.CopyBytesToJS(pubKey[:]), update)
+	}
+
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+
+		pi, err := codename.UnmarshalPrivateIdentity(privateIdentity)
+		if err != nil {
+			reject(utils.JsTrace(err))
+		}
+		dmPath := base64.RawStdEncoding.EncodeToString(pi.PubKey[:])
+		model, err := indexDB.NewWASMEventModel(dmPath, cipher,
+			messageReceivedCB)
+		if err != nil {
+			reject(utils.JsTrace(err))
+		}
+
+		cm, err := bindings.NewDMClientWithGoEventModel(
+			cmixID, privateIdentity, model)
+		if err != nil {
+			reject(utils.JsTrace(err))
+		} else {
+			resolve(newDMClientJS(cm))
+		}
+	}
+
+	return utils.CreatePromise(promiseFn)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Channel Sending Methods and Reports                                        //
+////////////////////////////////////////////////////////////////////////////////
+
+// SendGeneric is used to send a raw message over a channel. In general, it
+// should be wrapped in a function which defines the wire protocol. If the final
+// message, before being sent over the wire, is too long, this will return an
+// error. Due to the underlying encoding using compression, it isn't possible to
+// define the largest payload that can be sent, but it will always be possible
+// to send a payload of 802 bytes at minimum. The meaning of validUntil depends
+// on the use case.
+//
+// Parameters:
+//   - args[0] - Marshalled bytes of the channel [id.ID] (Uint8Array).
+//   - args[1] - The message type of the message. This will be a valid
+//     [channels.MessageType] (int).
+//   - args[2] - The contents of the message (Uint8Array).
+//   - args[3] - The lease of the message. This will be how long the message is
+//     valid until, in milliseconds. 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 (int).
+//   - args[4] - 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 (ch *DMClient) Send(_ js.Value, args []js.Value) any {
+	messageType := args[0].Int()
+	partnerPubKeyBytes := utils.CopyBytesToGo(args[1])
+	partnerToken := args[2].Int()
+	message := utils.CopyBytesToGo(args[3])
+	leaseTimeMS := int64(args[4].Int())
+	cmixParamsJSON := utils.CopyBytesToGo(args[5])
+
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		sendReport, err := ch.api.Send(messageType, partnerPubKeyBytes,
+			uint32(partnerToken), message, leaseTimeMS,
+			cmixParamsJSON)
+		if err != nil {
+			reject(utils.JsTrace(err))
+		} else {
+			resolve(utils.CopyBytesToJS(sendReport))
+		}
+	}
+
+	return utils.CreatePromise(promiseFn)
+}
+
+// SendMessage is used to send a formatted message over a channel.
+// Due to the underlying encoding using compression, it isn't possible to define
+// the largest payload that can be sent, but it will always be possible to send
+// a payload of 798 bytes at minimum.
+//
+// The message will auto delete validUntil after the round it is sent in,
+// lasting forever if [channels.ValidForever] is used.
+//
+// Parameters:
+//   - args[0] - Marshalled bytes of the channel [id.ID] (Uint8Array).
+//   - args[1] - The contents of the message (string).
+//   - args[2] - The lease of the message. This will be how long the message is
+//     valid until, in milliseconds. 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 (int).
+//   - 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 (ch *DMClient) SendText(_ js.Value, args []js.Value) any {
+	partnerPubKeyBytes := utils.CopyBytesToGo(args[0])
+	partnerToken := args[1].Int()
+	message := args[2].String()
+	leaseTimeMS := int64(args[3].Int())
+	cmixParamsJSON := utils.CopyBytesToGo(args[4])
+
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		sendReport, err := ch.api.SendText(partnerPubKeyBytes,
+			uint32(partnerToken), message, leaseTimeMS,
+			cmixParamsJSON)
+		if err != nil {
+			reject(utils.JsTrace(err))
+		} else {
+			resolve(utils.CopyBytesToJS(sendReport))
+		}
+	}
+
+	return utils.CreatePromise(promiseFn)
+}
+
+// SendReply is used to send a formatted message over a channel. Due to the
+// underlying encoding using compression, it isn't possible to define the
+// largest payload that can be sent, but it will always be possible to send a
+// payload of 766 bytes at minimum.
+//
+// If the message ID the reply is sent to is nonexistent, the other side will
+// post the message as a normal message and not a reply. The message will auto
+// delete validUntil after the round it is sent in, lasting forever if
+// [channels.ValidForever] is used.
+//
+// Parameters:
+//   - args[0] - Marshalled bytes of the channel [id.ID] (Uint8Array).
+//   - args[1] - 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 (string).
+//   - args[2] - JSON of [channel.MessageID] of the message you wish to reply
+//     to. This may be found in the [bindings.ChannelSendReport] if replying to
+//     your own. Alternatively, if reacting to another user's message, you may
+//     retrieve it via the [bindings.ChannelMessageReceptionCallback] registered
+//     using  RegisterReceiveHandler (Uint8Array).
+//   - args[3] - The lease of the message. This will be how long the message is
+//     valid until, in milliseconds. 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 (int).
+//   - args[4] - 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 (ch *DMClient) SendReply(_ js.Value, args []js.Value) any {
+	partnerPubKeyBytes := utils.CopyBytesToGo(args[0])
+	partnerToken := args[1].Int()
+	replyID := utils.CopyBytesToGo(args[2])
+	message := args[3].String()
+	leaseTimeMS := int64(args[4].Int())
+	cmixParamsJSON := utils.CopyBytesToGo(args[5])
+
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		sendReport, err := ch.api.SendReply(partnerPubKeyBytes,
+			uint32(partnerToken), message, replyID, leaseTimeMS,
+			cmixParamsJSON)
+		if err != nil {
+			reject(utils.JsTrace(err))
+		} else {
+			resolve(utils.CopyBytesToJS(sendReport))
+		}
+	}
+
+	return utils.CreatePromise(promiseFn)
+}
+
+// SendReaction is used to send a reaction to a message over a channel.
+// The reaction must be a single emoji with no other characters, and will
+// be rejected otherwise.
+// 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[1] - The user's reaction. This should be a single emoji with no
+//     other characters. As such, a Unicode string is expected (string).
+//   - args[2] - JSON of [channel.MessageID] of the message you wish to reply
+//     to. This may be found in the [bindings.ChannelSendReport] if replying to
+//     your own. Alternatively, if reacting to another user's message, you may
+//     retrieve it via the ChannelMessageReceptionCallback registered using
+//     RegisterReceiveHandler (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 (ch *DMClient) SendReaction(_ js.Value, args []js.Value) any {
+	partnerPubKeyBytes := utils.CopyBytesToGo(args[0])
+	partnerToken := args[1].Int()
+	replyID := utils.CopyBytesToGo(args[2])
+	message := args[3].String()
+	cmixParamsJSON := utils.CopyBytesToGo(args[4])
+
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		sendReport, err := ch.api.SendReaction(partnerPubKeyBytes,
+			uint32(partnerToken), message, replyID,
+			cmixParamsJSON)
+		if err != nil {
+			reject(utils.JsTrace(err))
+		} else {
+			resolve(utils.CopyBytesToJS(sendReport))
+		}
+	}
+
+	return utils.CreatePromise(promiseFn)
+}
+
+// GetIdentity returns the marshaled public identity ([codename.Identity]) that
+// the client is using.
+//
+// Returns:
+//   - JSON of the [channel.Identity] (Uint8Array).
+//   - Throws TypeError if marshalling the identity fails.
+func (ch *DMClient) GetIdentity(js.Value, []js.Value) any {
+	i := ch.api.GetIdentity()
+
+	return utils.CopyBytesToJS(i)
+}
+
+// ExportPrivateIdentity encrypts and exports the private identity to a portable
+// string.
+//
+// Parameters:
+//   - args[0] - Password to encrypt the identity with (string).
+//
+// Returns:
+//   - JSON of the encrypted private identity (Uint8Array).
+//   - Throws TypeError if exporting the identity fails.
+func (ch *DMClient) ExportPrivateIdentity(_ js.Value, args []js.Value) any {
+	i, err := ch.api.ExportPrivateIdentity(args[0].String())
+	if err != nil {
+		utils.Throw(utils.TypeError, err)
+		return nil
+	}
+
+	return utils.CopyBytesToJS(i)
+}
+
+// SetNickname sets the nickname for a given channel. The nickname must be valid
+// according to [IsNicknameValid].
+//
+// Parameters:
+//   - args[0] - The nickname to set (string).
+//   - args[1] - Marshalled bytes if the channel's [id.ID] (Uint8Array).
+//
+// Returns:
+//   - Throws TypeError if unmarshalling the ID fails or the nickname is
+//     invalid.
+func (ch *DMClient) SetNickname(_ js.Value, args []js.Value) any {
+	ch.api.SetNickname(args[0].String())
+	return nil
+}
+
+// GetNickname returns the nickname set for a given channel. Returns an error if
+// there is no nickname set.
+//
+// Parameters:
+//   - args[0] - Marshalled bytes if the channel's [id.ID] (Uint8Array).
+//
+// Returns:
+//   - The nickname (string).
+//   - Throws TypeError if the channel has no nickname set.
+func (ch *DMClient) GetNickname(_ js.Value, args []js.Value) any {
+	nickname, err := ch.api.GetNickname(utils.CopyBytesToGo(args[0]))
+	if err != nil {
+		utils.Throw(utils.TypeError, err)
+		return nil
+	}
+
+	return nickname
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// 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.
+//
+// Parameters:
+//   - receivedChannelMessageReport - Returns the JSON of
+//     [bindings.ReceivedChannelMessageReport] (Uint8Array).
+//   - err - Returns an error on failure (Error).
+//
+// 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()
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Event Model Logic                                                          //
+////////////////////////////////////////////////////////////////////////////////
+
+// dmReceiver wraps Javascript callbacks to adhere to the [bindings.EventModel]
+// interface.
+type dmReceiver struct {
+	receive          func(args ...any) js.Value
+	receiveText      func(args ...any) js.Value
+	receiveReply     func(args ...any) js.Value
+	receiveReaction  func(args ...any) js.Value
+	updateSentStatus func(args ...any) js.Value
+}
+
+// ReceiveMessage is called whenever a message is received 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.
+//
+// Parameters:
+//   - channelID - Marshalled bytes of the channel [id.ID] (Uint8Array).
+//   - messageID - The bytes of the [channel.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).
+//   - 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).
+//
+// Statuses will be enumerated as such:
+//
+//	Sent      =  0
+//	Delivered =  1
+//	Failed    =  2
+//
+// 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, pubKey []byte, dmToken int32, codeset int, timestamp,
+	roundId, mType, status int64) int64 {
+	uuid := em.receive(messageID, nickname, text, pubKey, 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.
+//
+// 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
+//     (Uint8Array).
+//   - reactionTo - The [channel.MessageID] for the message that received a
+//     reply (Uint8Array).
+//   - senderUsername - The username 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).
+//   - 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).
+//
+// Statuses will be enumerated as such:
+//
+//	Sent      =  0
+//	Delivered =  1
+//	Failed    =  2
+//
+// Returns:
+//   - A non-negative unique UUID for the message that it can be referenced by
+//     later with [dmReceiver.UpdateSentStatus].
+func (em *dmReceiver) ReceiveText(messageID []byte, nickname, text string,
+	pubKey []byte, dmToken int32, codeset int, timestamp,
+	roundId, status int64) int64 {
+
+	uuid := em.receiveText(messageID, nickname, text, pubKey, 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.
+//
+// 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
+//     (Uint8Array).
+//   - reactionTo - The [channel.MessageID] for the message that received a
+//     reply (Uint8Array).
+//   - senderUsername - The username 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).
+//   - 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).
+//
+// Statuses will be enumerated as such:
+//
+//	Sent      =  0
+//	Delivered =  1
+//	Failed    =  2
+//
+// 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,
+	text string, pubKey []byte, dmToken int32, codeset int,
+	timestamp, roundId, status int64) int64 {
+	uuid := em.receiveReply(messageID, replyTo, nickname, text, pubKey,
+		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
+// 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
+//     (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).
+//   - pubKey - The sender's Ed25519 public key (Uint8Array).
+//   - dmToken - The dmToken (int32).
+//   - 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).
+//
+// Statuses will be enumerated as such:
+//
+//	Sent      =  0
+//	Delivered =  1
+//	Failed    =  2
+//
+// Returns:
+//   - A non-negative unique UUID for the message that it can be referenced by
+//     later with [dmReceiver.UpdateSentStatus].
+func (em *dmReceiver) ReceiveReaction(messageID, reactionTo []byte,
+	nickname, reaction string, pubKey []byte, dmToken int32,
+	codeset int, timestamp, roundId,
+	status int64) int64 {
+	uuid := em.receiveReaction(messageID, reactionTo, nickname, reaction,
+		pubKey, dmToken, codeset, timestamp, roundId, status)
+
+	return int64(uuid.Int())
+}
+
+// UpdateSentStatus is called whenever the sent status of a message has
+// changed.
+//
+// Parameters:
+//   - uuid - The unique identifier for the message (int).
+//   - messageID - The bytes of the [channel.MessageID] of the received message
+//     (Uint8Array).
+//   - timestamp - Time the message was received; represented as nanoseconds
+//     since unix epoch (int).
+//   - roundId - The ID of the round that the message was received on (int).
+//   - status - The [channels.SentStatus] of the message (int).
+//
+// Statuses will be enumerated as such:
+//
+//	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)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// 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
+}
diff --git a/wasm/docs.go b/wasm/docs.go
index 33b0d56ed2951fa2e98c6217f09e77ebe4024fb4..b2d6f4894014b15873b2649118e2af09a8cd7091 100644
--- a/wasm/docs.go
+++ b/wasm/docs.go
@@ -10,6 +10,7 @@
 package wasm
 
 import (
+	"crypto/ed25519"
 	"github.com/spf13/jwalterweatherman"
 	"gitlab.com/elixxir/client/v4/auth"
 	"gitlab.com/elixxir/client/v4/catalog"
@@ -23,11 +24,11 @@ import (
 	"gitlab.com/elixxir/client/v4/restlike"
 	"gitlab.com/elixxir/client/v4/single"
 	"gitlab.com/elixxir/crypto/broadcast"
-	"gitlab.com/elixxir/crypto/channel"
 	"gitlab.com/elixxir/crypto/contact"
 	"gitlab.com/elixxir/crypto/cyclic"
 	"gitlab.com/elixxir/crypto/fileTransfer"
 	"gitlab.com/elixxir/crypto/group"
+	cryptoMessage "gitlab.com/elixxir/crypto/message"
 	"gitlab.com/elixxir/primitives/fact"
 	"gitlab.com/elixxir/primitives/format"
 	"gitlab.com/xx_network/primitives/id"
@@ -50,7 +51,7 @@ var (
 	_ = connect.Callback(nil)
 	_ = partner.Manager(nil)
 	_ = ndf.NetworkDefinition{}
-	_ = channel.MessageID{}
+	_ = cryptoMessage.ID{}
 	_ = channels.SentStatus(0)
 	_ = ftE2e.Params{}
 	_ = fileTransfer.TransferID{}
@@ -67,4 +68,5 @@ var (
 	_ = broadcast.PrivacyLevel(0)
 	_ = broadcast.Channel{}
 	_ = netTime.Now
+	_ = ed25519.PublicKey{}
 )
diff --git a/wasm_test.go b/wasm_test.go
index 97aab01dceb11834f53a541c57a4be2de0ccc069..68cc545ba79c45b67cf1eefdb596e28c69b5eba3 100644
--- a/wasm_test.go
+++ b/wasm_test.go
@@ -48,6 +48,12 @@ func TestPublicFunctions(t *testing.T) {
 		// client versions
 		"GetGitVersion":   {},
 		"GetDependencies": {},
+
+		// DM Functions these are used but not exported by
+		// WASM bindins, so are not exposed.
+		"NewDMReceiver":               {},
+		"NewDMClientWithGoEventModel": {},
+		"GetDMDbCipherTrackerFromID":  {},
 	}
 	wasmFuncs := getPublicFunctions("wasm", t)
 	bindingsFuncs := getPublicFunctions(