I recently deployed my tiny Web application bytekit.app. This is tool for developers doing stuff like JWT, base64, hashes and so on. I wanted something easy to use, privacy-oriented, and fast.

Bytekit is written in Go. That might sound an odd choice for a browser app, but Go itself targets WebAssembly. Using GOARCH=wasm together with go-app let me run Go in the browser.

The motivation

I mostly use the command line, but I still rely on online tools from time to time. Many are available, yet most have annoying issues:

  • no privacy - “we and 635 trustworthy partners swear we won’t share your data”
  • dubious security - is data processing done client-side or server-side? Are results shared with third parties?
  • no consistency - each tool uses a different UI, and some need extra clicks for simple actions
  • no source code

My main motivation was a better JWT decoder. I personally find the UI of https://jwt.iot as confusing with regards to verifying of signatures. So I wanted to have a cleaner and simpler interface. As a GNOME user, I like the Adwaita and GNOME HIG in general. So the goal was to have something similar to Dev Toolbox only on web.

1. Security first

Developer tools often handle sensitive data like auth tokens and API keys. Sending that data through a web app can be risky. Bytekit runs entirely in the browser

  • nothing is ever sent outside your computer. You can verify this easily with your browser’s developer tools.

2. Privacy

The Privacy policy for https://bytekit.app is simple. I run the service alone - there are no partners, no trackers, no ads and no analytics. The HTTP requests are sent to journald and are placed to 100MB big in-memory ring buffer together with all other services. I inspect them manually from time to time for a debugging purposes.

The site links to its source code hosted on Codeberg and the it runs from quay.io/vyskocilm/bytekit container image. So self-hosting is always an option.

3. Open source / Free software

I chose AGPLv3 for the main app because I want to encourage people to self-host, reuse, or improve the project. At the same time, the license asks that any changes to be shared back with the community.

4. Easy to use

I avoided forcing clicks for simple tasks. The app runs locally in your browser and processes data as you type. The UI is meant to be clear and minimal — no unnecessary bells and whistles. One example is the hash/checksum page. Instead of making you choose an algorithm, it computes them all at once.

Using Go on a frontend?

Let’s start with an elephant in the room first. Which is the artifacts size.

