Introducing Bytekit.app: Privacy Oriented Web App for developers
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.zstFor 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
embedmeans 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-MatchandVaryHTTP headers - it ensures a correct
Content-Typefor all requests - it has a rewrite rules included - so paths never ends with
.html - it takes a care about
Content-Security-Policyheaders
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/bytekitThis 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/mageAnd 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].