glowvm

Run Gleam serverlessly via AtomVM compiled to WebAssembly

GlowVM compiles AtomVM to wasm32-wasi, with WASI bindings and a few shims and NIFs for compatibility, so Gleam’s Erlang target can run inside a Cloudflare Worker (or any WASI-compliant host). Gleam modules are compiled and packed into an AtomVM PackBeam (.avm) file, loaded by AtomVM at request time.

Docs — a Giolt project.

Status: experimental, not ready for production use. GlowVM inherits AtomVM’s constraints — 64-bit signed integers only, no hot code swapping, no distributed Erlang, no C-based NIFs, limited non-byte-aligned bitstring matching, partial OTP stdlib coverage, and a stateless, request-scoped execution model. Prefer pure-Gleam dependencies and test early on GlowVM rather than assuming a successful build implies runtime correctness.

Known gaps beyond that:

Setup

Requires macOS or Linux.

ToolVersionUsed for
cmake>= 3.20building native/
Erlang/OTP>= 28erlc, and Gleam’s Erlang target
Gleam>= 1.18building this library
Binaryenanywasm-opt / wasm-dis / wasm-as, used to repack the WASM output into a form workerd accepts
wasi-sdk34the WASI clang toolchain
justanyThe runner

On macOS, the first four are a brew install cmake erlang gleam binaryen just away.

wasi-sdk has no Homebrew formula: download the release archive for your platform from the v34 release page and extract it to ~/.wasi-sdk. scripts/build-atomvm.sh looks for it at ~/.wasi-sdk, /opt/wasi-sdk, /usr/local/wasi-sdk, and ./wasi-sdk in this repo, or at $WASI_SDK_PATH if set.

Build

just build

Clones the vendor/AtomVM submodule on first run if it isn’t present, builds native/ with cmake, repacks and optimizes the WASM with Binaryen, and writes priv/glowvm.wasm plus the Erlang stdlib subset into priv/stdlib/.

gleam build      # compile this library on its own

Building an app

glowvm is the runtime only. The giolt CLI compiles your project, checks that your entry module exports handle, packs it into an AtomVM PackBeam (app.avm) and copies the runtime files below into your output directory. Your project does not depend on glowvm directly.

pub fn handle(req: wisp.Request) -> wisp.Response {
  wisp.ok() |> wisp.string_body("hello from glowvm")
}

giolt generates a start/0 that calls glowvm.serve(fn(req) { your.handle(req) }). The handler has to be called through a lambda: passing your.handle by reference makes the packer drop your modules.

Runtime contract

What giolt relies on, and what a change here has to keep stable:

PathPurpose
glowvm.serve/1Guest-side bridge: reads one request (Erlang external term format) from stdin, calls the handler, writes the response to stdout. One VM run per request
priv/glowvm.wasmAtomVM compiled to wasm32-wasi
priv/index.js, priv/index.min.jsWorker entry that loads ./glowvm.wasm and ./app.avm and answers fetch
priv/stdlib/*.beamCurated Erlang stdlib subset, packed into every app

The protocol between the host JavaScript and the guest bridge is versioned with this package: a change to it is a minor version bump while glowvm is 0.x, and giolt declares the range of glowvm versions it supports.

Issues

Found a bug or have a request? File it in withgiolt/issues, the central issue tracker for all Giolt projects.

Attribution

This project is inspired by Jaketdaniels/elixir-workers. The code has been heavily modified to remove the 2 pass system, use latest AtomVM with OTP >=28, add support for environment variables, Gleam support and more.

✨ Search Document