$ ls -lh public/web/*.wasm*
-rw-r--r--. 1 michal michal 9.1M Jan  7 16:14 public/web/app.wasm
-rw-r--r--. 1 michal michal 2.4M Jan  7 16:14 public/web/app.wasm.gz
-rw-r--r--. 1 michal michal 2.1M Jan  7 16:14 public/web/app.wasm.zst

For a perspective, a main Javascript file from YouTube says 1,835kB (resource size 9,446 kB). So given the fact this comes with full Go runtime, I consider this as pretty small and acceptable. The wasm binary and other assets are compressed to save bandwidth. Clients request the compressed version via Content Negotiation (Accept-Encoding). Browsers do this by default, so users get the smaller transfer automatically.

The server actually refuses to serve an uncompressed wasm file.

The application itself is just a directory of a static assets inside a public/ folder. I also included optional Go net/http server to serve those files. That server has a few advantages over serving static files:

  • it is compatible with OCI containers and can be placed behind a reverse proxy
  • use of embed means the app is a single executable which works from memory
    • it doesn’t need external services, neither a filesystem and can be deployed anywhere
  • it handles content negotiation for compressed assets and refuses to serve a plain wasm
  • it makes app cache friendly by handling Etags, If-None-Match and Vary HTTP headers
  • it ensures a correct Content-Type for all requests
  • it has a rewrite rules included - so paths never ends with .html
  • it takes a care about Content-Security-Policy headers

How the frontend Go look like?

This is not a tutorial, only a short showcase of a frontent Go. The example is a base64 encoding/decoding page. All UI components are plain Go structs which must embed the framework’s app.Compo.

package base64

import (
  bk "codeberg.org/vyskocilm/bytekit/internal/kit"

  "github.com/maxence-charriere/go-app/v10/pkg/app"
)
type Content struct {
  app.Compo

  showError bk.ToastFunc
  encoded   string
  decoded   string
  encoding  *base64.Encoding
}

To render a content as HTML one must implement the Render method from app.Composer. Those components are HTML components defined by go-app or those can be other structs implementing app.Composer interface. The app defines a few reusable UI components this way, like and InputBox.

func (c *Content) Render() app.UI {
  return app.Div().Body(
    &title{
      showError:  c.showError,
      onSelected: c.onEncodingChanged,
    },
    bk.InputBox().
      SetText(c.encoded).
      ShowError(c.showError).
      Label("Base64 encoded content").
      OnChangedText(c.onEncodedChange),
    bk.InputBox().
      SetText(c.decoded).
      ShowError(c.showError).
      Label("Decoded text").
      OnChangedText(c.onDecodedChange),
    )
}

The interactivity is done via callbacks - the framework calls a defined callback a particular event.

func (c *Content) onDecodedChange(ctx app.Context, decoded string) {
  ctx.Async((func() {
    encoded := c.encoding.EncodeToString([]byte(decoded))
    ctx.Dispatch(func(ctx app.Context) {
      c.encoded = encoded
      c.decoded = decoded
    })
  }))
}

In the browser, JavaScript runs on a single main thread. Heavy computations block that thread and freeze the UI because no other JavaScript can run at the same time. There is a concept of a Worker which lets you run code off the main thread.

In Concurrency model of go-app all things runs in a single UI goroutine which updates the UI and a state of the components. Async then runs the code inside another goroutine, which do not block the main UI. When code is finished running, the Dispatch call passes results safely back to the UI goroutine.

Javascript interop

No frontend code can live without an ability to call Javascript. This is the only way for accessing browser APIs. The app itself exposes some of the DOM API.

  ctx.Async(func() {
    app.Window().Get("navigator").
      Get("clipboard").
      Call("writeText", text)
  })

An access to elements triggering an event is possible too.

  ctx.Async(func() {
    title := ctx.JSSrc().Get("innerText").String()
  })

As well as a support for Promises, which is the only way of reading from a clipboard.

clipboard := app.Window().Get("navigator").
  Get("clipboard")
promise := clipboard.Call("readText")
thenFunc := app.FuncOf(...)
catchFunc := app.FuncOf(...)
promise.Call("then", thenFunc).Call("catch", catchFunc)

The frontend itself is generated by https://go-app.dev via

$ GOARCH=wasm GOOS=js go build -o "public/web/app.wasm" \
codeberg.org/vyskocilm/bytekit/cmd/bytekit

This populates the content of a public folder where all web assets are placed. The go-app framework is the one doing all the magic behind the scenes.

So how’s dev experience without npm?

The npm (pnpm, yarn, …) is pervasive in a FE development world. Not using the standard frontend toolbox means you often ends up solving problems yourself. That said, using npm for parts of the workflow can be reasonable. For example, bytekit uses purgecss to minify bulma.css by removing unused declarations, making assets as lean as possible.

On the other hand sometimes writing a part of a build chain is easy and very convenient. As an example code compressing assets with gzip and zstd turned out to be about 100 lines of straightforward, idiomatic Go code. This is a tradeoff worth making. Especially the build system used, https://magefile.org/, is Go code itself.

So there are gophers all the way down.

Having complete codebase in a single language including the build system makes the developer experience very pleasant. There is a little magic and build logic is only a code inside internal/mage and can be inspected, changed or tested exactly like the rest of the other code. The top level mage.go merely defines a build targets.

And as go supports go tool the build system is a dev dependency listed in go.mod.

tool github.com/magefile/mage

And a CI is a buch of calls of a form go tool mage $target

  •  smaller ecosystem to pull from
  •  coherent codebase - everything is written in one language
  •  speed - once is built, it runs faster than npm tooling

Closing thoughts

Bytekit started as a small experiment in running Go on the web. It stays small and focused on a few useful tools. It’s privacy-first, open source, and easy to self‑host if you prefer.

Try it at [https://bytekit.app]. The source is on Codeberg and the container image is on quay.io, so running your own copy is straightforward. Issues, ideas, and pull requests are welcome.

If you like the approach or find a rough edge, tell me - especially about the UI and privacy parts. Thanks for reading and for trying Bytekit.

Logo is remixed [https://github.com/egonelbre/gophers/blob/master/vector/science/power-to-the-linux.svg].