diff --git a/go.mod b/go.mod
index ae13c608d8398d72e03d345c3b1753501442c5ee..b41a262ff540e218f685e01353622c2db8d45d38 100644
--- a/go.mod
+++ b/go.mod
@@ -31,7 +31,7 @@ require (
 	github.com/gorilla/websocket v1.5.0 // indirect
 	github.com/hashicorp/hcl v1.0.0 // indirect
 	github.com/improbable-eng/grpc-web v0.15.0 // indirect
-	github.com/inconshreveable/mousetrap v1.0.0 // indirect
+	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/jinzhu/inflection v1.0.0 // indirect
 	github.com/jinzhu/now v1.1.5 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
@@ -54,7 +54,7 @@ require (
 	github.com/soheilhy/cmux v0.1.5 // indirect
 	github.com/spf13/afero v1.9.2 // indirect
 	github.com/spf13/cast v1.5.0 // indirect
-	github.com/spf13/cobra v1.5.0 // indirect
+	github.com/spf13/cobra v1.7.0 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
 	github.com/spf13/viper v1.12.0 // indirect
 	github.com/subosito/gotenv v1.4.0 // indirect
diff --git a/go.sum b/go.sum
index 34574840006813926e9b0b856e90d10cfb2002a9..75aad62399baf6290ffb2634114aa930ac1ffd3a 100644
--- a/go.sum
+++ b/go.sum
@@ -268,6 +268,8 @@ github.com/improbable-eng/grpc-web v0.15.0 h1:BN+7z6uNXZ1tQGcNAuaU1YjsLTApzkjt2t
 github.com/improbable-eng/grpc-web v0.15.0/go.mod h1:1sy9HKV4Jt9aEs9JSnkWlRJPuPtwNr0l57L4f878wP8=
 github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
 github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
 github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
@@ -452,6 +454,8 @@ github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155
 github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
 github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
 github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
+github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
+github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
 github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
 github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
 github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
diff --git a/indexedDb/impl/channels/callbacks.go b/indexedDb/impl/channels/callbacks.go
index df560535a779f2e6c355c6e49a66d4b45c2047e2..205a5e0df78b3cb6125042a963e5de8026fd4e08 100644
--- a/indexedDb/impl/channels/callbacks.go
+++ b/indexedDb/impl/channels/callbacks.go
@@ -32,24 +32,24 @@ var zeroUUID = []byte{0, 0, 0, 0, 0, 0, 0, 0}
 // manager handles the event model and the message callbacks, which is used to
 // send information between the event model and the main thread.
 type manager struct {
-	mh    *worker.ThreadManager
+	wtm   *worker.ThreadManager
 	model channels.EventModel
 }
 
 // registerCallbacks registers all the reception callbacks to manage messages
 // from the main thread for the channels.EventModel.
 func (m *manager) registerCallbacks() {
-	m.mh.RegisterCallback(wChannels.NewWASMEventModelTag, m.newWASMEventModelCB)
-	m.mh.RegisterCallback(wChannels.JoinChannelTag, m.joinChannelCB)
-	m.mh.RegisterCallback(wChannels.LeaveChannelTag, m.leaveChannelCB)
-	m.mh.RegisterCallback(wChannels.ReceiveMessageTag, m.receiveMessageCB)
-	m.mh.RegisterCallback(wChannels.ReceiveReplyTag, m.receiveReplyCB)
-	m.mh.RegisterCallback(wChannels.ReceiveReactionTag, m.receiveReactionCB)
-	m.mh.RegisterCallback(wChannels.UpdateFromUUIDTag, m.updateFromUUIDCB)
-	m.mh.RegisterCallback(wChannels.UpdateFromMessageIDTag, m.updateFromMessageIDCB)
-	m.mh.RegisterCallback(wChannels.GetMessageTag, m.getMessageCB)
-	m.mh.RegisterCallback(wChannels.DeleteMessageTag, m.deleteMessageCB)
-	m.mh.RegisterCallback(wChannels.MuteUserTag, m.muteUserCB)
+	m.wtm.RegisterCallback(wChannels.NewWASMEventModelTag, m.newWASMEventModelCB)
+	m.wtm.RegisterCallback(wChannels.JoinChannelTag, m.joinChannelCB)
+	m.wtm.RegisterCallback(wChannels.LeaveChannelTag, m.leaveChannelCB)
+	m.wtm.RegisterCallback(wChannels.ReceiveMessageTag, m.receiveMessageCB)
+	m.wtm.RegisterCallback(wChannels.ReceiveReplyTag, m.receiveReplyCB)
+	m.wtm.RegisterCallback(wChannels.ReceiveReactionTag, m.receiveReactionCB)
+	m.wtm.RegisterCallback(wChannels.UpdateFromUUIDTag, m.updateFromUUIDCB)
+	m.wtm.RegisterCallback(wChannels.UpdateFromMessageIDTag, m.updateFromMessageIDCB)
+	m.wtm.RegisterCallback(wChannels.GetMessageTag, m.getMessageCB)
+	m.wtm.RegisterCallback(wChannels.DeleteMessageTag, m.deleteMessageCB)
+	m.wtm.RegisterCallback(wChannels.MuteUserTag, m.muteUserCB)
 }
 
 // newWASMEventModelCB is the callback for NewWASMEventModel. Returns an empty
@@ -99,7 +99,7 @@ func (m *manager) messageReceivedCallback(
 	}
 
 	// Send it to the main thread
-	m.mh.SendMessage(wChannels.MessageReceivedCallbackTag, data)
+	m.wtm.SendMessage(wChannels.MessageReceivedCallbackTag, data)
 }
 
 // deletedMessageCallback sends calls to the channels.DeletedMessageCallback in
@@ -107,7 +107,7 @@ func (m *manager) messageReceivedCallback(
 //
 // storeEncryptionStatus adhere to the channels.MessageReceivedCallback type.
 func (m *manager) deletedMessageCallback(messageID message.ID) {
-	m.mh.SendMessage(wChannels.DeletedMessageCallbackTag, messageID.Marshal())
+	m.wtm.SendMessage(wChannels.DeletedMessageCallbackTag, messageID.Marshal())
 }
 
 // mutedUserCallback sends calls to the channels.MutedUserCallback in the main
@@ -129,7 +129,7 @@ func (m *manager) mutedUserCallback(
 	}
 
 	// Send it to the main thread
-	m.mh.SendMessage(wChannels.MutedUserCallbackTag, data)
+	m.wtm.SendMessage(wChannels.MutedUserCallbackTag, data)
 }
 
 // joinChannelCB is the callback for wasmModel.JoinChannel. Always returns nil;
diff --git a/indexedDb/impl/channels/channelsIndexedDbWorker.js b/indexedDb/impl/channels/channelsIndexedDbWorker.js
index 9e69bdd70eddebc9f82b23d04d823221ad3c1622..c109cba89735425f23c0d4d436532a3e3eb9a852 100644
--- a/indexedDb/impl/channels/channelsIndexedDbWorker.js
+++ b/indexedDb/impl/channels/channelsIndexedDbWorker.js
@@ -7,11 +7,15 @@
 
 importScripts('wasm_exec.js');
 
+const isReady = new Promise((resolve) => {
+    self.onWasmInitialized = resolve;
+});
+
 const go = new Go();
 const binPath = 'xxdk-channelsIndexedDkWorker.wasm'
-WebAssembly.instantiateStreaming(fetch(binPath), go.importObject).then((result) => {
+WebAssembly.instantiateStreaming(fetch(binPath), go.importObject).then(async (result) => {
     go.run(result.instance);
-    LogLevel(1);
+    await isReady;
 }).catch((err) => {
     console.error(err);
 });
\ No newline at end of file
diff --git a/indexedDb/impl/channels/main.go b/indexedDb/impl/channels/main.go
index 5290d0c89b6eedf92a1571aea04ff5b7fbfdc668..b84f13dc205ecd8bfe2466ea9ecb827770f31754 100644
--- a/indexedDb/impl/channels/main.go
+++ b/indexedDb/impl/channels/main.go
@@ -11,32 +11,70 @@ package main
 
 import (
 	"fmt"
+	"os"
+	"syscall/js"
+
+	"github.com/spf13/cobra"
 	jww "github.com/spf13/jwalterweatherman"
+
 	"gitlab.com/elixxir/xxdk-wasm/logging"
-	"gitlab.com/elixxir/xxdk-wasm/wasm"
 	"gitlab.com/elixxir/xxdk-wasm/worker"
-	"syscall/js"
 )
 
 // SEMVER is the current semantic version of the xxDK channels web worker.
 const SEMVER = "0.1.0"
 
-func init() {
-	// Set up Javascript console listener set at level INFO
-	ll := logging.NewJsConsoleLogListener(jww.LevelInfo)
-	logging.AddLogListener(ll.Listen)
-	jww.SetStdoutThreshold(jww.LevelFatal + 1)
-	jww.INFO.Printf("xxDK channels web worker version: v%s", SEMVER)
+func main() {
+	// Set to os.Args because the default is os.Args[1:] and in WASM, args start
+	// at 0, not 1.
+	channelsCmd.SetArgs(os.Args)
+
+	err := channelsCmd.Execute()
+	if err != nil {
+		fmt.Println(err)
+		os.Exit(1)
+	}
 }
 
-func main() {
-	jww.INFO.Print("[WW] Starting xxDK WebAssembly Channels Database Worker.")
+var channelsCmd = &cobra.Command{
+	Use:     "channelsIndexedDbWorker",
+	Short:   "IndexedDb database for channels.",
+	Example: "const go = new Go();\ngo.argv = [\"--logLevel=1\"]",
+	Run: func(cmd *cobra.Command, args []string) {
+		// Start logger first to capture all logging events
+		err := logging.EnableLogging(logLevel, -1, 0, "", "")
+		if err != nil {
+			fmt.Printf("Failed to intialize logging: %+v", err)
+			os.Exit(1)
+		}
 
-	js.Global().Set("LogLevel", js.FuncOf(wasm.LogLevel))
+		jww.INFO.Printf("xxDK channels web worker version: v%s", SEMVER)
 
-	m := &manager{mh: worker.NewThreadManager("ChannelsIndexedDbWorker", true)}
-	m.registerCallbacks()
-	m.mh.SignalReady()
-	<-make(chan bool)
-	fmt.Println("[WW] Closing xxDK WebAssembly Channels Database Worker.")
+		jww.INFO.Print("[WW] Starting xxDK WebAssembly Channels Database Worker.")
+		m := &manager{
+			wtm: worker.NewThreadManager("ChannelsIndexedDbWorker", true),
+		}
+		m.registerCallbacks()
+		m.wtm.SignalReady()
+
+		// Indicate to the Javascript caller that the WASM is ready by resolving
+		// a promise created by the caller.
+		js.Global().Get("onWasmInitialized").Invoke()
+
+		<-make(chan bool)
+		fmt.Println("[WW] Closing xxDK WebAssembly Channels Database Worker.")
+		os.Exit(0)
+	},
+}
+
+var (
+	logLevel jww.Threshold
+)
+
+func init() {
+	// Initialize all startup flags
+	channelsCmd.Flags().IntVarP((*int)(&logLevel), "logLevel", "l", 2,
+		"Sets the log level output when outputting to the Javascript console. "+
+			"0 = TRACE, 1 = DEBUG, 2 = INFO, 3 = WARN, 4 = ERROR, "+
+			"5 = CRITICAL, 6 = FATAL, -1 = disabled.")
 }
diff --git a/indexedDb/impl/dm/callbacks.go b/indexedDb/impl/dm/callbacks.go
index 5fe03874609ddfb4c92a4680494ac6c475568ae3..380f04d9953fdda65c4b0d80a66e7b40a7edc2b1 100644
--- a/indexedDb/impl/dm/callbacks.go
+++ b/indexedDb/impl/dm/callbacks.go
@@ -29,24 +29,24 @@ var zeroUUID = []byte{0, 0, 0, 0, 0, 0, 0, 0}
 // manager handles the event model and the message callbacks, which is used to
 // send information between the event model and the main thread.
 type manager struct {
-	mh    *worker.ThreadManager
+	wtm   *worker.ThreadManager
 	model dm.EventModel
 }
 
 // registerCallbacks registers all the reception callbacks to manage messages
 // from the main thread for the channels.EventModel.
 func (m *manager) registerCallbacks() {
-	m.mh.RegisterCallback(wDm.NewWASMEventModelTag, m.newWASMEventModelCB)
-	m.mh.RegisterCallback(wDm.ReceiveTag, m.receiveCB)
-	m.mh.RegisterCallback(wDm.ReceiveTextTag, m.receiveTextCB)
-	m.mh.RegisterCallback(wDm.ReceiveReplyTag, m.receiveReplyCB)
-	m.mh.RegisterCallback(wDm.ReceiveReactionTag, m.receiveReactionCB)
-	m.mh.RegisterCallback(wDm.UpdateSentStatusTag, m.updateSentStatusCB)
-
-	m.mh.RegisterCallback(wDm.BlockSenderTag, m.blockSenderCB)
-	m.mh.RegisterCallback(wDm.UnblockSenderTag, m.unblockSenderCB)
-	m.mh.RegisterCallback(wDm.GetConversationTag, m.getConversationCB)
-	m.mh.RegisterCallback(wDm.GetConversationsTag, m.getConversationsCB)
+	m.wtm.RegisterCallback(wDm.NewWASMEventModelTag, m.newWASMEventModelCB)
+	m.wtm.RegisterCallback(wDm.ReceiveTag, m.receiveCB)
+	m.wtm.RegisterCallback(wDm.ReceiveTextTag, m.receiveTextCB)
+	m.wtm.RegisterCallback(wDm.ReceiveReplyTag, m.receiveReplyCB)
+	m.wtm.RegisterCallback(wDm.ReceiveReactionTag, m.receiveReactionCB)
+	m.wtm.RegisterCallback(wDm.UpdateSentStatusTag, m.updateSentStatusCB)
+
+	m.wtm.RegisterCallback(wDm.BlockSenderTag, m.blockSenderCB)
+	m.wtm.RegisterCallback(wDm.UnblockSenderTag, m.unblockSenderCB)
+	m.wtm.RegisterCallback(wDm.GetConversationTag, m.getConversationCB)
+	m.wtm.RegisterCallback(wDm.GetConversationsTag, m.getConversationsCB)
 }
 
 // newWASMEventModelCB is the callback for NewWASMEventModel. Returns an empty
@@ -98,7 +98,7 @@ func (m *manager) messageReceivedCallback(uuid uint64, pubKey ed25519.PublicKey,
 	}
 
 	// Send it to the main thread
-	m.mh.SendMessage(wDm.MessageReceivedCallbackTag, data)
+	m.wtm.SendMessage(wDm.MessageReceivedCallbackTag, data)
 }
 
 // receiveCB is the callback for wasmModel.Receive. Returns a UUID of 0 on error
diff --git a/indexedDb/impl/dm/dmIndexedDbWorker.js b/indexedDb/impl/dm/dmIndexedDbWorker.js
index e199a7bb812b9ff119b7f130f41d3bb555247302..8a5fdbf8ad9a02967b408985a0219647003eaf7e 100644
--- a/indexedDb/impl/dm/dmIndexedDbWorker.js
+++ b/indexedDb/impl/dm/dmIndexedDbWorker.js
@@ -7,11 +7,15 @@
 
 importScripts('wasm_exec.js');
 
+const isReady = new Promise((resolve) => {
+    self.onWasmInitialized = resolve;
+});
+
 const go = new Go();
 const binPath = 'xxdk-dmIndexedDkWorker.wasm'
-WebAssembly.instantiateStreaming(fetch(binPath), go.importObject).then((result) => {
+WebAssembly.instantiateStreaming(fetch(binPath), go.importObject).then(async (result) => {
     go.run(result.instance);
-    LogLevel(1);
+    await isReady;
 }).catch((err) => {
     console.error(err);
 });
\ No newline at end of file
diff --git a/indexedDb/impl/dm/main.go b/indexedDb/impl/dm/main.go
index 20b20a0856c78ac753798c9fd4a692e4a88e4852..96fae8e6fbdbce3767739891d4d7ea466e08149c 100644
--- a/indexedDb/impl/dm/main.go
+++ b/indexedDb/impl/dm/main.go
@@ -11,32 +11,71 @@ package main
 
 import (
 	"fmt"
+	"os"
+	"syscall/js"
+
+	"github.com/spf13/cobra"
 	jww "github.com/spf13/jwalterweatherman"
+
 	"gitlab.com/elixxir/xxdk-wasm/logging"
-	"gitlab.com/elixxir/xxdk-wasm/wasm"
 	"gitlab.com/elixxir/xxdk-wasm/worker"
-	"syscall/js"
 )
 
 // SEMVER is the current semantic version of the xxDK DM web worker.
 const SEMVER = "0.1.0"
 
-func init() {
-	// Set up Javascript console listener set at level INFO
-	ll := logging.NewJsConsoleLogListener(jww.LevelInfo)
-	logging.AddLogListener(ll.Listen)
-	jww.SetStdoutThreshold(jww.LevelFatal + 1)
-	jww.INFO.Printf("xxDK DM web worker version: v%s", SEMVER)
+func main() {
+	// Set to os.Args because the default is os.Args[1:] and in WASM, args start
+	// at 0, not 1.
+	dmCmd.SetArgs(os.Args)
+
+	err := dmCmd.Execute()
+	if err != nil {
+		fmt.Println(err)
+		os.Exit(1)
+	}
 }
 
-func main() {
-	jww.INFO.Print("[WW] Starting xxDK WebAssembly DM Database Worker.")
+var dmCmd = &cobra.Command{
+	Use:     "dmIndexedDbWorker",
+	Short:   "IndexedDb database for DMs.",
+	Example: "const go = new Go();\ngo.argv = [\"--logLevel=1\"]",
+	Run: func(cmd *cobra.Command, args []string) {
+		// Start logger first to capture all logging events
+		err := logging.EnableLogging(logLevel, -1, 0, "", "")
+		if err != nil {
+			fmt.Printf(
+				"Failed to intialize logging in DM indexedDb worker: %+v", err)
+			os.Exit(1)
+		}
 
-	js.Global().Set("LogLevel", js.FuncOf(wasm.LogLevel))
+		jww.INFO.Printf("xxDK DM web worker version: v%s", SEMVER)
 
-	m := &manager{mh: worker.NewThreadManager("DmIndexedDbWorker", true)}
-	m.registerCallbacks()
-	m.mh.SignalReady()
-	<-make(chan bool)
-	fmt.Println("[WW] Closing xxDK WebAssembly Channels Database Worker.")
+		jww.INFO.Print("[WW] Starting xxDK WebAssembly DM Database Worker.")
+		m := &manager{
+			wtm: worker.NewThreadManager("DmIndexedDbWorker", true),
+		}
+		m.registerCallbacks()
+		m.wtm.SignalReady()
+
+		// Indicate to the Javascript caller that the WASM is ready by resolving
+		// a promise created by the caller.
+		js.Global().Get("onWasmInitialized").Invoke()
+
+		<-make(chan bool)
+		fmt.Println("[WW] Closing xxDK WebAssembly Channels Database Worker.")
+		os.Exit(0)
+	},
+}
+
+var (
+	logLevel jww.Threshold
+)
+
+func init() {
+	// Initialize all startup flags
+	dmCmd.Flags().IntVarP((*int)(&logLevel), "logLevel", "l", 2,
+		"Sets the log level output when outputting to the Javascript console. "+
+			"0 = TRACE, 1 = DEBUG, 2 = INFO, 3 = WARN, 4 = ERROR, "+
+			"5 = CRITICAL, 6 = FATAL, -1 = disabled.")
 }
diff --git a/logging/fileLogger.go b/logging/fileLogger.go
new file mode 100644
index 0000000000000000000000000000000000000000..831511a81e069b1899b081d40268cf38042cf0b7
--- /dev/null
+++ b/logging/fileLogger.go
@@ -0,0 +1,95 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+//go:build js && wasm
+
+package logging
+
+import (
+	"io"
+	"math"
+
+	"github.com/armon/circbuf"
+	"github.com/pkg/errors"
+	jww "github.com/spf13/jwalterweatherman"
+
+	"gitlab.com/elixxir/xxdk-wasm/worker"
+)
+
+// fileLogger manages the recording of jwalterweatherman logs to the local
+// in-memory file buffer.
+type fileLogger struct {
+	threshold jww.Threshold
+	cb        *circbuf.Buffer
+}
+
+// newFileLogger starts logging to a local, in-memory log file at the specified
+// threshold. Returns a [fileLogger] that can be used to get the log file.
+func newFileLogger(threshold jww.Threshold, maxLogFileSize int) (*fileLogger, error) {
+	b, err := circbuf.NewBuffer(int64(maxLogFileSize))
+	if err != nil {
+		return nil, errors.Wrap(err, "could not create new circular buffer")
+	}
+
+	fl := &fileLogger{
+		threshold: threshold,
+		cb:        b,
+	}
+
+	jww.FEEDBACK.Printf("[LOG] Outputting log to file of max size %d at level %s",
+		b.Size(), fl.threshold)
+
+	logger = fl
+	return fl, nil
+}
+
+// Write adheres to the io.Writer interface and writes log entries to the
+// buffer.
+func (fl *fileLogger) Write(p []byte) (n int, err error) {
+	return fl.cb.Write(p)
+}
+
+// Listen adheres to the [jwalterweatherman.LogListener] type and returns the
+// log writer when the threshold is within the set threshold limit.
+func (fl *fileLogger) Listen(threshold jww.Threshold) io.Writer {
+	if threshold < fl.threshold {
+		return nil
+	}
+	return fl
+}
+
+// StopLogging stops log message writes. Once logging is stopped, it cannot be
+// resumed and the log file cannot be recovered.
+func (fl *fileLogger) StopLogging() {
+	fl.threshold = math.MaxInt
+	fl.cb.Reset()
+}
+
+// GetFile returns the entire log file.
+func (fl *fileLogger) GetFile() []byte {
+	return fl.cb.Bytes()
+}
+
+// Threshold returns the log level threshold used in the file.
+func (fl *fileLogger) Threshold() jww.Threshold {
+	return fl.threshold
+}
+
+// MaxSize returns the max size, in bytes, that the log file is allowed to be.
+func (fl *fileLogger) MaxSize() int {
+	return int(fl.cb.Size())
+}
+
+// Size returns the current size, in bytes, written to the log file.
+func (fl *fileLogger) Size() int {
+	return int(fl.cb.TotalWritten())
+}
+
+// Worker returns nil.
+func (fl *fileLogger) Worker() *worker.Manager {
+	return nil
+}
diff --git a/logging/fileLogger_test.go b/logging/fileLogger_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..317f69544a927280827aa4f3739c7f1e39d30ac4
--- /dev/null
+++ b/logging/fileLogger_test.go
@@ -0,0 +1,228 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+//go:build js && wasm
+
+package logging
+
+import (
+	"bytes"
+	"github.com/armon/circbuf"
+	jww "github.com/spf13/jwalterweatherman"
+	"math/rand"
+	"reflect"
+	"testing"
+)
+
+func Test_newFileLogger(t *testing.T) {
+	expected := &fileLogger{
+		threshold: jww.LevelError,
+	}
+	expected.cb, _ = circbuf.NewBuffer(512)
+	fl, err := newFileLogger(expected.threshold, int(expected.cb.Size()))
+	if err != nil {
+		t.Fatalf("Failed to make new fileLogger: %+v", err)
+	}
+
+	if !reflect.DeepEqual(expected, fl) {
+		t.Errorf("Unexpected new fileLogger.\nexpected: %+v\nreceived: %+v",
+			expected, fl)
+	}
+	if !reflect.DeepEqual(logger, fl) {
+		t.Errorf("Failed to set logger.\nexpected: %+v\nreceived: %+v",
+			logger, fl)
+	}
+
+}
+
+// Tests that fileLogger.Write writes the expected data to the buffer and that
+// when the max file size is reached, old data is replaced.
+func Test_fileLogger_Write(t *testing.T) {
+	rng := rand.New(rand.NewSource(3424))
+	fl, err := newFileLogger(jww.LevelError, 512)
+	if err != nil {
+		t.Fatalf("Failed to make new fileLogger: %+v", err)
+	}
+
+	expected := make([]byte, fl.MaxSize())
+	rng.Read(expected)
+	n, err := fl.Write(expected)
+	if err != nil {
+		t.Fatalf("Failed to write: %+v", err)
+	} else if n != len(expected) {
+		t.Fatalf("Did not write expected length.\nexpected: %d\nreceived: %d",
+			len(expected), n)
+	}
+
+	if !bytes.Equal(fl.cb.Bytes(), expected) {
+		t.Fatalf("Incorrect bytes in buffer.\nexpected: %v\nreceived: %v",
+			expected, fl.cb.Bytes())
+	}
+
+	// Check that the data is overwritten
+	rng.Read(expected)
+	n, err = fl.Write(expected)
+	if err != nil {
+		t.Fatalf("Failed to write: %+v", err)
+	} else if n != len(expected) {
+		t.Fatalf("Did not write expected length.\nexpected: %d\nreceived: %d",
+			len(expected), n)
+	}
+
+	if !bytes.Equal(fl.cb.Bytes(), expected) {
+		t.Fatalf("Incorrect bytes in buffer.\nexpected: %v\nreceived: %v",
+			expected, fl.cb.Bytes())
+	}
+}
+
+// Tests that fileLogger.Listen only returns an io.Writer for valid thresholds.
+func Test_fileLogger_Listen(t *testing.T) {
+	th := jww.LevelError
+	fl, err := newFileLogger(th, 512)
+	if err != nil {
+		t.Fatalf("Failed to make new fileLogger: %+v", err)
+	}
+
+	thresholds := []jww.Threshold{-1, jww.LevelTrace, jww.LevelDebug,
+		jww.LevelFatal, jww.LevelWarn, jww.LevelError, jww.LevelCritical,
+		jww.LevelFatal}
+
+	for _, threshold := range thresholds {
+		w := fl.Listen(threshold)
+		if threshold < th {
+			if w != nil {
+				t.Errorf("Did not receive nil io.Writer for level %s: %+v",
+					threshold, w)
+			}
+		} else if w == nil {
+			t.Errorf("Received nil io.Writer for level %s", threshold)
+		}
+	}
+}
+
+// Tests that fileLogger.Listen always returns nil after fileLogger.StopLogging
+// is called.
+func Test_fileLogger_StopLogging(t *testing.T) {
+	fl, err := newFileLogger(jww.LevelError, 512)
+	if err != nil {
+		t.Fatalf("Failed to make new fileLogger: %+v", err)
+	}
+
+	fl.StopLogging()
+
+	if w := fl.Listen(jww.LevelFatal); w != nil {
+		t.Errorf("Listen returned non-nil io.Writer when logging should have "+
+			"been stopped: %+v", w)
+	}
+
+	file := fl.GetFile()
+	if !bytes.Equal([]byte{}, file) {
+		t.Errorf("Did not receice empty file: %+v", file)
+	}
+}
+
+// Tests that fileLogger.GetFile returns the expected file.
+func Test_fileLogger_GetFile(t *testing.T) {
+	rng := rand.New(rand.NewSource(9863))
+	fl, err := newFileLogger(jww.LevelError, 512)
+	if err != nil {
+		t.Fatalf("Failed to make new fileLogger: %+v", err)
+	}
+
+	var expected []byte
+	for i := 0; i < 5; i++ {
+		p := make([]byte, rng.Intn(64))
+		rng.Read(p)
+		expected = append(expected, p...)
+
+		if _, err = fl.Write(p); err != nil {
+			t.Errorf("Write %d failed: %+v", i, err)
+		}
+	}
+
+	file := fl.GetFile()
+	if !bytes.Equal(expected, file) {
+		t.Errorf("Unexpected file.\nexpected: %v\nreceived: %v", expected, file)
+	}
+}
+
+// Tests that fileLogger.Threshold returns the expected threshold.
+func Test_fileLogger_Threshold(t *testing.T) {
+	thresholds := []jww.Threshold{-1, jww.LevelTrace, jww.LevelDebug,
+		jww.LevelFatal, jww.LevelWarn, jww.LevelError, jww.LevelCritical,
+		jww.LevelFatal}
+
+	for _, threshold := range thresholds {
+		fl, err := newFileLogger(threshold, 512)
+		if err != nil {
+			t.Fatalf("Failed to make new fileLogger: %+v", err)
+		}
+
+		if fl.Threshold() != threshold {
+			t.Errorf("Incorrect threshold.\nexpected: %s (%d)\nreceived: %s (%d)",
+				threshold, threshold, fl.Threshold(), fl.Threshold())
+		}
+	}
+}
+
+// Unit test of fileLogger.MaxSize.
+func Test_fileLogger_MaxSize(t *testing.T) {
+	maxSize := 512
+	fl, err := newFileLogger(jww.LevelError, maxSize)
+	if err != nil {
+		t.Fatalf("Failed to make new fileLogger: %+v", err)
+	}
+
+	if fl.MaxSize() != maxSize {
+		t.Errorf("Incorrect max size.\nexpected: %d\nreceived: %d",
+			maxSize, fl.MaxSize())
+	}
+}
+
+// Unit test of fileLogger.Size.
+func Test_fileLogger_Size(t *testing.T) {
+	rng := rand.New(rand.NewSource(9863))
+	fl, err := newFileLogger(jww.LevelError, 512)
+	if err != nil {
+		t.Fatalf("Failed to make new fileLogger: %+v", err)
+	}
+
+	var expected []byte
+	for i := 0; i < 5; i++ {
+		p := make([]byte, rng.Intn(64))
+		rng.Read(p)
+		expected = append(expected, p...)
+
+		if _, err = fl.Write(p); err != nil {
+			t.Errorf("Write %d failed: %+v", i, err)
+		}
+
+		size := fl.Size()
+		if size != len(expected) {
+			t.Errorf("Incorrect size (%d).\nexpected: %d\nreceived: %d",
+				i, len(expected), size)
+		}
+	}
+
+	file := fl.GetFile()
+	if !bytes.Equal(expected, file) {
+		t.Errorf("Unexpected file.\nexpected: %v\nreceived: %v", expected, file)
+	}
+}
+
+// Tests that fileLogger.Worker always returns nil.
+func Test_fileLogger_Worker(t *testing.T) {
+	fl, err := newFileLogger(jww.LevelError, 512)
+	if err != nil {
+		t.Fatalf("Failed to make new fileLogger: %+v", err)
+	}
+
+	w := fl.Worker()
+	if w != nil {
+		t.Errorf("Did not get nil worker: %+v", w)
+	}
+}
diff --git a/logging/logLevel.go b/logging/logLevel.go
deleted file mode 100644
index 895857475c4c647d7625b43644e6c4f32be98155..0000000000000000000000000000000000000000
--- a/logging/logLevel.go
+++ /dev/null
@@ -1,89 +0,0 @@
-////////////////////////////////////////////////////////////////////////////////
-// Copyright © 2022 xx foundation                                             //
-//                                                                            //
-// Use of this source code is governed by a license that can be found in the  //
-// LICENSE file.                                                              //
-////////////////////////////////////////////////////////////////////////////////
-
-//go:build js && wasm
-
-package logging
-
-import (
-	"fmt"
-	"github.com/pkg/errors"
-	jww "github.com/spf13/jwalterweatherman"
-	"gitlab.com/elixxir/xxdk-wasm/utils"
-	"log"
-	"syscall/js"
-)
-
-// LogLevel sets level of logging. All logs at the set level and below will be
-// displayed (e.g., when log level is ERROR, only ERROR, CRITICAL, and FATAL
-// messages will be printed).
-//
-// The default log level without updates is INFO.
-func LogLevel(threshold jww.Threshold) error {
-	if threshold < jww.LevelTrace || threshold > jww.LevelFatal {
-		return errors.Errorf("log level is not valid: log level: %d", threshold)
-	}
-
-	jww.SetLogThreshold(threshold)
-	jww.SetFlags(log.LstdFlags | log.Lmicroseconds)
-
-	ll := NewJsConsoleLogListener(threshold)
-	AddLogListener(ll.Listen)
-	jww.SetStdoutThreshold(jww.LevelFatal + 1)
-
-	msg := fmt.Sprintf("Log level set to: %s", threshold)
-	switch threshold {
-	case jww.LevelTrace:
-		fallthrough
-	case jww.LevelDebug:
-		fallthrough
-	case jww.LevelInfo:
-		jww.INFO.Print(msg)
-	case jww.LevelWarn:
-		jww.WARN.Print(msg)
-	case jww.LevelError:
-		jww.ERROR.Print(msg)
-	case jww.LevelCritical:
-		jww.CRITICAL.Print(msg)
-	case jww.LevelFatal:
-		jww.FATAL.Print(msg)
-	}
-
-	return nil
-}
-
-// LogLevelJS sets level of logging. All logs at the set level and below will be
-// displayed (e.g., when log level is ERROR, only ERROR, CRITICAL, and FATAL
-// messages will be printed).
-//
-// Log level options:
-//
-//	TRACE    - 0
-//	DEBUG    - 1
-//	INFO     - 2
-//	WARN     - 3
-//	ERROR    - 4
-//	CRITICAL - 5
-//	FATAL    - 6
-//
-// The default log level without updates is INFO.
-//
-// Parameters:
-//   - args[0] - Log level (int).
-//
-// Returns:
-//   - Throws TypeError if the log level is invalid.
-func LogLevelJS(_ js.Value, args []js.Value) any {
-	threshold := jww.Threshold(args[0].Int())
-	err := LogLevel(threshold)
-	if err != nil {
-		utils.Throw(utils.TypeError, err)
-		return nil
-	}
-
-	return nil
-}
diff --git a/logging/logger.go b/logging/logger.go
index 03bd1cf4abebde1e4645bc87d31089ed9bf1c5d3..3064a00d551a464a486c8f8a23925cf74cea7998 100644
--- a/logging/logger.go
+++ b/logging/logger.go
@@ -10,29 +10,13 @@
 package logging
 
 import (
-	"encoding/binary"
-	"encoding/json"
-	"fmt"
-	"github.com/armon/circbuf"
 	"github.com/pkg/errors"
-	jww "github.com/spf13/jwalterweatherman"
-	"gitlab.com/elixxir/xxdk-wasm/utils"
-	"gitlab.com/elixxir/xxdk-wasm/worker"
-	"io"
-	"strconv"
-	"sync/atomic"
 	"syscall/js"
-	"time"
-)
 
-const (
-	// DefaultInitThreshold is the log threshold used for the initial log before
-	// any logging options is set.
-	DefaultInitThreshold = jww.LevelTrace
+	jww "github.com/spf13/jwalterweatherman"
 
-	// logListenerChanSize is the size of the listener channel that stores log
-	// messages before they are written.
-	logListenerChanSize = 3000
+	"gitlab.com/elixxir/xxdk-wasm/utils"
+	"gitlab.com/elixxir/xxdk-wasm/worker"
 )
 
 // List of tags that can be used when sending a message or registering a handler
@@ -46,346 +30,79 @@ const (
 )
 
 // logger is the global that all jwalterweatherman logging is sent to.
-var logger *Logger
-
-// Logger manages the recording of jwalterweatherman logs. It can write logs to
-// a local, in-memory buffer or to an external worker.
-type Logger struct {
-	threshold      jww.Threshold
-	maxLogFileSize int
-	logListenerID  uint64
-
-	listenChan  chan []byte
-	mode        atomic.Uint32
-	processQuit chan struct{}
-
-	cb *circbuf.Buffer
-	wm *worker.Manager
-}
-
-// InitLogger initializes the logger. Include this in the init function in main.
-func InitLogger() *Logger {
-	logger = NewLogger()
-	return logger
-}
+var logger Logger
 
 // GetLogger returns the Logger object, used to manager where logging is
 // recorded.
-func GetLogger() *Logger {
+func GetLogger() Logger {
 	return logger
 }
 
-// NewLogger creates a new Logger that begins storing the first
-// DefaultInitThreshold log entries. If either the log file or log worker is
-// enabled, then these logs are redirected to the set destination. If the
-// channel fills up with no log recorder enabled, then the listener is disabled.
-func NewLogger() *Logger {
-	lf := newLogger()
-
-	// Add the log listener
-	lf.logListenerID = AddLogListener(lf.Listen)
+type Logger interface {
+	// StopLogging stops log message writes. Once logging is stopped, it cannot
+	// be resumed and the log file cannot be recovered.
+	StopLogging()
 
-	jww.INFO.Printf("[LOG] Enabled initial log file listener in %s with ID %d "+
-		"at threshold %s that can store %d entries",
-		lf.getMode(), lf.logListenerID, lf.Threshold(), cap(lf.listenChan))
-
-	return lf
-}
+	// GetFile returns the entire log file.
+	GetFile() []byte
 
-// newLogger initialises a Logger without adding it as a log listener.
-func newLogger() *Logger {
-	lf := &Logger{
-		threshold:   DefaultInitThreshold,
-		listenChan:  make(chan []byte, logListenerChanSize),
-		mode:        atomic.Uint32{},
-		processQuit: make(chan struct{}),
-	}
-	lf.setMode(initMode)
-
-	return lf
-}
+	// Threshold returns the log level threshold used in the file.
+	Threshold() jww.Threshold
 
-// LogToFile starts logging to a local, in-memory log file.
-func (l *Logger) LogToFile(threshold jww.Threshold, maxLogFileSize int) error {
-	err := l.prepare(threshold, maxLogFileSize, fileMode)
-	if err != nil {
-		return err
-	}
+	// MaxSize returns the maximum size, in bytes, of the log file before it
+	// rolls over and starts overwriting the oldest entries
+	MaxSize() int
 
-	b, err := circbuf.NewBuffer(int64(maxLogFileSize))
-	if err != nil {
-		return err
-	}
-	l.cb = b
-
-	sendLog := func(p []byte) {
-		if n, err2 := l.cb.Write(p); err2 != nil {
-			jww.ERROR.Printf(
-				"[LOG] Error writing log to circular buffer: %+v", err2)
-		} else if n != len(p) {
-			jww.ERROR.Printf(
-				"[LOG] Wrote %d bytes when %d bytes expected", n, len(p))
-		}
-	}
-	go l.processLog(workerMode, sendLog, l.processQuit)
+	// Size returns the number of bytes written to the log file.
+	Size() int
 
-	return nil
+	// Worker returns the manager for the Javascript Worker object. If the
+	// worker has not been initialized, it returns nil.
+	Worker() *worker.Manager
 }
 
-// LogToFileWorker starts a new worker that begins listening for logs and
-// writing them to file. This function blocks until the worker has started.
-func (l *Logger) LogToFileWorker(threshold jww.Threshold, maxLogFileSize int,
-	wasmJsPath, workerName string) error {
-	err := l.prepare(threshold, maxLogFileSize, workerMode)
-	if err != nil {
-		return err
-	}
+// EnableLogging enables logging to the Javascript console and to a local or
+// worker file buffer. This must be called only once at initialisation.
+func EnableLogging(logLevel, fileLogLevel jww.Threshold, maxLogFileSizeMB int,
+	workerScriptURL, workerName string) error {
 
-	// Create new worker manager, which will start the worker and wait until
-	// communication has been established
-	wm, err := worker.NewManager(wasmJsPath, workerName, false)
-	if err != nil {
-		return err
-	}
-	l.wm = wm
-
-	// Register the callback used by the Javascript to request the log file.
-	// This prevents an error print when GetFileExtTag is not registered.
-	l.wm.RegisterCallback(GetFileExtTag, func([]byte) {
-		jww.DEBUG.Print("[LOG] Received file requested from external " +
-			"Javascript. Ignoring file.")
-	})
-
-	data, err := json.Marshal(l.maxLogFileSize)
-	if err != nil {
-		return err
+	var listeners []jww.LogListener
+	if logLevel > -1 {
+		// Overwrites setting the log level to INFO done in bindings so that the
+		// Javascript console can be used
+		ll := NewJsConsoleLogListener(logLevel)
+		listeners = append(listeners, ll.Listen)
+		jww.SetStdoutThreshold(jww.LevelFatal + 1)
+		jww.FEEDBACK.Printf("[LOG] Log level for console set to %s", logLevel)
+	} else {
+		jww.FEEDBACK.Print("[LOG] Disabling logging to console.")
 	}
 
-	// Send message to initialize the log file listener
-	errChan := make(chan error)
-	l.wm.SendMessage(NewLogFileTag, data, func(data []byte) {
-		if len(data) > 0 {
-			errChan <- errors.New(string(data))
+	if fileLogLevel > -1 {
+		maxLogFileSize := maxLogFileSizeMB * 1_000_000
+		if workerScriptURL == "" {
+			fl, err := newFileLogger(fileLogLevel, maxLogFileSize)
+			if err != nil {
+				return errors.Wrap(err, "could not initialize logging to file")
+			}
+			listeners = append(listeners, fl.Listen)
 		} else {
-			errChan <- nil
-		}
-	})
-
-	// Wait for worker to respond
-	select {
-	case err = <-errChan:
-		if err != nil {
-			return err
-		}
-	case <-time.After(worker.ResponseTimeout):
-		return errors.Errorf("timed out after %s waiting for new log "+
-			"file in worker to initialize", worker.ResponseTimeout)
-	}
-
-	jww.INFO.Printf("[LOG] Initialized log to file web worker %s.", workerName)
-
-	sendLog := func(p []byte) { l.wm.SendMessage(WriteLogTag, p, nil) }
-	go l.processLog(workerMode, sendLog, l.processQuit)
-
-	return nil
-}
+			wl, err := newWorkerLogger(
+				fileLogLevel, maxLogFileSize, workerScriptURL, workerName)
+			if err != nil {
+				return errors.Wrap(err, "could not initialize logging to worker file")
+			}
 
-// processLog processes the log messages sent to the listener channel and sends
-// them to the appropriate recorder.
-func (l *Logger) processLog(m mode, sendLog func(p []byte), quit chan struct{}) {
-	jww.INFO.Printf("[LOG] Starting log file processing thread in %s.", m)
-
-	for {
-		select {
-		case <-quit:
-			jww.INFO.Printf("[LOG] Stopping log file processing thread.")
-			return
-		case p := <-l.listenChan:
-			go sendLog(p)
+			listeners = append(listeners, wl.Listen)
 		}
-	}
-}
 
-// prepare sets the threshold, maxLogFileSize, and mode of the logger and
-// prints a log message indicating this information.
-func (l *Logger) prepare(
-	threshold jww.Threshold, maxLogFileSize int, m mode) error {
-	if m := l.getMode(); m != initMode {
-		return errors.Errorf("log already set to %s", m)
-	} else if threshold < jww.LevelTrace || threshold > jww.LevelFatal {
-		return errors.Errorf("log level of %d is invalid", threshold)
-	}
-
-	l.threshold = threshold
-	l.maxLogFileSize = maxLogFileSize
-	l.setMode(m)
-
-	msg := fmt.Sprintf("[LOG] Outputting log to file in %s of max size %d "+
-		"with level %s", m, l.MaxSize(), l.Threshold())
-	switch l.Threshold() {
-	case jww.LevelTrace:
-		fallthrough
-	case jww.LevelDebug:
-		fallthrough
-	case jww.LevelInfo:
-		jww.INFO.Print(msg)
-	case jww.LevelWarn:
-		jww.WARN.Print(msg)
-	case jww.LevelError:
-		jww.ERROR.Print(msg)
-	case jww.LevelCritical:
-		jww.CRITICAL.Print(msg)
-	case jww.LevelFatal:
-		jww.FATAL.Print(msg)
+		js.Global().Set("GetLogger", js.FuncOf(GetLoggerJS))
 	}
+	jww.SetLogListeners(listeners...)
 
 	return nil
 }
 
-// StopLogging stops the logging of log messages and disables the log listener.
-// If the log worker is running, it is terminated. Once logging is stopped, it
-// cannot be resumed the log file cannot be recovered.
-func (l *Logger) StopLogging() {
-	jww.DEBUG.Printf("[LOG] Removing log listener with ID %d", l.logListenerID)
-	RemoveLogListener(l.logListenerID)
-
-	switch l.getMode() {
-	case workerMode:
-		l.wm.Stop()
-		jww.DEBUG.Printf("[LOG] Terminated log worker.")
-	case fileMode:
-		jww.DEBUG.Printf("[LOG] Reset circular buffer.")
-		l.cb.Reset()
-	}
-
-	select {
-	case l.processQuit <- struct{}{}:
-		jww.DEBUG.Printf("[LOG] Sent quit channel to log process.")
-	default:
-		jww.DEBUG.Printf("[LOG] Failed to stop log processes.")
-	}
-}
-
-// GetFile returns the entire log file.
-//
-// If the log file is listening locally, it returns it from the local buffer. If
-// it is listening from the worker, it blocks until the file is returned.
-func (l *Logger) GetFile() []byte {
-	switch l.getMode() {
-	case fileMode:
-		return l.cb.Bytes()
-	case workerMode:
-		fileChan := make(chan []byte)
-		l.wm.SendMessage(GetFileTag, nil, func(data []byte) { fileChan <- data })
-
-		select {
-		case file := <-fileChan:
-			return file
-		case <-time.After(worker.ResponseTimeout):
-			jww.FATAL.Panicf("[LOG] Timed out after %s waiting for log "+
-				"file from worker", worker.ResponseTimeout)
-			return nil
-		}
-	default:
-		return nil
-	}
-}
-
-// Threshold returns the log level threshold used in the file.
-func (l *Logger) Threshold() jww.Threshold {
-	return l.threshold
-}
-
-// MaxSize returns the max size, in bytes, that the log file is allowed to be.
-func (l *Logger) MaxSize() int {
-	return l.maxLogFileSize
-}
-
-// Size returns the current size, in bytes, written to the log file.
-//
-// If the log file is listening locally, it returns it from the local buffer. If
-// it is listening from the worker, it blocks until the size is returned.
-func (l *Logger) Size() int {
-	switch l.getMode() {
-	case fileMode:
-		return int(l.cb.Size())
-	case workerMode:
-		sizeChan := make(chan []byte)
-		l.wm.SendMessage(SizeTag, nil, func(data []byte) { sizeChan <- data })
-
-		select {
-		case data := <-sizeChan:
-			return int(jww.Threshold(binary.LittleEndian.Uint64(data)))
-		case <-time.After(worker.ResponseTimeout):
-			jww.FATAL.Panicf("[LOG] Timed out after %s waiting for log "+
-				"file size from worker", worker.ResponseTimeout)
-			return 0
-		}
-	default:
-		return 0
-	}
-}
-
-////////////////////////////////////////////////////////////////////////////////
-// JWW Listener                                                               //
-////////////////////////////////////////////////////////////////////////////////
-
-// Listen is called for every logging event. This function adheres to the
-// [jwalterweatherman.LogListener] type.
-func (l *Logger) Listen(t jww.Threshold) io.Writer {
-	if t < l.threshold {
-		return nil
-	}
-
-	return l
-}
-
-// Write sends the bytes to the listener channel. It always returns the length
-// of p and a nil error. This function adheres to the io.Writer interface.
-func (l *Logger) Write(p []byte) (n int, err error) {
-	select {
-	case l.listenChan <- append([]byte{}, p...):
-	default:
-		jww.ERROR.Printf(
-			"[LOG] Logger channel filled. Log file recording stopping.")
-		l.StopLogging()
-		return 0, errors.Errorf(
-			"Logger channel filled. Log file recording stopping.")
-	}
-	return len(p), nil
-}
-
-////////////////////////////////////////////////////////////////////////////////
-// Log File Mode                                                              //
-////////////////////////////////////////////////////////////////////////////////
-
-// mode represents the state of the Logger.
-type mode uint32
-
-const (
-	initMode mode = iota
-	fileMode
-	workerMode
-)
-
-func (l *Logger) setMode(m mode) { l.mode.Store(uint32(m)) }
-func (l *Logger) getMode() mode  { return mode(l.mode.Load()) }
-
-// String returns a human-readable representation of the mode for logging and
-// debugging. This function adheres to the fmt.Stringer interface.
-func (m mode) String() string {
-	switch m {
-	case initMode:
-		return "uninitialized mode"
-	case fileMode:
-		return "file mode"
-	case workerMode:
-		return "worker mode"
-	default:
-		return "invalid mode: " + strconv.Itoa(int(m))
-	}
-}
-
 ////////////////////////////////////////////////////////////////////////////////
 // Javascript Bindings                                                        //
 ////////////////////////////////////////////////////////////////////////////////
@@ -396,142 +113,98 @@ func (m mode) String() string {
 // Returns:
 //   - A Javascript representation of the [Logger] object.
 func GetLoggerJS(js.Value, []js.Value) any {
-	return newLoggerJS(GetLogger())
+	// l := GetLogger()
+	// if l != nil {
+	// 	return newLoggerJS(LoggerJS{GetLogger()})
+	// }
+	// return js.Null()
+	return newLoggerJS(LoggerJS{GetLogger()})
+}
+
+type LoggerJS struct {
+	api Logger
 }
 
 // newLoggerJS creates a new Javascript compatible object (map[string]any) that
 // matches the [Logger] structure.
-func newLoggerJS(lfw *Logger) map[string]any {
+func newLoggerJS(l LoggerJS) map[string]any {
 	logFileWorker := map[string]any{
-		"LogToFile":       js.FuncOf(lfw.LogToFileJS),
-		"LogToFileWorker": js.FuncOf(lfw.LogToFileWorkerJS),
-		"StopLogging":     js.FuncOf(lfw.StopLoggingJS),
-		"GetFile":         js.FuncOf(lfw.GetFileJS),
-		"Threshold":       js.FuncOf(lfw.ThresholdJS),
-		"MaxSize":         js.FuncOf(lfw.MaxSizeJS),
-		"Size":            js.FuncOf(lfw.SizeJS),
-		"Worker":          js.FuncOf(lfw.WorkerJS),
+		"StopLogging": js.FuncOf(l.StopLogging),
+		"GetFile":     js.FuncOf(l.GetFile),
+		"Threshold":   js.FuncOf(l.Threshold),
+		"MaxSize":     js.FuncOf(l.MaxSize),
+		"Size":        js.FuncOf(l.Size),
+		"Worker":      js.FuncOf(l.Worker),
 	}
 
 	return logFileWorker
 }
 
-// LogToFileJS starts logging to a local, in-memory log file.
-//
-// Parameters:
-//   - args[0] - Log level (int).
-//   - args[1] - Max log file size, in bytes (int).
-//
-// Returns:
-//   - Throws a TypeError if starting the log file fails.
-func (l *Logger) LogToFileJS(_ js.Value, args []js.Value) any {
-	threshold := jww.Threshold(args[0].Int())
-	maxLogFileSize := args[1].Int()
-
-	err := l.LogToFile(threshold, maxLogFileSize)
-	if err != nil {
-		utils.Throw(utils.TypeError, err)
-		return nil
-	}
-
-	return nil
-}
-
-// LogToFileWorkerJS starts a new worker that begins listening for logs and
-// writing them to file. This function blocks until the worker has started.
-//
-// Parameters:
-//   - args[0] - Log level (int).
-//   - args[1] - Max log file size, in bytes (int).
-//   - args[2] - Path to Javascript start file for the worker WASM (string).
-//   - args[3] - Name of the worker (used in logs) (string).
-//
-// Returns a promise:
-//   - Resolves to nothing on success (void).
-//   - Rejected with an error if starting the worker fails.
-func (l *Logger) LogToFileWorkerJS(_ js.Value, args []js.Value) any {
-	threshold := jww.Threshold(args[0].Int())
-	maxLogFileSize := args[1].Int()
-	wasmJsPath := args[2].String()
-	workerName := args[3].String()
-
-	promiseFn := func(resolve, reject func(args ...any) js.Value) {
-		err := l.LogToFileWorker(
-			threshold, maxLogFileSize, wasmJsPath, workerName)
-		if err != nil {
-			reject(utils.JsTrace(err))
-		} else {
-			resolve()
-		}
-	}
-
-	return utils.CreatePromise(promiseFn)
-}
-
-// StopLoggingJS stops the logging of log messages and disables the log
+// StopLogging stops the logging of log messages and disables the log
 // listener. If the log worker is running, it is terminated. Once logging is
 // stopped, it cannot be resumed the log file cannot be recovered.
-func (l *Logger) StopLoggingJS(js.Value, []js.Value) any {
-	l.StopLogging()
+func (l *LoggerJS) StopLogging(js.Value, []js.Value) any {
+	l.api.StopLogging()
 
 	return nil
 }
 
-// GetFileJS returns the entire log file.
+// GetFile returns the entire log file.
 //
 // If the log file is listening locally, it returns it from the local buffer. If
 // it is listening from the worker, it blocks until the file is returned.
 //
 // Returns a promise:
 //   - Resolves to the log file contents (string).
-func (l *Logger) GetFileJS(js.Value, []js.Value) any {
+func (l *LoggerJS) GetFile(js.Value, []js.Value) any {
 	promiseFn := func(resolve, _ func(args ...any) js.Value) {
-		resolve(string(l.GetFile()))
+		resolve(string(l.api.GetFile()))
 	}
 
 	return utils.CreatePromise(promiseFn)
 }
 
-// ThresholdJS returns the log level threshold used in the file.
+// Threshold returns the log level threshold used in the file.
 //
 // Returns:
 //   - Log level (int).
-func (l *Logger) ThresholdJS(js.Value, []js.Value) any {
-	return int(l.Threshold())
+func (l *LoggerJS) Threshold(js.Value, []js.Value) any {
+	return int(l.api.Threshold())
 }
 
-// MaxSizeJS returns the max size, in bytes, that the log file is allowed to be.
+// MaxSize returns the max size, in bytes, that the log file is allowed to be.
 //
 // Returns:
 //   - Max file size (int).
-func (l *Logger) MaxSizeJS(js.Value, []js.Value) any {
-	return l.MaxSize()
+func (l *LoggerJS) MaxSize(js.Value, []js.Value) any {
+	return l.api.MaxSize()
 }
 
-// SizeJS returns the current size, in bytes, written to the log file.
+// Size returns the current size, in bytes, written to the log file.
 //
 // If the log file is listening locally, it returns it from the local buffer. If
 // it is listening from the worker, it blocks until the size is returned.
 //
 // Returns a promise:
 //   - Resolves to the current file size (int).
-func (l *Logger) SizeJS(js.Value, []js.Value) any {
+func (l *LoggerJS) Size(js.Value, []js.Value) any {
 	promiseFn := func(resolve, _ func(args ...any) js.Value) {
-		resolve(l.Size())
+		resolve(l.api.Size())
 	}
 
 	return utils.CreatePromise(promiseFn)
 }
 
-// WorkerJS returns the web worker object.
+// Worker returns the web worker object.
 //
 // Returns:
 //   - Javascript worker object. If the worker has not been initialized, it
 //     returns null.
-func (l *Logger) WorkerJS(js.Value, []js.Value) any {
-	if l.getMode() == workerMode {
-		return l.wm.GetWorker()
+func (l *LoggerJS) Worker(js.Value, []js.Value) any {
+	wm := l.api.Worker()
+	if wm == nil {
+		return js.Null()
 	}
 
-	return js.Null()
+	return wm.GetWorker()
 }
diff --git a/logging/logger_test.go b/logging/logger_test.go
deleted file mode 100644
index 0b5267be9cabaf995ddc8fcdba4b5d3ec8eea133..0000000000000000000000000000000000000000
--- a/logging/logger_test.go
+++ /dev/null
@@ -1,172 +0,0 @@
-////////////////////////////////////////////////////////////////////////////////
-// Copyright © 2022 xx foundation                                             //
-//                                                                            //
-// Use of this source code is governed by a license that can be found in the  //
-// LICENSE file.                                                              //
-////////////////////////////////////////////////////////////////////////////////
-
-//go:build js && wasm
-
-package logging
-
-import (
-	"bytes"
-	"fmt"
-	jww "github.com/spf13/jwalterweatherman"
-	"testing"
-)
-
-// Tests InitLogger
-func TestInitLogger(t *testing.T) {
-}
-
-// Tests GetLogger
-func TestGetLogger(t *testing.T) {
-}
-
-// Tests NewLogger
-func TestNewLogger(t *testing.T) {
-}
-
-// Tests Logger.LogToFile
-func TestLogger_LogToFile(t *testing.T) {
-	jww.SetStdoutThreshold(jww.LevelTrace)
-	l := NewLogger()
-
-	err := l.LogToFile(jww.LevelTrace, 50000000)
-	if err != nil {
-		t.Fatalf("Failed to LogToFile: %+v", err)
-	}
-
-	jww.INFO.Printf("test")
-
-	file := l.cb.Bytes()
-	fmt.Printf("file:----------------------------\n%s\n---------------------------------\n", file)
-}
-
-// Tests Logger.LogToFileWorker
-func TestLogger_LogToFileWorker(t *testing.T) {
-}
-
-// Tests Logger.processLog
-func TestLogger_processLog(t *testing.T) {
-}
-
-// Tests Logger.prepare
-func TestLogger_prepare(t *testing.T) {
-}
-
-// Tests Logger.StopLogging
-func TestLogger_StopLogging(t *testing.T) {
-}
-
-// Tests Logger.GetFile
-func TestLogger_GetFile(t *testing.T) {
-}
-
-// Tests Logger.Threshold
-func TestLogger_Threshold(t *testing.T) {
-}
-
-// Tests Logger.MaxSize
-func TestLogger_MaxSize(t *testing.T) {
-}
-
-// Tests Logger.Size
-func TestLogger_Size(t *testing.T) {
-}
-
-// Tests Logger.Listen
-func TestLogger_Listen(t *testing.T) {
-
-	// l := newLogger()
-
-}
-
-// Tests that Logger.Write can fill the listenChan channel completely and that
-// all messages are received in the order they were added.
-func TestLogger_Write(t *testing.T) {
-	l := newLogger()
-	expectedLogs := make([][]byte, logListenerChanSize)
-
-	for i := range expectedLogs {
-		p := []byte(
-			fmt.Sprintf("Log message %d of %d.", i+1, logListenerChanSize))
-		expectedLogs[i] = p
-		n, err := l.Listen(jww.LevelError).Write(p)
-		if err != nil {
-			t.Errorf("Received impossible error (%d): %+v", i, err)
-		} else if n != len(p) {
-			t.Errorf("Received incorrect bytes written (%d)."+
-				"\nexpected: %d\nreceived: %d", i, len(p), n)
-		}
-	}
-
-	for i, expected := range expectedLogs {
-		select {
-		case received := <-l.listenChan:
-			if !bytes.Equal(expected, received) {
-				t.Errorf("Received unexpected meessage (%d)."+
-					"\nexpected: %q\nreceived: %q", i, expected, received)
-			}
-		default:
-			t.Errorf("Failed to read from channel.")
-		}
-	}
-}
-
-// Error path: Tests that Logger.Write returns an error when the listener
-// channel is full.
-func TestLogger_Write_ChannelFilledError(t *testing.T) {
-	l := newLogger()
-	expectedLogs := make([][]byte, logListenerChanSize)
-
-	for i := range expectedLogs {
-		p := []byte(
-			fmt.Sprintf("Log message %d of %d.", i+1, logListenerChanSize))
-		expectedLogs[i] = p
-		n, err := l.Listen(jww.LevelError).Write(p)
-		if err != nil {
-			t.Errorf("Received impossible error (%d): %+v", i, err)
-		} else if n != len(p) {
-			t.Errorf("Received incorrect bytes written (%d)."+
-				"\nexpected: %d\nreceived: %d", i, len(p), n)
-		}
-	}
-
-	_, err := l.Write([]byte("test"))
-	if err == nil {
-		t.Error("Failed to receive error when the chanel should be full.")
-	}
-}
-
-// Tests that Logger.getMode gets the same value set with Logger.setMode.
-func TestLogger_setMode_getMode(t *testing.T) {
-	l := newLogger()
-
-	for i, m := range []mode{initMode, fileMode, workerMode, 12} {
-		l.setMode(m)
-		received := l.getMode()
-		if m != received {
-			t.Errorf("Received wrong mode (%d).\nexpected: %s\nreceived: %s",
-				i, m, received)
-		}
-	}
-
-}
-
-// Unit test of mode.String.
-func Test_mode_String(t *testing.T) {
-	for m, expected := range map[mode]string{
-		initMode:   "uninitialized mode",
-		fileMode:   "file mode",
-		workerMode: "worker mode",
-		12:         "invalid mode: 12",
-	} {
-		s := m.String()
-		if s != expected {
-			t.Errorf("Wrong string for mode %d.\nexpected: %s\nreceived: %s",
-				m, expected, s)
-		}
-	}
-}
diff --git a/logging/workerLogger.go b/logging/workerLogger.go
new file mode 100644
index 0000000000000000000000000000000000000000..bfac4703943c94956a8dc6501b722e7d0e5cad62
--- /dev/null
+++ b/logging/workerLogger.go
@@ -0,0 +1,162 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+//go:build js && wasm
+
+package logging
+
+import (
+	"encoding/binary"
+	"encoding/json"
+	"io"
+	"math"
+	"time"
+
+	"github.com/pkg/errors"
+	jww "github.com/spf13/jwalterweatherman"
+
+	"gitlab.com/elixxir/xxdk-wasm/worker"
+)
+
+// TODO: add ability to import worker so that multiple threads can send logs: https://stackoverflow.com/questions/8343781/how-to-do-worker-to-worker-communication
+
+// workerLogger manages the recording of jwalterweatherman logs to the in-memory
+// file buffer in a remote Worker thread.
+type workerLogger struct {
+	threshold      jww.Threshold
+	maxLogFileSize int
+	wm             *worker.Manager
+}
+
+// newWorkerLogger starts logging to an in-memory log file in a remote Worker
+// at the specified threshold. Returns a [workerLogger] that can be used to get
+// the log file.
+func newWorkerLogger(threshold jww.Threshold, maxLogFileSize int,
+	wasmJsPath, workerName string) (*workerLogger, error) {
+	// Create new worker manager, which will start the worker and wait until
+	// communication has been established
+	wm, err := worker.NewManager(wasmJsPath, workerName, false)
+	if err != nil {
+		return nil, err
+	}
+
+	wl := &workerLogger{
+		threshold:      threshold,
+		maxLogFileSize: maxLogFileSize,
+		wm:             wm,
+	}
+
+	// Register the callback used by the Javascript to request the log file.
+	// This prevents an error print when GetFileExtTag is not registered.
+	wl.wm.RegisterCallback(GetFileExtTag, func([]byte) {
+		jww.DEBUG.Print("[LOG] Received file requested from external " +
+			"Javascript. Ignoring file.")
+	})
+
+	data, err := json.Marshal(wl.maxLogFileSize)
+	if err != nil {
+		return nil, err
+	}
+
+	// Send message to initialize the log file listener
+	errChan := make(chan error)
+	wl.wm.SendMessage(NewLogFileTag, data, func(data []byte) {
+		if len(data) > 0 {
+			errChan <- errors.New(string(data))
+		} else {
+			errChan <- nil
+		}
+	})
+
+	// Wait for worker to respond
+	select {
+	case err = <-errChan:
+		if err != nil {
+			return nil, err
+		}
+	case <-time.After(worker.ResponseTimeout):
+		return nil, errors.Errorf("timed out after %s waiting for new log "+
+			"file in worker to initialize", worker.ResponseTimeout)
+	}
+
+	jww.FEEDBACK.Printf("[LOG] Outputting log to file of max size %d at level "+
+		"%s using web worker %s", wl.maxLogFileSize, wl.threshold, workerName)
+
+	logger = wl
+	return wl, nil
+}
+
+// Write adheres to the io.Writer interface and sends the log entries to the
+// worker to be added to the file buffer. Always returns the length of p and
+// nil. All errors are printed to the log.
+func (wl *workerLogger) Write(p []byte) (n int, err error) {
+	wl.wm.SendMessage(WriteLogTag, p, nil)
+	return len(p), nil
+}
+
+// Listen adheres to the [jwalterweatherman.LogListener] type and returns the
+// log writer when the threshold is within the set threshold limit.
+func (wl *workerLogger) Listen(threshold jww.Threshold) io.Writer {
+	if threshold < wl.threshold {
+		return nil
+	}
+	return wl
+}
+
+// StopLogging stops log message writes and terminates the worker. Once logging
+// is stopped, it cannot be resumed and the log file cannot be recovered.
+func (wl *workerLogger) StopLogging() {
+	wl.threshold = math.MaxInt
+
+	wl.wm.Stop()
+	jww.DEBUG.Printf("[LOG] Terminated log worker.")
+}
+
+// GetFile returns the entire log file.
+func (wl *workerLogger) GetFile() []byte {
+	fileChan := make(chan []byte)
+	wl.wm.SendMessage(GetFileTag, nil, func(data []byte) { fileChan <- data })
+
+	select {
+	case file := <-fileChan:
+		return file
+	case <-time.After(worker.ResponseTimeout):
+		jww.FATAL.Panicf("[LOG] Timed out after %s waiting for log "+
+			"file from worker", worker.ResponseTimeout)
+		return nil
+	}
+}
+
+// Threshold returns the log level threshold used in the file.
+func (wl *workerLogger) Threshold() jww.Threshold {
+	return wl.threshold
+}
+
+// MaxSize returns the max size, in bytes, that the log file is allowed to be.
+func (wl *workerLogger) MaxSize() int {
+	return wl.maxLogFileSize
+}
+
+// Size returns the number of bytes written to the log file.
+func (wl *workerLogger) Size() int {
+	sizeChan := make(chan []byte)
+	wl.wm.SendMessage(SizeTag, nil, func(data []byte) { sizeChan <- data })
+
+	select {
+	case data := <-sizeChan:
+		return int(binary.LittleEndian.Uint64(data))
+	case <-time.After(worker.ResponseTimeout):
+		jww.FATAL.Panicf("[LOG] Timed out after %s waiting for log "+
+			"file size from worker", worker.ResponseTimeout)
+		return 0
+	}
+}
+
+// Worker returns the manager for the Javascript Worker object.
+func (wl *workerLogger) Worker() *worker.Manager {
+	return wl.wm
+}
diff --git a/logging/workerThread/logFileWorker.js b/logging/workerThread/logFileWorker.js
index 159bfaa0d919a4f0cb2758af48d80c65891e7820..ed246f62563f89645f95fe13c715a81b60aa756b 100644
--- a/logging/workerThread/logFileWorker.js
+++ b/logging/workerThread/logFileWorker.js
@@ -7,11 +7,15 @@
 
 importScripts('wasm_exec.js');
 
+const isReady = new Promise((resolve) => {
+    self.onWasmInitialized = resolve;
+});
+
 const go = new Go();
 const binPath = 'xxdk-logFileWorker.wasm'
-WebAssembly.instantiateStreaming(fetch(binPath), go.importObject).then((result) => {
+WebAssembly.instantiateStreaming(fetch(binPath), go.importObject).then(async (result) => {
     go.run(result.instance);
-    LogLevel(1);
+    await isReady;
 }).catch((err) => {
     console.error(err);
 });
\ No newline at end of file
diff --git a/logging/workerThread/main.go b/logging/workerThread/main.go
index 91b059f3da1195b0ff244027608a0f8a98482fed..1a9a31a0ca39ee684b4427eef03fc89f4a407b8c 100644
--- a/logging/workerThread/main.go
+++ b/logging/workerThread/main.go
@@ -13,25 +13,21 @@ import (
 	"encoding/binary"
 	"encoding/json"
 	"fmt"
+	"os"
+	"syscall/js"
+
 	"github.com/armon/circbuf"
 	"github.com/pkg/errors"
+	"github.com/spf13/cobra"
 	jww "github.com/spf13/jwalterweatherman"
+
 	"gitlab.com/elixxir/xxdk-wasm/logging"
 	"gitlab.com/elixxir/xxdk-wasm/worker"
-	"syscall/js"
 )
 
 // SEMVER is the current semantic version of the xxDK Logger web worker.
 const SEMVER = "0.1.0"
 
-func init() {
-	// Set up Javascript console listener set at level INFO
-	ll := logging.NewJsConsoleLogListener(jww.LevelDebug)
-	logging.AddLogListener(ll.Listen)
-	jww.SetStdoutThreshold(jww.LevelFatal + 1)
-	jww.INFO.Printf("xxDK Logger web worker version: v%s", SEMVER)
-}
-
 // workerLogFile manages communication with the main thread and writing incoming
 // logging messages to the log file.
 type workerLogFile struct {
@@ -40,17 +36,60 @@ type workerLogFile struct {
 }
 
 func main() {
-	jww.INFO.Print("[LOG] Starting xxDK WebAssembly Logger Worker.")
+	// Set to os.Args because the default is os.Args[1:] and in WASM, args start
+	// at 0, not 1.
+	LoggerCmd.SetArgs(os.Args)
+
+	err := LoggerCmd.Execute()
+	if err != nil {
+		fmt.Println(err)
+		os.Exit(1)
+	}
+}
+
+var LoggerCmd = &cobra.Command{
+	Use:     "Logger",
+	Short:   "Web worker buffer file logger",
+	Example: "const go = new Go();\ngo.argv = [\"--logLevel=1\"]",
+	Run: func(cmd *cobra.Command, args []string) {
+		// Start logger first to capture all logging events
+		err := logging.EnableLogging(logLevel, -1, 0, "", "")
+		if err != nil {
+			fmt.Printf(
+				"Failed to intialize logging in logging worker: %+v", err)
+			os.Exit(1)
+		}
 
-	js.Global().Set("LogLevel", js.FuncOf(logging.LogLevelJS))
+		jww.INFO.Printf("xxDK Logger web worker version: v%s", SEMVER)
 
-	wlf := workerLogFile{wtm: worker.NewThreadManager("Logger", false)}
+		jww.INFO.Print("[LOG] Starting xxDK WebAssembly Logger Worker.")
 
-	wlf.registerCallbacks()
+		wlf := workerLogFile{wtm: worker.NewThreadManager("Logger", false)}
 
-	wlf.wtm.SignalReady()
-	<-make(chan bool)
-	fmt.Println("[WW] Closing xxDK WebAssembly Log Worker.")
+		wlf.registerCallbacks()
+
+		wlf.wtm.SignalReady()
+
+		// Indicate to the Javascript caller that the WASM is ready by resolving
+		// a promise created by the caller.
+		js.Global().Get("onWasmInitialized").Invoke()
+
+		<-make(chan bool)
+		fmt.Println("[WW] Closing xxDK WebAssembly Log Worker.")
+		os.Exit(0)
+	},
+}
+
+var (
+	logLevel jww.Threshold
+)
+
+func init() {
+	// Initialize all startup flags
+	LoggerCmd.Flags().IntVarP((*int)(&logLevel), "logLevel", "l", 2,
+		"Sets the log level output when outputting to the Javascript console. "+
+			"0 = TRACE, 1 = DEBUG, 2 = INFO, 3 = WARN, 4 = ERROR, "+
+			"5 = CRITICAL, 6 = FATAL, -1 = disabled.")
 }
 
 // registerCallbacks registers all the necessary callbacks for the main thread
diff --git a/main.go b/main.go
index c109ff54f41fc0c3acb34bd7d3c848013237360c..46cdc133897c1225c9956ea44c5cc2d3018b70aa 100644
--- a/main.go
+++ b/main.go
@@ -10,38 +10,75 @@
 package main
 
 import (
-	"gitlab.com/elixxir/xxdk-wasm/logging"
+	"fmt"
+	"github.com/spf13/cobra"
 	"os"
 	"syscall/js"
 
 	jww "github.com/spf13/jwalterweatherman"
+
+	"gitlab.com/elixxir/xxdk-wasm/logging"
 	"gitlab.com/elixxir/xxdk-wasm/storage"
 	"gitlab.com/elixxir/xxdk-wasm/utils"
 	"gitlab.com/elixxir/xxdk-wasm/wasm"
 )
 
-func init() {
-	// Start logger first to capture all logging events
-	logging.InitLogger()
-
-	// Overwrites setting the log level to INFO done in bindings so that the
-	// Javascript console can be used
-	ll := logging.NewJsConsoleLogListener(jww.LevelInfo)
-	logging.AddLogListener(ll.Listen)
-	jww.SetStdoutThreshold(jww.LevelFatal + 1)
+func main() {
+	// Set to os.Args because the default is os.Args[1:] and in WASM, args start
+	// at 0, not 1.
+	wasmCmd.SetArgs(os.Args)
 
-	// Check that the WASM binary version is correct
-	err := storage.CheckAndStoreVersions()
+	err := wasmCmd.Execute()
 	if err != nil {
-		jww.FATAL.Panicf("WASM binary version error: %+v", err)
+		fmt.Println(err)
+		os.Exit(1)
 	}
 }
 
-func main() {
-	jww.INFO.Printf("Starting xxDK WebAssembly bindings.")
+var wasmCmd = &cobra.Command{
+	Use:     "xxdk-wasm",
+	Short:   "WebAssembly bindings for xxDK.",
+	Example: "const go = new Go();\ngo.argv = [\"--logLevel=1\"]",
+	Run: func(cmd *cobra.Command, args []string) {
+		// Start logger first to capture all logging events
+		err := logging.EnableLogging(logLevel, fileLogLevel, maxLogFileSizeMB,
+			workerScriptURL, workerName)
+		if err != nil {
+			fmt.Printf("Failed to intialize logging: %+v", err)
+			os.Exit(1)
+		}
+
+		// Check that the WASM binary version is correct
+		err = storage.CheckAndStoreVersions()
+		if err != nil {
+			jww.FATAL.Panicf("WASM binary version error: %+v", err)
+		}
+
+		// Enable all top level bindings functions
+		setGlobals()
+
+		// Indicate to the Javascript caller that the WASM is ready by resolving
+		// a promise created by the caller, as shown below:
+		//
+		//  let isReady = new Promise((resolve) => {
+		//    window.onWasmInitialized = resolve;
+		//  });
+		//
+		//  const go = new Go();
+		//  go.run(result.instance);
+		//  await isReady;
+		//
+		// Source: https://github.com/golang/go/issues/49710#issuecomment-986484758
+		js.Global().Get("onWasmInitialized").Invoke()
+
+		<-make(chan bool)
+		os.Exit(0)
+	},
+}
 
-	// logging/worker.go
-	js.Global().Set("GetLogger", js.FuncOf(logging.GetLoggerJS))
+// setGlobals enables all global functions to be accessible to Javascript.
+func setGlobals() {
+	jww.INFO.Printf("Starting xxDK WebAssembly bindings.")
 
 	// storage/password.go
 	js.Global().Set("GetOrInitPassword", js.FuncOf(storage.GetOrInitPassword))
@@ -157,7 +194,6 @@ func main() {
 		js.FuncOf(wasm.GetFactsFromContact))
 
 	// wasm/logging.go
-	js.Global().Set("LogLevel", js.FuncOf(wasm.LogLevel))
 	js.Global().Set("RegisterLogWriter", js.FuncOf(wasm.RegisterLogWriter))
 	js.Global().Set("EnableGrpcLogs", js.FuncOf(wasm.EnableGrpcLogs))
 
@@ -212,7 +248,32 @@ func main() {
 	js.Global().Set("GetClientDependencies", js.FuncOf(wasm.GetClientDependencies))
 	js.Global().Set("GetWasmSemanticVersion", js.FuncOf(wasm.GetWasmSemanticVersion))
 	js.Global().Set("GetXXDKSemanticVersion", js.FuncOf(wasm.GetXXDKSemanticVersion))
+}
 
-	<-make(chan bool)
-	os.Exit(0)
+var (
+	logLevel, fileLogLevel      jww.Threshold
+	maxLogFileSizeMB              int
+	workerScriptURL, workerName string
+)
+
+func init() {
+	// Initialize all startup flags
+	wasmCmd.Flags().IntVarP((*int)(&logLevel), "logLevel", "l", 2,
+		"Sets the log level output when outputting to the Javascript console. "+
+			"0 = TRACE, 1 = DEBUG, 2 = INFO, 3 = WARN, 4 = ERROR, "+
+			"5 = CRITICAL, 6 = FATAL, -1 = disabled.")
+	wasmCmd.Flags().IntVarP((*int)(&fileLogLevel), "fileLogLevel", "m", -1,
+		"The log level when outputting to the file buffer. "+
+			"0 = TRACE, 1 = DEBUG, 2 = INFO, 3 = WARN, 4 = ERROR, "+
+			"5 = CRITICAL, 6 = FATAL, -1 = disabled.")
+	wasmCmd.Flags().IntVarP(&maxLogFileSizeMB, "maxLogFileSize", "s", 5,
+		"Max file size, in MB, for the file buffer before it rolls over "+
+			"and starts overwriting the oldest entries.")
+	wasmCmd.Flags().StringVarP(&workerScriptURL, "workerScriptURL", "w", "",
+		"URL to the script that executes the worker. If set, it enables the "+
+			"saving of log file to buffer in Worker instead of in the local "+
+			"thread. This allows logging to be available after the main WASM "+
+			"thread crashes.")
+	wasmCmd.Flags().StringVar(&workerName, "workerName", "xxdkLogFileWorker",
+		"Name of the logger worker.")
 }
diff --git a/wasm/logging.go b/wasm/logging.go
index 8199edc02a829a8e8b81b59dbfc73cddb4d46857..1ae9bb7127447f1a786833788eb34250ed783979 100644
--- a/wasm/logging.go
+++ b/wasm/logging.go
@@ -10,35 +10,10 @@
 package wasm
 
 import (
-	"gitlab.com/elixxir/client/v4/bindings"
-	"gitlab.com/elixxir/xxdk-wasm/logging"
 	"syscall/js"
-)
 
-// LogLevel sets level of logging. All logs at the set level and below will be
-// displayed (e.g., when log level is ERROR, only ERROR, CRITICAL, and FATAL
-// messages will be printed).
-//
-// Log level options:
-//
-//	TRACE    - 0
-//	DEBUG    - 1
-//	INFO     - 2
-//	WARN     - 3
-//	ERROR    - 4
-//	CRITICAL - 5
-//	FATAL    - 6
-//
-// The default log level without updates is INFO.
-//
-// Parameters:
-//   - args[0] - Log level (int).
-//
-// Returns:
-//   - Throws TypeError if the log level is invalid.
-func LogLevel(this js.Value, args []js.Value) any {
-	return logging.LogLevelJS(this, args)
-}
+	"gitlab.com/elixxir/client/v4/bindings"
+)
 
 // logWriter wraps Javascript callbacks to adhere to the [bindings.LogWriter]
 // interface.
diff --git a/wasm_test.go b/wasm_test.go
index 2d36f7b0c8d3ae5a024ea837f6da9526dc42d3c8..59c4485c516894972b0cbcfa74b49218f28299f8 100644
--- a/wasm_test.go
+++ b/wasm_test.go
@@ -63,6 +63,9 @@ func TestPublicFunctions(t *testing.T) {
 		// C-Library specific bindings not needed by the browser
 		"GetDMInstance":   {},
 		"GetCMixInstance": {},
+
+		// Logging has been moved to startup flags
+		"LogLevel": {},
 	}
 	wasmFuncs := getPublicFunctions("wasm", t)
 	bindingsFuncs := getPublicFunctions(
diff --git a/worker/manager.go b/worker/manager.go
index c38438facdf8b502d8b2fc00ba0fe4855b5cfaf8..1f8328c97f0aa2c9059edc3a6042f423e9c89b17 100644
--- a/worker/manager.go
+++ b/worker/manager.go
@@ -41,7 +41,7 @@ const (
 // put on.
 const receiveQueueChanSize = 100
 
-// ReceptionCallback is the function that handles incoming data from the worker.
+// ReceptionCallback is called with a message received from the worker.
 type ReceptionCallback func(data []byte)
 
 // Manager manages the handling of messages received from the worker.
@@ -64,7 +64,7 @@ type Manager struct {
 
 	// receiveQueue is the channel that all received messages are queued on
 	// while they wait to be processed.
-	receiveQueue chan []byte
+	receiveQueue chan js.Value
 
 	// quit, when triggered, stops the thread that processes received messages.
 	quit chan struct{}
@@ -89,7 +89,7 @@ func NewManager(aURL, name string, messageLogging bool) (*Manager, error) {
 		worker:         js.Global().Get("Worker").New(aURL, opts),
 		callbacks:      make(map[Tag]map[uint64]ReceptionCallback),
 		responseIDs:    make(map[Tag]uint64),
-		receiveQueue:   make(chan []byte, receiveQueueChanSize),
+		receiveQueue:   make(chan js.Value, receiveQueueChanSize),
 		quit:           make(chan struct{}),
 		name:           name,
 		messageLogging: messageLogging,
@@ -138,11 +138,24 @@ func (m *Manager) processThread() {
 		case <-m.quit:
 			jww.INFO.Printf("[WW] [%s] Quitting process thread.", m.name)
 			return
-		case message := <-m.receiveQueue:
-			err := m.processReceivedMessage(message)
-			if err != nil {
-				jww.ERROR.Printf("[WW] [%s] Failed to process received "+
-					"message from worker: %+v", m.name, err)
+		case msgData := <-m.receiveQueue:
+
+			switch msgData.Type() {
+			case js.TypeObject:
+				if msgData.Get("constructor").Equal(utils.Uint8Array) {
+					err := m.processReceivedMessage(utils.CopyBytesToGo(msgData))
+					if err != nil {
+						jww.ERROR.Printf("[WW] [%s] Failed to process received "+
+							"message from worker: %+v", m.name, err)
+					}
+					break
+				}
+				fallthrough
+
+			default:
+				jww.ERROR.Printf("[WW] [%s] Cannot handle data of type %s "+
+					"from worker: %s", m.name, msgData.Type(),
+					utils.JsToJson(msgData))
 			}
 		}
 	}
@@ -174,12 +187,12 @@ func (m *Manager) SendMessage(
 			"ID %d going to worker: %+v", m.name, msg, tag, id, err)
 	}
 
-	go m.postMessage(string(payload))
+	go m.postMessage(payload)
 }
 
 // receiveMessage is registered with the Javascript event listener and is called
 // every time a new message from the worker is received.
-func (m *Manager) receiveMessage(data []byte) {
+func (m *Manager) receiveMessage(data js.Value) {
 	m.receiveQueue <- data
 }
 
@@ -303,7 +316,7 @@ func (m *Manager) addEventListeners() {
 	// occurs when a message is received from the worker.
 	// Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker/message_event
 	messageEvent := js.FuncOf(func(_ js.Value, args []js.Value) any {
-		m.receiveMessage([]byte(args[0].Get("data").String()))
+		m.receiveMessage(args[0].Get("data"))
 		return nil
 	})
 
@@ -336,20 +349,19 @@ func (m *Manager) addEventListeners() {
 
 // postMessage sends a message to the worker.
 //
-// message is the object to deliver to the worker; this will be in the data
-// field in the event delivered to the worker. It must be a js.Value or a
-// primitive type that can be converted via js.ValueOf. The Javascript object
-// must be "any value or JavaScript object handled by the structured clone
-// algorithm, which includes cyclical references.". See the doc for more
-// information.
+// msg is the object to deliver to the worker; this will be in the data
+// field in the event delivered to the worker. It must be a transferable object
+// because this function transfers ownership of the message instead of copying
+// it for better performance. See the doc for more information.
 //
 // If the message parameter is not provided, a SyntaxError will be thrown by the
 // parser. If the data to be passed to the worker is unimportant, js.Null or
 // js.Undefined can be passed explicitly.
 //
 // Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker/postMessage
-func (m *Manager) postMessage(msg any) {
-	m.worker.Call("postMessage", msg)
+func (m *Manager) postMessage(msg []byte) {
+	buffer := utils.CopyBytesToJS(msg)
+	m.worker.Call("postMessage", buffer, []any{buffer.Get("buffer")})
 }
 
 // terminate immediately terminates the Worker. This does not offer the worker
diff --git a/worker/thread.go b/worker/thread.go
index 07591fc7d8f7a905f521b98215096b2d8d5ba044..203b00302a97c22f4e44501e2c721b52312c14ab 100644
--- a/worker/thread.go
+++ b/worker/thread.go
@@ -20,8 +20,9 @@ import (
 	"gitlab.com/elixxir/xxdk-wasm/utils"
 )
 
-// ThreadReceptionCallback is the function that handles incoming data from the
-// main thread.
+// ThreadReceptionCallback is called with a message received from the main
+// thread. Any bytes returned are sent as a response back to the main thread.
+// Any returned errors are printed to the log.
 type ThreadReceptionCallback func(data []byte) ([]byte, error)
 
 // ThreadManager queues incoming messages from the main thread and handles them
@@ -34,9 +35,9 @@ type ThreadManager struct {
 	// main thread keyed on the callback tag.
 	callbacks map[Tag]ThreadReceptionCallback
 
-	// receiveQueue is the channel that all received messages are queued on
-	// while they wait to be processed.
-	receiveQueue chan []byte
+	// receiveQueue is the channel that all received MessageEvent.data are
+	// queued on while they wait to be processed.
+	receiveQueue chan js.Value
 
 	// quit, when triggered, stops the thread that processes received messages.
 	quit chan struct{}
@@ -56,7 +57,7 @@ func NewThreadManager(name string, messageLogging bool) *ThreadManager {
 	tm := &ThreadManager{
 		messages:       make(chan js.Value, 100),
 		callbacks:      make(map[Tag]ThreadReceptionCallback),
-		receiveQueue:   make(chan []byte, receiveQueueChanSize),
+		receiveQueue:   make(chan js.Value, receiveQueueChanSize),
 		quit:           make(chan struct{}),
 		name:           name,
 		messageLogging: messageLogging,
@@ -88,14 +89,24 @@ func (tm *ThreadManager) processThread() {
 		case <-tm.quit:
 			jww.INFO.Printf("[WW] [%s] Quitting worker process thread.", tm.name)
 			return
-		case message := <-tm.receiveQueue:
-			if tm.messageLogging {
-				jww.INFO.Printf("[WW] Worker processors received message: %q", message)
-			}
-			err := tm.processReceivedMessage(message)
-			if err != nil {
-				jww.ERROR.Printf("[WW] [%s] Failed to receive message from "+
-					"main thread: %+v", tm.name, err)
+		case msgData := <-tm.receiveQueue:
+
+			switch msgData.Type() {
+			case js.TypeObject:
+				if msgData.Get("constructor").Equal(utils.Uint8Array) {
+					err := tm.processReceivedMessage(utils.CopyBytesToGo(msgData))
+					if err != nil {
+						jww.ERROR.Printf("[WW] [%s] Failed to process message "+
+							"received from main thread: %+v", tm.name, err)
+					}
+					break
+				}
+				fallthrough
+
+			default:
+				jww.ERROR.Printf("[WW] [%s] Cannot handle data of type %s "+
+					"from main thread: %s",
+					tm.name, msgData.Type(), utils.JsToJson(msgData))
 			}
 		}
 	}
@@ -128,7 +139,7 @@ func (tm *ThreadManager) SendMessage(tag Tag, data []byte) {
 			"to main: %+v", tm.name, msg, tag, err)
 	}
 
-	go tm.postMessage(string(payload))
+	go tm.postMessage(payload)
 }
 
 // sendResponse sends a reply to the main thread with the given tag and ID.
@@ -151,14 +162,14 @@ func (tm *ThreadManager) sendResponse(tag Tag, id uint64, data []byte) error {
 			"%d going to main: %+v", msg, tag, id, err)
 	}
 
-	go tm.postMessage(string(payload))
+	go tm.postMessage(payload)
 
 	return nil
 }
 
 // receiveMessage is registered with the Javascript event listener and is called
 // every time a new message from the main thread is received.
-func (tm *ThreadManager) receiveMessage(data []byte) {
+func (tm *ThreadManager) receiveMessage(data js.Value) {
 	tm.receiveQueue <- data
 }
 
@@ -224,7 +235,7 @@ func (tm *ThreadManager) addEventListeners() {
 	// occurs when a message is received from the main thread.
 	// Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker/message_event
 	messageEvent := js.FuncOf(func(_ js.Value, args []js.Value) any {
-		tm.receiveMessage([]byte(args[0].Get("data").String()))
+		tm.receiveMessage(args[0].Get("data"))
 		return nil
 	})
 
@@ -260,10 +271,16 @@ func (tm *ThreadManager) addEventListeners() {
 // aMessage must be a js.Value or a primitive type that can be converted via
 // js.ValueOf. The Javascript object must be "any value or JavaScript object
 // handled by the structured clone algorithm". See the doc for more information.
+
+// aMessage is the object to deliver to the main thread; this will be in the
+// data field in the event delivered to the thread. It must be a transferable
+// object because this function transfers ownership of the message instead of
+// copying it for better performance. See the doc for more information.
 //
 // Doc: https://developer.mozilla.org/docs/Web/API/DedicatedWorkerGlobalScope/postMessage
-func (tm *ThreadManager) postMessage(aMessage any) {
-	js.Global().Call("postMessage", aMessage)
+func (tm *ThreadManager) postMessage(aMessage []byte) {
+	buffer := utils.CopyBytesToJS(aMessage)
+	js.Global().Call("postMessage", buffer, []any{buffer.Get("buffer")})
 }
 
 // close discards any tasks queued in the worker's event loop, effectively