MQ Studio Architecture
September 6, 2026 · View on GitHub
Process model
React UI (system WebView)
│ Wails bindings — direct, in-process calls
internal/bridge ── the only layer allowed to reshape data for the UI
│
internal/service ── domain logic
│
RocketMQ Admin API / local encrypted settings
The application is a single process. The UI runs in the platform WebView (WKWebView on macOS, WebView2 on Windows, WebKitGTK on Linux) and reaches Go through generated bindings, so there is no local HTTP server, no auth token and no child process to supervise.
The bridge layer
internal/bridge exposes one Wails service per domain. Its methods delegate
straight to internal/service, and it is the only place that reshapes data on
the way out:
- Stored AccessKey / SecretKey values never leave the Go process. The bridge
replaces them with
accessKeyConfigured/secretKeyConfiguredflags. - Connection and settings updates carry an explicit credential mode:
preserve,replaceorclear. - Config file paths and plaintext exports stay in Go:
SystemServiceowns the file dialogs, reads and writes the file, and returns only the chosen path. - External links are checked against a host allowlist before being handed to the OS browser.
wails3 generate bindings writes the TypeScript for these services into
frontend/bindings/, typed directly from the Go structs. npm run check fails
if the committed bindings drift from the Go source.
Frontend seams
The UI never calls a binding directly. Two modules sit in between:
frontend/src/api/*.tswraps each bound service and is the only place that knows the binding shapes.frontend/src/api/models.tsre-exports the generated domain types under the vocabulary the pages use, including friendlier names for the generated enums.
Window chrome (minimise, maximise, close, drag regions) is driven by the Wails window runtime from the frontend; only the native background colour, which has to stay in step with the light/dark theme, goes through a Go service.
macOS title bar
The renderer paints the title bar itself, so the native traffic lights have to
line up with it. Wails exposes no equivalent of Electron's
trafficLightPosition, so internal/macwindow moves the standard window
buttons through a small Objective-C shim. AppKit rebuilds the themed frame on
resize and when leaving fullscreen, which resets the buttons, so the shim keeps
a positioner attached to the window and re-applies the offset on those
notifications. It leaves the buttons alone while the window is fullscreen,
where AppKit owns them.
The geometry lives in two places that must agree: titleBarHeight and
trafficLightLeft in main.go, and the .tb2 / .tb2--mac rules in
frontend/src/design/tokens.css, which keep the title bar clear of the
buttons instead of drawing stand-ins for them.
Repository layout
main.go Wails application entrypoint
internal/
bridge/ Wails services exposed to the frontend
app/ Service wiring
service/ Domain services, one package per domain
driver/ The broker seam: ports, capabilities, conformance
rocketmq/ RocketMQ driver
rabbitmq/ RabbitMQ driver
kafka/ Kafka driver
pulsar/ Pulsar driver
redisstream/ Redis Stream driver
mqtt/ MQTT driver, with emqx/ for the vendor management API
nats/ NATS driver, JetStream and the $SYS account
activemq/ ActiveMQ driver: Classic and Artemis over Jolokia
nsq/ NSQ driver: nsqd and nsqlookupd over HTTP
sqs/ Amazon SQS driver: the AWS API, with no address to dial
googlepubsub/ Google Pub/Sub driver: topics and subscriptions, no address
azureservicebus/ Azure Service Bus driver: AMQP for messages, Atom for topology
kinesis/ Amazon Kinesis driver: streams and shards, with no address to dial
ibmmq/ IBM MQ driver: the mqweb server's two REST interfaces, no wire client
solace/ Solace driver: SEMP v2, and REST messaging on its own port
model/ Domain models and the capability vocabulary
crypto/ Local encryption helpers
storage/ On-disk layout and atomic writes
update/ In-app updater: check, download, verify, install
macwindow/ Native macOS chrome Wails does not expose (cgo)
tray/ System tray
frontend/
bindings/ Generated TypeScript bindings (committed)
src/api/ Binding wrappers, domain types, platform access
src/components/ shadcn/ui primitives and the app composites over them
src/design/ The shell, the page registry, and every board
src/hooks/ React hooks / providers
src/mq/ Per-family attribute readers, navigation, capabilities
src/lib/ Pure helpers: formatting, alert rules, storage
src/i18n/ Locale bundles, zh and en
src/styles/ Global CSS and early theme bootstrap
build/ Wails build assets and per-platform Taskfiles
scripts/ Version check, e2e seeds, packaging asset generators
tests/
e2e/rocketmq/ RocketMQ e2e environment
e2e/rocketmq-acl/ RocketMQ with ACL on, for the access-control tests
e2e/rabbitmq/ RabbitMQ with the optional plugins on
e2e/rabbitmq-plain/ RabbitMQ with none of them, for the degraded paths
e2e/kafka/ Three-broker KRaft cluster
e2e/kafka-secure/ Kafka with SASL and an authorizer
e2e/pulsar/ Pulsar standalone with the admin API
e2e/redis/ Redis standalone with ACL users
e2e/redis-cluster/ Redis in cluster mode, for the multi-master paths
e2e/mqtt/ Mosquitto: a $SYS tree and no management API
e2e/mqtt-emqx/ EMQX: a management API and no readable $SYS
e2e/nats/ Three-server NATS cluster with JetStream and $SYS
e2e/nats-plain/ NATS with neither, for the degraded paths
e2e/activemq/ ActiveMQ Artemis with its console and AMQP acceptor
e2e/activemq-classic/ ActiveMQ Classic, the family's other product
e2e/nsq/ Two nsqd and two nsqlookupd, with a consumer attached
e2e/sqs/ LocalStack running sqs, reached through the endpoint override
e2e/google-pubsub/ Google's own Pub/Sub emulator, reached through the host option
e2e/azure-servicebus/ Microsoft's Service Bus emulator, and the SQL Server it needs
e2e/kinesis/ LocalStack running kinesis, on a port of its own beside sqs
e2e/ibmmq/ IBM's own developer queue manager, and the mqweb server beside it
e2e/solace/ Solace PubSub+ Standard, the vendor's own free edition
throughput-load/ Load generator for the throughput charts (own module)
Build
Taskfile.yml drives everything through the wails3 CLI: wails3 task dev for
hot reload, wails3 task build for a binary, wails3 task package for a
distributable. The app version lives in package.json and is injected into the
binary with -ldflags "-X main.version=...".
Bumping it means editing package.json, both lockfiles, frontend/package.json
and info.version in build/config.yml, then running
wails3 task common:update:build-assets to regenerate the committed platform
manifests. Those manifests are what the packaged artifacts declare to the OS, so
npm run check:version verifies them too and names any that are stale.
The app id is com.mqstudio.app and the user data directory is mq-studio.
Both were renamed along with the app, and nothing carries pre-rename data
across: an install that predates the rename keeps its own directory untouched
and MQ Studio starts empty. Copying those files over by hand does not help
either, because crypto.hkdfInfoPrefix feeds key derivation and was renamed
too, so the stored ENC: values no longer decrypt.
.github/workflows/ci.yml runs the same gate on pushes and pull requests, but
splits it across jobs that run at the same time: the frontend build and tests
need neither Go nor Docker, the static checks are the only thing that needs the
GTK headers, and the live suites are sharded one job per broker family, each
starting only its own compose stacks.
That last split is not free. MQ_STUDIO_E2E_FAMILIES tells internal/e2e
which families a shard is responsible for, and the ones it does not name skip -
so a test can now go unrun without anything turning red, which is how issue #48
went unnoticed. Two things stop that. TestEveryFamilyHasACIShard pins
e2e.AllFamilies against the workflow's shard matrix, and the coverage job
runs scripts/ci-coverage.mjs over every shard's go test -json output to
assert that each test passed in at least one of them. A skip only counts as
deliberate if it did not come from the gate, which is what e2e.SkipMarker
distinguishes. package waits on that job, so an artifact is never produced
from a run that tested less than it should have.
release.yml packages a tag. Its runner matrix follows what each platform needs
to compile: macOS builds both slices from one SDK and joins them with lipo,
Windows cross-compiles both architectures from one runner because it does not
need cgo, and Linux does, so each architecture gets a runner of its own.